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也可以在每个表的模型文件中定义