1、promise、async、await
const Koa = require('koa') const app = new Koa() // 應用程序對象 有很多中間件 // 發送HTTP KOA 接收HTTP(使用中間件,中間件實際就是函數) // await: 1、求值關鍵字,不僅是promise,表達式也可以(100*100) // 2、阻塞當前線程 // async 只要函數前面加了async,返回的值就會被promise包裹 // 注冊 app.use(async (ctx, next) => { // ctx 上下文 console.log('1') const a =await next() console.log(a) console.log('2') }) app.use(async (ctx, next) => { console.log('3') console.log('4') return 'abc' }) app.listen(3300)
async 只要函數前面加了async,返回的值就會被promise包裹
await: 1、求值關鍵字,不僅是promise,表達式也可以(100*100)
2、阻塞當前線程
2、
第一種情況
app.use((ctx, next) => { // ctx 上下文 console.log('1') next() console.log('2') }) app.use(async (ctx, next) => { console.log('3') next() console.log('4') }) app.listen(3300)
這是因為node的洋蔥模型,next()為中間分割點
第二種情況
// 注冊 app.use((ctx, next) => { // ctx 上下文 console.log('1') next() console.log('2') }) app.use(async (ctx, next) => { console.log('3') const axios = require('axios') const res =await axios.get('http://7yue.pro') next() console.log('4') }) app.listen(3300)
async await阻塞了當前線程,所以就跳轉到其他線程
第三章情況:要想讓中間件一直都執行洋蔥模型,就需要在next前面使用await
// 注冊 app.use(async (ctx, next) => { // ctx 上下文 console.log('1') await next() console.log('2') }) app.use(async (ctx, next) => { console.log('3') const axios = require('axios') const res =await axios.get('http://7yue.pro') await next() console.log('4') }) app.listen(3300)
3、koa-router
https://www.npmjs.com/package/koa-router
const Koa = require('koa') const Router = require('koa-router') const app = new Koa() const router = new Router() router.get('/classic/latest', (ctx, next) => { ctx.body = {key: 'classic'} // 返回的信息 }) app .use(router.routes()) .use(router.allowedMethods) app.listen(3300)
4、exports和module.exports的區別
真正的是module.exports
module.exports是一個對象 module.exports={}
exports是module.exports的引用,類似於: var a= {},b=a;
5、使用require-directory批量加載router
https://www.npmjs.com/package/require-directory
const Koa = require('koa') const requireDirectory = require('require-directory') const Router = require('koa-router') const app = new Koa() // 通過requireDirectory獲取app/api/v1下的所有routers // visit: whenLoadModule 函數 const modules = requireDirectory(module, './app/api/v1', { visit: whenLoadModule }) // 用來判斷引入的是router function whenLoadModule(obj) { if(obj instanceof Router) { app.use(obj.routes()) } } app.listen(3300)
6、NodeJs中process.cwd()與__dirname的區別
process.cwd() 是當前執行node命令時候的文件夾地址 ——工作目錄,保證了文件在不同的目錄下執行時,路徑始終不變
__dirname 是被執行的js 文件的地址 ——文件所在目錄
Nodejs官方文檔上的解釋:
=> process.cwd(): The process.cwd()
method returns the current working directory of theNode.js process.
意思很明了啦,就是說process.cwd()返回的是當前Node.js進程執行時的工作目錄
那么來看看__dirname的官方解釋:
=> __dirname: 當前模塊的目錄名。 等同於 __filename
的 path.dirname()
。__dirname
實際上不是一個全局變量,而是每個模塊內部的。
7、Node koa2中獲取參數
http://localhost:3300/v1/:1/classic/latest?param=8yue
router.post('/v1/:id/classic/latest', (ctx, next) => { const path = ctx.params // 獲取的是:id里的值 1 const query = ctx.request.query // 獲取的是?param的值 8yue const headers = ctx.request.header // header傳遞的值 const body = ctx.request.body // body里的值,json格式 ctx.body = { key: 'classic' } })
8、如果是異步操作,而且用的是promise,一定要加上async、await
而且要用try ctach處理異常
9、定義錯誤基類
class HttpException extends Error { constructor (msg = '服務器異常', errorCode = 10000, code = 400) { super() this.msg = msg this.errorCode = errorCode this.code = code } }
使用
const { HttpException } = require('../../../core/http-exception') const error = new HttpException() throw error
在中間件里throw出error,這樣才能被try...catch捕獲
10、特定異常類
class ParameterException extends HttpException { constructor (msg, errorCode) { super() this.errorCode = errorCode || 10000 this.msg = msg || '參數錯誤' this.code = 400 } } module.exports = { HttpException, ParameterException }
使用
const { ParameterException } = require('../../../core/http-exception') const error = new ParameterException() throw error
優化
每次使用錯誤的時候,都需要引入,然后new,可以把這個錯誤放到global里面
static loadHttpException() { const errors = require('./http-exception') global.errs = errors }
使用
const error = new global.errs.ParameterException() throw error
這倆種方法都可以使用,看自己喜歡
11、async、await進行全局異常處理可以使用try...catch
在項目中,我們如果給每一個使用async、await的函數使用try...catch,這樣太麻煩,我們可以定義一個全局異常處理中間件
const { HttpException } = require('../core/http-exception') const catchError = async (ctx, next) => { try { await next() // 有了next,函數調用后就會觸發 } catch (error) { // 判斷error是否是HttpException if (error instanceof HttpException) { // 返回的錯誤信息 ctx.body = { mag: error.msg, error_code: error.errorCode, request: `${ctx.method} ${ctx.path}` } ctx.status = error.code } } } module.exports = catchError
app.js
const catchError = require('./middlewares/exception') app.use(catchError)
這樣就注冊了一個全局異常處理中間件,只要Node里有中間件運行時拋出異常,就會被這個中間件捕獲
12、LinValidator校驗器 http://doc.cms.7yue.pro/lin/server/koa/validator.html#類校驗
是用方式:
(1)、npm install validator --save-dev
(2)、創建lin-validator.js和util.js
lin-validator.js
/** * Lin-Validator v1 * 作者:7七月 * 微信公眾號:林間有風 */ const validator = require('validator') const { ParameterException } = require('./http-exception') const { get, last, set, cloneDeep } = require("lodash") const { findMembers } = require('./util') class LinValidator { constructor() { this.data = {} this.parsed = {} } _assembleAllParams(ctx) { return { body: ctx.request.body, query: ctx.request.query, path: ctx.params, header: ctx.request.header } } get(path, parsed = true) { if (parsed) { const value = get(this.parsed, path, null) if (value == null) { const keys = path.split('.') const key = last(keys) return get(this.parsed.default, key) } return value } else { return get(this.data, path) } } _findMembersFilter(key) { if (/validate([A-Z])\w+/g.test(key)) { return true } if (this[key] instanceof Array) { this[key].forEach(value => { const isRuleType = value instanceof Rule if (!isRuleType) { throw new Error('驗證數組必須全部為Rule類型') } }) return true } return false } validate(ctx, alias = {}) { this.alias = alias let params = this._assembleAllParams(ctx) this.data = cloneDeep(params) this.parsed = cloneDeep(params) const memberKeys = findMembers(this, { filter: this._findMembersFilter.bind(this) }) const errorMsgs = [] // const map = new Map(memberKeys) for (let key of memberKeys) { const result = this._check(key, alias) if (!result.success) { errorMsgs.push(result.msg) } } if (errorMsgs.length != 0) { throw new ParameterException(errorMsgs) } ctx.v = this return this } _check(key, alias = {}) { const isCustomFunc = typeof (this[key]) == 'function' ? true : false let result; if (isCustomFunc) { try { this[key](this.data) result = new RuleResult(true) } catch (error) { result = new RuleResult(false, error.msg || error.message || '參數錯誤') } // 函數驗證 } else { // 屬性驗證, 數組,內有一組Rule const rules = this[key] const ruleField = new RuleField(rules) // 別名替換 key = alias[key] ? alias[key] : key const param = this._findParam(key) result = ruleField.validate(param.value) if (result.pass) { // 如果參數路徑不存在,往往是因為用戶傳了空值,而又設置了默認值 if (param.path.length == 0) { set(this.parsed, ['default', key], result.legalValue) } else { set(this.parsed, param.path, result.legalValue) } } } if (!result.pass) { const msg = `${isCustomFunc ? '' : key}${result.msg}` return { msg: msg, success: false } } return { msg: 'ok', success: true } } _findParam(key) { let value value = get(this.data, ['query', key]) if (value) { return { value, path: ['query', key] } } value = get(this.data, ['body', key]) if (value) { return { value, path: ['body', key] } } value = get(this.data, ['path', key]) if (value) { return { value, path: ['path', key] } } value = get(this.data, ['header', key]) if (value) { return { value, path: ['header', key] } } return { value: null, path: [] } } } class RuleResult { constructor(pass, msg = '') { Object.assign(this, { pass, msg }) } } class RuleFieldResult extends RuleResult { constructor(pass, msg = '', legalValue = null) { super(pass, msg) this.legalValue = legalValue } } class Rule { constructor(name, msg, ...params) { Object.assign(this, { name, msg, params }) } validate(field) { if (this.name == 'optional') return new RuleResult(true) if (!validator[this.name](field + '', ...this.params)) { return new RuleResult(false, this.msg || this.message || '參數錯誤') } return new RuleResult(true, '') } } class RuleField { constructor(rules) { this.rules = rules } validate(field) { if (field == null) { // 如果字段為空 const allowEmpty = this._allowEmpty() const defaultValue = this._hasDefault() if (allowEmpty) { return new RuleFieldResult(true, '', defaultValue) } else { return new RuleFieldResult(false, '字段是必填參數') } } const filedResult = new RuleFieldResult(false) for (let rule of this.rules) { let result = rule.validate(field) if (!result.pass) { filedResult.msg = result.msg filedResult.legalValue = null // 一旦一條校驗規則不通過,則立即終止這個字段的驗證 return filedResult } } return new RuleFieldResult(true, '', this._convert(field)) } _convert(value) { for (let rule of this.rules) { if (rule.name == 'isInt') { return parseInt(value) } if (rule.name == 'isFloat') { return parseFloat(value) } if (rule.name == 'isBoolean') { return value ? true : false } } return value } _allowEmpty() { for (let rule of this.rules) { if (rule.name == 'optional') { return true } } return false } _hasDefault() { for (let rule of this.rules) { const defaultValue = rule.params[0] if (rule.name == 'optional') { return defaultValue } } } } module.exports = { Rule, LinValidator }
util.js
const jwt = require('jsonwebtoken') /*** * */ const findMembers = function (instance, { prefix, specifiedType, filter }) { // 遞歸函數 function _find(instance) { //基線條件(跳出遞歸) if (instance.__proto__ === null) return [] let names = Reflect.ownKeys(instance) names = names.filter((name) => { // 過濾掉不滿足條件的屬性或方法名 return _shouldKeep(name) }) return [...names, ..._find(instance.__proto__)] } function _shouldKeep(value) { if (filter) { if (filter(value)) { return true } } if (prefix) if (value.startsWith(prefix)) return true if (specifiedType) if (instance[value] instanceof specifiedType) return true } return _find(instance) } const generateToken = function(uid, scope){ const secretKey = global.config.security.secretKey const expiresIn = global.config.security.expiresIn const token = jwt.sign({ uid, scope },secretKey,{ expiresIn }) return token } module.exports = { findMembers, generateToken, }
(3)、定義自己的validator.js文件
const { LinValidator, Rule } = require('../../core/lin-validator') class PositiveIntegerValidator extends LinValidator { constructor () { super() this.id = [ // 校驗的參數是:id,校驗的函數名是:isInt,提示信息是:需要是正整數,其他條件是min: 1 new Rule('isInt', '需要是正整數', { min: 1 }) ] } } module.exports = { PositiveIntegerValidator }
(4)、使用
const { PositiveIntegerValidator } = require('../../validators/validator') const v = new PositiveIntegerValidator().validate(ctx)
// 獲取path里的值 console.log(v.get('path')) // path、query、body、header
可以使用get()獲取參數的值
13、倆種獲取參數的方法
const path = ctx.params const query = ctx.request.query const headers = ctx.request.header const body = ctx.request.body
和get()方法
推薦使用get()獲取
原因:現在有個多層嵌套函數,如果用第一種方式,某一層沒有值,會報錯。而使用get(),會報空值
14、LinValidator校驗器中的自定義規則函數 http://doc.cms.7yue.pro/lin/server/koa/validator.html#類校驗
我們把以validate
開頭的類方法稱之為規則函數,我們會在校驗的時候自動的調用 這些規則函數。
規則函數是校驗器中另一種用於對參數校驗的方式,它比顯示的 Rule 校驗具有更加的靈活 性和可操作性。下面我們以一個小例子來深入理解規則函數:
validateConfirmPassword(data) { if (!data.body.password || !data.body.confirm_password) { return [false, "兩次輸入的密碼不一致,請重新輸入"]; } let ok = data.body.password === data.body.confirm_password; if (ok) { return ok; } else { return [false, "兩次輸入的密碼不一致,請重新輸入"]; } }
首先任何一個規則函數,滿足以validate
開頭的類方法,除validate()
這個函數外。都 會被帶入一個重要的參數 data
。data 是前端傳入參數的容器,它的整體結構如下:
this.data = { body: ctx.request.body, // body -> body query: ctx.request.query, // query -> query path: ctx.params, // params -> path header: ctx.request.header // header -> header };
請記住 data 參數是一個二級的嵌套對象,它的屬性如下:
data
是所有參數的原始數據,前端傳入的參數會原封不動的裝進 data。通過這個 data 我們可以很方便的對所有參數進行校驗,如在validateConfirmPassword
這個規則函數中 ,我們便對data.body
中的password
和confirm_password
進行了聯合校驗。
我們通過對規則函數的返回值來判斷,當前規則函數的校驗是否通過。簡單的理解,如果規 則返回true
,則校驗通過,如果返回false
,則校驗失敗。但是校驗失敗的情況下,我們 需要返回一條錯誤信息,如:
return [false, "兩次輸入的密碼不一致,請重新輸入"];
表示規則函數校驗失敗,並且錯誤信息為兩次輸入的密碼不一致,請重新輸入
。
15、Koa2中使用sequelize創建數據表
文檔: https://itbilu.com/nodejs/npm/VkYIaRPz-.html#api-instance-createSchema
操作mysql
需要安裝sequelize和mysql2
const { Sequelize } = require('sequelize') const { dbName, host, port, username, password } = require('../config/config').database const sequelize = new Sequelize(dbName, username, password, { dialect:'mysql', // 數據庫類型 host, port, logging: true, // 記錄操作的sql語句 timezone: '+08:00' // 默認的時間會比正常時間慢8小時 })
// 加了這個,才能把定義的模型同步到數據庫中 sequelize.sync({ force: false // true會自動運行,通過定義的model修改數據庫中的表 })
force為true的時候,如果我們修改models中定義的屬性值,就會自動同步到數據庫表(會清空數據庫表並重建),我們一般設置為false
News.init({ id: { type: Sequelize.INTEGER, primaryKey: true, // 鍵 autoIncrement: true // 自增長 }, title: Sequelize.STRING, summary: Sequelize.STRING }, { sequelize, tableName: 'news' // 自定義數據庫表名 })
mode中定義好數據庫表的字段以后,需要在其他js文件引入這個model文件,Sequelize才會操作數據庫表
16、使用lin-validator-v2,new RegisterValidator()前面加await?
router.post('/register', async (ctx) => { // 為何要在new RegisterValidator()前面加await,而且使用lin-validator-v2? /** * 因為RegisterValidator里validateEmail方法中的User.findOne是一個Promise異步操作, * 不加await的話,無法阻止錯誤,還是會執行后面的代碼,引起系統報錯 */ const v = await new RegisterValidator().validate(ctx) const user = { email: v.get('body.email'), password: v.get('body.password1'), nickname: v.get('body.nickname') } User.create(user) })
17、在node項目中的請求,每一個請求都要new一次validate,為什么一定要這樣做,有沒有其他的寫法?
router.post('/register', async (ctx) => { // 為何要在new RegisterValidator()前面加await,而且使用lin-validator-v2? /** * 因為RegisterValidator里validateEmail方法中的User.findOne是一個Promise異步操作, * 不加await的話,無法阻止錯誤,還是會執行后面的代碼,引起系統報錯 */ const v = await new RegisterValidator().validate(ctx) const user = { email: v.get('body.email'), password: v.get('body.password1'), nickname: v.get('body.nickname') } User.create(user) })
就有另一種寫法,就是把validate當成中間件
router.post('/register',new RegisterValidator(), async (ctx) => { // 為何要在new RegisterValidator()前面加await,而且使用lin-validator-v2? /** * 因為RegisterValidator里validateEmail方法中的User.findOne是一個Promise異步操作, * 不加await的話,無法阻止錯誤,還是會執行后面的代碼,引起系統報錯 */ // const v = await new RegisterValidator().validate(ctx) const user = { email: v.get('body.email'), password: v.get('body.password1'), nickname: v.get('body.nickname') } User.create(user) })
但是這有個很大的問題,node中的中間件只new一次,我們的validate是class,在class保存屬性的時候回出錯
比如validate.a = 1,后面改成validate.a = 2,這樣判斷就會出錯
但是如果我們在每個請求中New一次validate,這就沒問題
這個涉及到面向對象的知識,使用中間件的方式,只new一次,生成一個對象,這樣多個地方會改變里面的值
如果是每個請求new一次,那么就生成單獨的對象,改變屬性值的時候,不會影響其他對象里的值
18、使用bcryptjs加密
const bcrypt = require('bcryptjs') const salt = bcrypt.genSaltSync(10) // 10是位數,標識計算機計算的時候用多久,不宜太大 const psw = bcrypt.hashSync(v.get('body.password1'), salt)
這樣的寫法每個有password的地方都要這樣寫,有另一種更好的
在定義模型的文件里,使用set方法進行監控
password: { type: Sequelize.STRING, // 觀察者模式 set (val) { const salt = bcrypt.genSaltSync(10) // 10是位數,標識計算機計算的時候用多久,不宜太大 const psw = bcrypt.hashSync(val, salt) this.setDataValue('password', psw) } }
使用bcryptjs中的compareSync驗證轉入的密碼和數據庫中加密的密碼是否一致
const correct = bcrypt.compareSync(plainPassword, User.password)
19、success效果,執行成功以后返回提示信息
第一種方案:
ctx.body = { msg: '', code: '' }
使用ctx.body返回成功信息
第二種方案:
把success包裝成exception,返回的時候,傳遞success的msg和code
// exception.js class Success extends HttpException { constructor(msg, errorCode) { super() this.code = 201 this.msg = msg || 'ok' this.errorCode = errorCode || 0 } } // heaper.hs function success(msg, errorCode) { throw new global.errs.Success(msg, errorCode) } module.exports = { success }
在使用的地方引入,直接success()
20、權限的判斷
我們可以給不同的角色設置不同的值,比如user(普通用戶)為8,admin(管理員)為16,默認的權限值為1,每個接口可以設置不同的權限值。
登錄的時候,把用戶的權限通過token傳遞到前端。前端請求接口的時候,通過token判斷當前用戶的權限,如果比接口需要的權限值大,那就說明可以訪問,不然就是權限不足
auth.js
const basicAuth = require('basic-auth') const jwt = require('jsonwebtoken') class Auth { constructor (level) { this.level = level || 1 // 設置默認的權限值,可以由接口自定義 Auth.USER = 8 Auth.ADMIN = 16 Auth.SUPER_ADMIN = 32 } get m () { return async (ctx, next) => { const userToken = basicAuth(ctx.req) let errMsg = 'token不合法' if (!userToken || !userToken.name) { throw new global.errs.Forbbiden(errMsg) } try { var decode = jwt.verify(userToken.name, global.config.security.secretKey) } catch (error) { if (error.name === 'TokenExpiredError'){ errMsg = 'token已過期' } throw new global.errs.Forbbiden(errMsg) } // 判斷用戶的權限是否比接口需要的權限小 if(decode.scope < this.level){ errMsg = '權限不足' throw new global.errs.Forbbiden(errMsg) } // 獲取token里的uid,scope ctx.auth = { uid: decode.uid, scope: decode.scope } await next() } } } module.exports = { Auth }
classic.js
const { Auth } = require('../../../middlewares/auth') // auth也是一個中間件,一定要寫在后面的中間件前面,這樣才能阻止后面的中間件 // 可以在new Auth()里傳遞值,確定訪問當前接口需要什么權限 router.get('/latest', new Auth(2).m, async (ctx, next) => { const v = new PositiveIntegerValidator().validate(ctx) const id = await v.get('path.id', parsed = false) // path、query、body、header ctx.body = { msg: 'success', id: v.get('path.id') } })
21、微信小程序從登錄到獲取token一系列操作
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
微信登錄后調用wx.login方法,獲取臨時憑證code,傳遞給后端服務器
后端服務器調用 auth.code2Session接口,換取 用戶唯一標識 OpenID 和 會話密鑰 session_key。
使用OpenID生成token,返回給前端
22、獲取openid每次都會返回errcode:40013 錯誤
檢查appId的值
23、訪問Node項目的時候會突然奔潰,並報錯ctx.onerror is not a function 地址:https://segmentfault.com/q/1010000009716118
原因是:koa-bodyparser不對
正確應該是:
const parser = require('koa-bodyparser') app.use(parser())
24、使用了Sequelize創建的模型,如果要給模型添加額外的屬性,就需要使用setDataValue
art.setDataValue('index', flow.index)
給art模型添加index屬性並賦值
25、每次引入數據的時候,都會寫很長的路徑,為了方便,我們可以起別名,node里的別名是寫在package.json里的
"_moduleAliases":{ "@root":".", "@models":"app/models", "@validator":"app/validators/validator.js" }
使用
const { Favor } = require('@models/favor')
不過我在使用中發現,一直在報錯,無法找到路徑,不知道為什么
26。我們在查詢表的時候,會返回表中所有的字段,有時候一些字段不需要,就要過濾掉,這是我們用Sequelize中的Scopes
https://sequelize.org/master/manual/scopes.html
在db表里統一進行設置
const sequelize = new Sequelize(dbName, username, password, { dialect:'mysql', // 數據庫類型 host, port, logging: true, // 記錄操作的sql語句 timezone: '+08:00', // 默認的時間會比正常時間慢8小時 define: { // timestamps: false // 設置為false,就不會生成createdAt和updateAt了 timestamps: true, // 管理 createdAt和updateAt paranoid: true, // 管理deletedAt createdAt: 'created_at', updatedAt: 'updated_at', deletedAt: 'deleted_at', underscored: true, // 把駝峰轉換成下划線 scopes: { bh: { attributes:{ exclude:['updated_at','deleted_at','created_at'] } } } } })
scopes: { bh: { attributes:{ exclude:['updated_at','deleted_at','created_at'] } } }
在scopes里我們可以定義多個函數,現在定義的是bh,exclude表示過濾掉這些字段
使用的時候
art = await Movie.scope('bh').findOne(finder)
在調用Sequelize自帶的查詢方法錢調用scope(),並傳入之前定義好的函數名
備注:scopes也可以在每個表的模型文件中定義