# NodeJS Koa 笔记

# 基础

编写一个接口的最基本代码

const koa = require('koa')
const Router =require('koa-router')

const router = new Router()

const app = new koa()

router.get('/classic/latest',(ctx,next)=>{
  ctx.body ={key:"classic"}
})
// router.routes() 注册中间件
app.use(router.routes())
app.listen(3000,()=>{
  console.log('http监听端口3000')
})

# 中间件

中间件-发送http调用的函数,一个实例可以定义多个中间件,中间件调用总是返回promise

app.use注册中间件。ctx上下文,next下一个中间件

// 中间件--就是函数
app.use((ctx,next)=>{
  // ctx 上下文
  console.log('七月')
  next()
})
app.use((ctx,next)=>{
  console.log('八月')
})

传递参数 通过挂载到ctx传参,首先要保证洋葱模型

app.use(async(ctx,next)=>{
    await next()
    console.log(ctx,r)
})

app.use(async(ctx,next)=>{
    ctx.r = await axios.get('www.baidu.com').data
    await next()
})

// 打印出dom结构,实现了传参

# 洋葱模型

先执行fun1上 再执行中间件函数 在执行fun1下。可以判断函数是否完全执行,以中间件函数为分界线

简单的例子:

app.use(async(ctx,next)=>{
    fun1()
    await next()
    funt1下()
})

app.use(async(ctx,next)=>{
    funt2()
})

使用async,await强制promise同步调用化,调用顺序 fun1() ====> fun2() ====> fun1下

# async await

await特性:

  • 求值 await 理解为计算promise的值,使用await使用一定要在function前加上async,也可以对表达式求值。使用async,await一定可以使中间件保持洋葱模型
app.use(async(ctx,next)=>{
    console.log(1)
    const a = await next()
    console.log(2)
})

app.use(async(ctx,next)=>{
    console.log(3)
})

// 1 3 2
  • 阻塞线程 常见的异步调用:对资源 读文件 操作数据库 发送http
//默认异步调用

app.use((ctx,next)=>{
    console.log(1)
    axios.get('www.baidu.com').then((res)=>{
          const a = res
    })
    console.log(a)
    console.log(2)
})

// 由于是异步请求 打印结果 1 2 res

使用await阻塞线程之后,异步同步化
app.use(async(ctx,next)=>{
    console.log(1)
    const a = await axios.get('www.baidu.com')
    console.log(a)
    console.log(2)
})
// 打印 1 res 2

# 路由

初级路由判断 ctx.path返回路由,ctx.method返回调用方法,ctx.body定义返回内容

app.use(async(ctx,next)=>{
    if(ctx.path ==='/clasic/latest' && ctx.method ==='GET'){
        ctx.body =  {key:"clasic"}
    }
})

使用koa-router方法

const router = new route()

router.get('/',(ctx,next)=>{
    ...
})

app.use(router.routes())

nodemon 自动重启node服务 全局启动nodemon app.js

一键导入module require-directory轮子


const requireDirectory = reuire('require-directory')

requreDirectory(module,'./api/v1',visit:function)

function whenExportModule(obj){
    if(obj instanceof Router){
        app.use(obj.routes())
    }
}

# 改造 路由

创建 core.js

const Router  = require('koa-router')
const requireDirectory = require('require-dicectory')

// 导入全部模块前判断 是否是Router的对象 
class InitManger{
    static initcore(app){
        InitManager.app = app
        InitManager.InitLoadRouters()
    }
    static InitLoadRouters(){
    const path = `${process.cwd()}/api/v1`
        requireDirectory(module,path,{
            visit:whenLoadrouters
        })
        function whenLoadrouters(obj){
            if(obj instanceof Router){
              InitManger.app.use(obj.routes())
            }
        }
    }
}

app.js引入

const IniManager = require('core.js')

InitManager.initcore(app)
app.listen(3000)

# 校验处理

# 获取参数

假设访问的路由地址为localhost:3000/v1/3/classic/latest?password=123 header:token:1111 ,body:{"key":"localhost"}

router.get('/v1/:id/classic/latest',(ctx,next)=>{
    // 获取url参数
    const params = ctx.params
    // 获取query 
    const query = ctx.request.query
    // 获取token
    const query = ctx.request.header
    // 获取访问时内容
    const body = ctx.request.body
    ctx.body={key:"classic"}
})

# 异常处理

设置全局返回的状态码 异常分为:已知异常 未知异常

message
error_code
request_url
HTTP status code 2xx 4xx 5xx
常见Http状态码

200 'ok'
400 'params error'
404 'Not found'
403 'forbidden'
502 'bad gateway' 路径错误
500 '服务器异常'
504 '服务器超时'

全局异常处理

定义一个execption类继承Error,然后抛出异常时实例化,传递参数,打印全局异常返回json

  • 创建Http-execption.js

    class Httpexecption extends Errror{
       // 定义构造函数
        constructor(msg="服务器异常",code=400,errorCode=10001){
            super()
            this.msg = msg
            this.code = code
            this.errorCode = 10001
        }
    }
    
    class Paramexecption extends Httpexecption{
        constructor(msg,code,errCode){
            super()
            this.msg = msg || '参数错误'
            this.code = 400
            this.errorCode = 10000
        }
        
    }
    module.exports = {
        Httpexecption,
        Paramexecption
    }
    
  • 创建全局异常处理中间件

    const {Httpexecption} = require('Http-execption')
    
    const execption = async(ctx,next){
        try{
            await next()
        }catch(error){
            if(error instanceof Httpexecption){
                ctx.body = {
                    msg: error.msg,
                    error_code:error.errorCode,
                    request:`${ctx.method} ${ctx.path}`
                }
            }
            
        }
    }
    
    module.exports=execption  // app.js注册该中间件
    
  • 在定义路由时抛出异常

    const {Paramexecption} = require('Http-execption')
    router.get('/latest',(ctx,next)=>{
        const query = ctx.request.query
        if(!query){
            const error = new Paramexecption()
            thorw error
       }
    })
    
  • 未知异常

    else{
          ctx.body = {
            msg:"未知异常发生",
            erro_code:999,
            request:`${ctx.method} ${ctx.path}`
          }
          ctx.status = 500
        }
      }
    

# 参数校验

使用Lin-validator进行参数校验 使用前必须定义好全局抛出参数异常的 Paramexecption,然后引入util.js

第一步 创建校验器类

const {Linvalidator,Rule} from 'Lin-validator.js'

// 校验正整数 
class PositiveIntegerValidator extends Linvalidator{
    constructor(){
       super()
    //  使用lin-validator校验规则 三个参数 规则,返回提示信息,附加参数
    //  要与路由的参数信息一一对应 数组形式
    this.id = [ 
      new Rule('isInt','参数必须是正整数',{min:1})
    ]
  }
    }
}
module.exports = {PositiveIntegerValidator}

第二步 引用校验器进行校验 (调用validate方法)

const {PositiveIntegerValidator}  = require('validator.js')

router.get('/classic/:id/latest',(ctx,next)=>{
    const v = new PositiveIntegerValidator().validate(ctx)
})

**使用校验器获参数 ** get方法

const param = ctx.params
const v =new PositiveIntegerValidator().validate(ctx)
const id = v.get('param.id')  // 自动将id 转换为整形
// 如果不想转换  
const id = v.get('param.id',parsed:false)

配置开发环境的异常抛出

由于我们捕获到的异常都去做了全局异常处理,导致某些异常无法判断, 所以定义config来配置开发环境
module.exports={
    enviorment:"dev"
}

在全局异常中间件,判断是否是开发环境,然后抛出异常

if(global.config.enviorment === 'dev'){
    throw error
}

# sql复习

# 创建数据库

CREATE DATABASE 数据库名

# 删除数据库

DROP DATABASE 数据库名

# 创建表

约束

1. 非空约束  NOT NULL
2. 默认值约束  DEFAULT '男'
3. 唯一性约束 UNIQUE
3. 主键约束 PRIMARY KEY
create table 表名(
字段名 类型(长度) [约束],
    ...
)

常见类型

# 删除表

DROP TABLE 表名;

# 查看表结构

DESC 表名

# 修改表结构

修改列名
Alter table 表名  change  列名  新列名 类型;

修改列类型
Alter table 表名  modify  列名  新类型;

#

insert into 表名(字段1,字段2...)values(1,2...)

其他方式

insert into 表名(字段1,字段2) values(1,2),(1,2);     //插入多条数据【MYSQL】
insert into 表名 values(1,2);                    //针对全表所有字段进行插入操作
insert into 表名(字段) select 字段 from2;         //查询结果插入
insert into 表名 select 字段 from2;               //查询结果,全表插入

#

delete fromwhere 条件

#

updateset 字段=where 条件
例: update user set username=7yue where id =1;

#

查询表中全部内容

select * from 表名

查询指定列信息

select1 from 表名

条件查询

select.. from 表名 where 条件

条件运算符 逻辑运算符

=  >  >=  <  <=  and  &&  or  not

范围查询

wherebetween 条件1  and 条件2;          //列在这个区间的值

wherenot between 条件1 and 条件2;    //不在这个区间

where !(between 条件1 and 条件2 );     //同样表示不在这个区间

空值查询

whereis null;  //查询列中值为null的数据

模糊查询

wherelike '%0';   //表示以0结尾
wherelike  '0%';   //表示以0开头
wherelike  '%0%';   //表示数据中包含0

排序

where 条件 order by[asc/desc]

多表查询

select * from1,2  where1.字段=2.字段;  //隐式内连接,使用where条件消除笛卡尔积

select * from1 [inner] join2 on1.字段=2.字段;  //显式内连接,如果是多张表,则一直在join..on后依次添加join..on即可,inner关键字可被省略

# sequlize(模型导入数据库)

# 初始化配置

以上配置都可以参考sequelize文档 (opens new window) 或者中文文档 (opens new window)

第一步,定义数据库配置

config.js

module.exports = {
    database:{
        // 数据库名 主机号 端口 用户名 密码
        dbName:"koa",
        host:"localhost",
        port:3306,
        user:"root",
        password:"wohenpi0918"
        
    }
}

第二步,配置sequelize 更多配置参考 API文档 (opens new window)

const Sequelize = require('sequelize')
const {dbName,host,port,user,password} = require('config.js')

// 参数: 数据库名 用户名 密码 配置具体 
const sequlize = new Sequelize(dbName,user,password.{
 	// 数据库类型
      dialect:'mysql',
      host,
      port,
    // 是否在命令行打印出sql
      logging:true,
    // 显示北京时间
  	  timezone:'+08:00',
      define:{
    // 加入创建时间 更新 updatedAt, createdAt
      timestamps:true,
    // 必须和timestamps同时使用 增加一个 deletedAt 标识当前时间
      paranoid:true,
    // 重命名 时间戳字段
       createdAt:'created_at',
       updatedAt:'updated_at',
       deletedAt:'deleted_at',
    // 不使用驼峰式命令规则,这样会在使用下划线分隔
    // 这样 updatedAt 的字段名会是 updated_at
       underscored: true,
  }
      })
      
   // 同步模型到数据库中
   sequelize.sync({
       // 是否强制更新 删除后直接覆盖数据表
       force: false,
   })
module.exports = {sequelize}

第三步,定义模型层

model下创建user.js 更多定义方法 参考数据类型 (opens new window)

const {sequelize} =require('db.js')
const {Sequelize,Model} = require('sequelize')

class User extends Model{
    
}
// 定义模型层
User.init({
  // 主键: 不能为空 不能重复
  id:{
    type:Sequelize.INTEGER,
    primaryKey:true,
    // 自动增长 id编号
    autoIncrement:true
  },
  username:{type:Sequelize.STRING,unique:true},
  password:Sequelize.STRING,
  email:{type:Sequelize.STRING,unique:true},
  openid:{
    // 最大字符长度 64
    type:Sequelize.STRING(64),
    unique:true
  }
},{
  sequelize,
   // 自定义表名  默认会以模型名为表名
  tableName:'user'
})

启动项目, sequelize会创建一张user

# sequelize相关API

sequelize大多数查询的API都是返回的pormise对象,所以定义模型方法时加上 asyncawait

1. 定义模型  class A extends Model{}  A.unit({},{sequelize,tableName:"name"})
2. 访问字段和设置字段值  get(){ let title = this.getDataValue('title')..}  set(val){this.setDataValue('index',value)}

3. 验证Validations
username:{type:Sequelize.STRING,validate:{len:[2,10] ....}}

----------
Model类API

1. removeAttribute([attribute]) 删除一个字段属性()

2. sync() 将当前模型同步到数据库  可以配置{force:true} 强制覆盖,每次同步都会先删除之前的表

3. drop() 删除数据库的表

4. getTableName() 获取表名,可以指定schema

5. scope() 定义限制范围

6. findOne 查询单条数据  await ModelNmae.findOne({
    where:{
        'index':value
    }
})

7. findAll 查询多条数据  await ModelName.findAll({
    where:{
        attr1:value,
        attr2:value
    }
})   
这里可以使用到 大于,小于等`$gt` `lte`  `$or`
Model.findAll({
  where: {
    attr1: {
      $gt: 50
    },
    attr2: {
      $lte: 45
    },
    attr3: {
      $in: [1,2,3]
    },
    attr4: {
      $ne: 5
    }
  }
})
// WHERE attr1 > 50 AND attr2 <= 45 AND attr3 IN (1,2,3) AND attr4 != 5


8. findById()  通过主键id查询单个实例

9. count()  统计数量

10. findAndCount  分页查询

11. create() 创建实例

12. max()  min()  sum()

13. upsert 创建或更新

14. className.transition(async (t)=>{
    ... 
})  
 创建事务
 Modle类.transcation(async t =>{
     await Favor.create({
         uid,art_id,type
     },{transcation:t})
     ...更多操作
 })

15. destory 删除记录

16. restore 恢复记录

17 自增自减  increment decrement
user.increment(['age', 'number'], {by:2}).then(function(user){
  console.log('success');
})

# 注册

首先定义好模型 然后编写校验器 密码用盐加密,处理好异常

# 编写校验器

用户名: 用户名长度规范 唯一性规范

密码: 正则表达式规范

邮箱: 邮箱规范 唯一性规范

const {User} = require('user.js')

class RegisterValidator extends Linvalidator{
    constructor(){
        // 用户名
        this.username= [
            new Rule('isLength','用户名不符合规范',{min:4,max:32})
        ]
        // 密码
        this.password1 =  [
            new Rule('matches','密码必须包含特殊字符,字母,数字并且超过六位''/^.*(?=.{6,})(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*? ]).*$/')
        ]
        // 校验规则相同
        this.password1 = this.password2
        this.email = [
            new Rule('isEmail','邮箱不符合规范')
        ]
    }
    //自定义方法 校验用户和邮箱的唯一性 必须以validate开头
    validateUserName(params){
        // 通过params.body 拿到body参数
        const username = params.body.username
        // sequelize条件查询
        const user = User.finOne({
            where:{
                username:username
            }
        })
    }
    validateEmail(params){
        const email = params.body.email
        const user = USer.findOne({
            where:{
                email:email
            }
        })
    }
}

# 编写注册API (标准流程)

const Router = require('koa-router')
const router = new Router({
    // 添加前缀
    prefix:'/v1/user'
})

router.post('/register',async(ctx)=>{
    // 守门员 校验参数 只有当通过校验器才能插入模型数据
    const v = await new RegisterValidator().validate(ctx)
    // 获取通过校验的参数
    const params = {
        username:v.get('body.username'),
        password:v.get('body.password1'),
        email:v.get('body.email')
    }
    // 插入数据库  模型.create(参数列表)
	await User.create(user)
    
})

# 明文加密

const bcryptjs = require('bcryptjs')
//定义密码模型时加密
User.init({
    password:{
        type:Sequeize.STRING,
        set(val){
            // 取盐 赋盐
            const sault = bcryptjs.getSaltSync(10)
            const pwd = bcryptjs.hashSync(val,sault)
            // 将盐赋值
            this.setDataValue('password',pwd)
        }
    }
})

# 登录

定义好登陆的方式 然后编写相关的校验器,

# 准备工作

首先定义好登录的方式 ,模仿枚举方式

// 定义一个判断方法 ,判断是否在这几个类型中
function isInType(val){
    for(let key in this){
        if(this[key] === val){
            return true
        }
    }
    return false
}
const LoginType = {
    //  邮箱 手机号 小程序登录
    USER_EMAIL:100,
    USER_PHONE:101,
    USER_MINI:102,
    isInType
}

module.exports = {
    LoginType
}

正式编写路由 (三个步骤)

  • 编写校验器
  • 校验传入的参数的账号密码是否存在数据库
  • 返回响应

accountsecret进行校验,然后校验登录方式合法



class Loginvalidator extends Linvalidator{
    constructor(){
        super()
        this.account = [
            new Rule('isLength',"账号不符合规范",{min:4,max:128})
        ]
        this.secret = [
          new Rule =('isOptional'),
          new Rule('isLength','密码长度必须大于6',{min:6})    
        ]
    }
    validateLoginType(params){
        const type = params.body.loginType
        if(!type){
            throw new Error('请传入登录方式')
        }
     if(!global.config.LoginType.isInType(type)){
            throw new Error('type参数不合法 ')
        }
    }
}

module.exports = {Loginvalidator}

isOptional 参数可以传可以不传

当参数通过校验器时, 定义方法判断传入的参数是否在数据库中存在

async function emailLogin (account,secret){
    const user = await User.vertifyEmail(account,secret)
}

一般情况下 校验传入的参数是否在数据库中,在模型中完成

user.js

class User extends Model{
    async function vertifyEmail(account,secret){
        const user = await User.findOne({
            where:{
                email:account
            }
        })
        if(!user){
            throw 没有此账户的错误异常
        }
        // 判断密码是否和不加密前的密码相同
        const corret = bcrypt.compareSync(secret,User.password)
        if(!corret){
            throw 密码错误异常
        }
        return user
    } 
}

模型定义完之后

# 书写路由

router.get('/token',async(ctx,next)=>{
    const v = new Loginvalidator().validate(ctx)
    const type = v.get('body.loginType')
    const account = v.get('body.account')
    const secret = v.get('body.secret')
    switch(type){
        case LoginType.USER_EMAIL:
            // 检验密码
            await emailLogin(account,secret)
            break;
        case :
        break;
    }
    // 暂时返回token
    ctx.body={
        token
    }
})

# 微信登录接口

首先定义好需要的三个参数

URL
appId
appSecret
class wxManager{
    static async openidTotoken(code){
        // 拼接url
        const url = util.format(URL,appId,appSecret,code)
        const result = await axios.get(url)
        if(result.status!==200){
            throw new getOpenIDException()
        }
        if(result.data.errcode!==0){
            throw new getOpenIDException('获取openID失败'+result.data.errcode)
        }
        // 成功后 查询数据库 然后插入数据库 再获取token
        let user = User.getOpenIdUser(result.data.openid)
        if(!user){
           user = User.registerOpenId(result.data.openid)
        }
        let token = generate(user.id,Auth.USER)
    }
}

util.format Node.js提供的util的API,可以将第一个参数中的占位符换成后面的参数

getOpenIdUser Model层中定义的静态方法 查询user

registerOpenId 插入openid

Auth.USER 提前定义在Auth 获取令牌的class中的,表示权限的值 用户 只需要判断scope和传入的level值(当传入的level小于用户级别的scope时就可以获取token,反之就不可以)

class Auth {
constructor(level){
    this.level = level || 1
    Auth.USER = 8
    Auth.ADMIN = 16
    Auth.SUPER_ADMIN = 32
}
}

书写完微信登录获取openid之后,可以书写一个校验拿到的token的方法

static verifyToken(token){
    try {
       jwt.verify(token, global.config.security.secretKey)
      return true
    } catch (error) {
      return false
    }
  }

当前端从Storage里面拿出token,检验它的合法性,然后再继续走下去

# 颁布令牌,获取token

在获取token之前先了解jwt的主要API

jwt.sign() // 生成令牌  需要传入参数:1.传入自定义信息(后面可封装在auth里) 2.secretKey秘钥(用户自定义) 3.配置(失效时间):jwt.sign(
   {uid,scope},secretKey,{expiresIn}
  )


jwt.verify() //校验令牌 如果token无效会抛出异常
// 需要传入 token 秘钥 两个个参数  
最好是放在try catch中捕获异常
  • 定义基本配置

    security = {
        secretKey:'abcdefg',// 自定义
        expiresIn:60*60 //令牌时效
    }
    
  • 书写颁发令牌方法

    const generateToken = function(uid,scope){
        const token= jwt.sign({uid,scope},security.secretKey,{expiresIn})
        return token
    }
    
  • 登录时获取token

    async function emailLogin(account,secret){
        // user 登录时获
    
        
        
        const token = generate(user.id,2)
        return token
    }
    
     async function vertifyEmail(account, secret) {
      const user = await User.findOne({
        where: {
          email: account
        }
      })
      if (!user) {
        throw new LoginExecption('账号不存在')
      }
      if (!bcrypt.compareSync(secret, user.password)) {
        throw new LoginExecption('密码输入错误')
      }
      return user
    }
    

# 路由携带令牌校验

想清楚三件事:

  1. token约定放在header还是body
  2. 用什么方式来检验token是否合法
  3. 校验令牌中间件放在什么位置

具体思路:首先一般情况下token在HTTPBasicAuth规则中是放在header部分的,然后我们通过这种方式测试

校验合法性,书写中间价时调用 jwt.verify()来检验合法token

校验令牌必须放在路由中间件前面,因为是最高权重 只有放了权限才能进行后面

const baseAuth = require('base-auth')
class Auth {
    constructor(){}
    get m(){
        return async (ctx,next)=>{
            // 获取basicAuth的token值  而且这里必须用原生API req
            const UserToken = baseAuth(ctx.req)
            if(!USerToken || !UserToken.name){
                throw new token异常
            }
            try{
                var decode = jwt.verify(UserToken,secretKey)
                // 校验令牌合法 不合法抛出异常
            }catch(error){
                 if (error.name == 'TokenExpiredError') {
                  errMsg = 'token令牌已经过期'
                     }
                    throw new ForbidenException(errMsg)
                }
        }
        ctx.auth = {
            uid:decode.uid,
            scope:decode.scope
        }
        // 下一个中间件执行
        await next()
    }
    
}

new Auth().m 这里 m并不是方法 是class里面属性 通过get获取 实则是个中间件函数

校验完令牌之后,在router.get('') 中间注册中间件 new Auth().m

# 前端携带令牌(BasicAuth方式) (API key方式)

在发送HTTP请求时,加入这样的header

header:{
    Authorization:Basic base64(account:password) //必须 是base64加密后的token信息
}

// base64基本用法
import {Base64} from 'base64-js'
const base64 = Base64.encode(token+':')
return 'Basic'+base64

此时携带的令牌数据就可以传递

header:{
    Authorization:封装的函数
}

使用API key方式就不需要base64加密处理

// 拿到约定好放在header或者query的token
class Auth {
    get m(){
        return async(ctx,next)=>{
            const UserToken = ctx.request.header.token
            if(!USerToken){
                throw new token不合法异常
            }
            // 校验token合法性
            try{
                var decode = jwt.verify(UserToken,global.config.secretKey)
            }catch(error){
              if(error.name ='TokenExpiredError'){
                  throw new Error('令牌过期')
              }
                throw new token不合法
            }
        }
    }
}

前端调用:

wx.request({
    url:'',
    method:'POST',
    header:{
        token:wx.getStorageSync('token')
    },
    success:(res)=>{
        console.log(res.data)
    }
})

# 具体业务

首先先把数据表的概念分清

  • 业务表 : 解决业务问题 抽象出来的,记录业务 比如说:一期又一期的期刊,存放他们不同的index来区分期刊

  • 实体表 :具体到某个模型的数据,各种字段

    sequelize操控数据库具体参考地址 (opens new window)

    模型层:分出 classic art flow user favor

    定义模型层

    引入sequelize实例

    定义字段

    导出模型

    classic: 分为 movie sentence music 共同字段定义

    art :所有的实体,所有期刊 由几个模型拼凑

    flow: 业务模型 应有的字段:art_id,index,type 对取出实体模型记录非常重要

    user: 用户模型

    favor

# 获取最新一期期刊

获取最新一期,就是拿出期刊号最大的那一个实体。

fow表降序取出第一条记录

router.get('/latest', new Auth().m,async(ctx)=>{
   let latest = async folw.findOne({
       // sequlize排序 写法 升序 ASC
       order:[
           ['index','DESC']
       ]
   })
   let art = async Art.getOne(latest.index)
   art.setDataValue('index',latest.index)
    ctx.body = art
  
})

前端请求数据之前需要携带令牌,

Art模型中定义查找记录方法

class Art {
    // 传入art_id 和 type
 static async getOne(art_id, type) {
    const find = {
      where: {
        id: art_id
      }
    }

    let result = null
    switch (type) {
      case 100:
        result = await movie.findOne(find)
        break
      case 200:
        result = await music.findOne(find)
        break
      case 300:
        result =await sentence.findOne(find)
        break
      case 400:
        break
      default:
        break
    }
    return result
  }
}

# 点赞 取消点赞

首先确定业务,点赞和取消点赞需要操作两张表,一张表记录用户点赞的记录,另一张实体表里面的收藏数量就会增加,或者减少

  • 对业务表添加记录或者删除记录
  • 修改实体表的数据

保证两个操作都能同时进行,可以使用数据库的事物

sequelize操作数据库的事物

sequelize.transaction(async (t)=>{
    ...
})
// 首先获取Favor中是否该用户点过赞 然后如果没有点赞向数据库添加一条记录 并且增加art实体的fav_nums

const favor =await Favor.finOne({
    where:{
        uid,art_id,type
    }
})
if(favor){
    throw new LikeException('已经点过赞')
}
// 事务一定要用return的方式执行
return sequelize.trancation(async t =>{
    await Favor.create({
        art_id,
        type,
        uid
    },{transcation : t})
    const art =await Art.getOne(art_id,type)
    // 自增涨一个字段 by 增长值
    await art.increment('fav_nums',{by:1,transaction:t})
})

// dislike 同理 
软删除 增加一条deleted_at
MOdel.class.destroy(force:false,transcation:t)

# 上一期 下一期

首先查找flow表中index的记录,然后index+1 ,增加异常判断,查询出art表的记录,并田间like_status和index


router.get('/:index/next',new Auth().m,async(ctx)=>{
  // 校验 获取art_id type 获取实例 然后增加属性
 const v = await new IndexValidator().validate(ctx)
 const index = v.get('path.index')
 const next = await flow.findOne({
   where:{
     index:index + 1  
   }
 })
 if(!next){
   throw new NotFoundException('没有下一期了')
 }
 let art = await Art.getOne(next.art_id,next.type)
 const like_status = await Favor.Userlike(ctx.auth.uid,next.art_id,next.type)
 art.setDataValue('index',next.index)
 art.setDataValue('like_status',like_status)
 ctx.body = art
})

# 获取点赞信息

传入uid, art_id和type对favor表进行记录查询,返回布尔值

static async Userlike(uid, art_id, type) {
    // 返回用户是否喜欢这个art
    const favor = await Favor.findOne({
      where: {
        uid,
        art_id,
        type:type
      }
    })
    // 如果存在点赞 就返回true
    return !!favor
  }

# 获取某一期的详情信息

传入type和art_id查找flow的记录 找到后查找like_status和index

router.get('/:type/:id/detail',new Auth().m,async(ctx)=>{
  const v = await new ClassicValidator().validate(ctx)
  const art_id = v.get('path.id')
  const type = parseInt(v.get('path.type'))
  const classic = await flow.scope('bh').findOne({
    where:{
      art_id,
      type:{
        [Op.not]:400
      }
    }
  })
  if(!classic){
    throw new NotFoundException('找不到该资源')
  }
  let art = await Art.getOne(art_id,type)
  const like_status = await Favor.Userlike(ctx.auth.uid,art_id,type)
  art.setDataValue('index',classic.index)
  art.setDataValue('like_status',like_status)
  ctx.body = {
    art
  }
})

# 获取用户喜欢期刊列表

传入uid查询favor表得到一个数组,然后Art模型编写获取列表方法

定义一个对象 注意json的key永远都是字符串

const artInfoObj ={
	100:[],
	200:[],
	300:[]
}

获取列表元素 for循环 artInfoList

artInfoObj[artinfo.type].push(artinfo.art_id)

再循环对象 查找type下的数组列表

ids artInfoObj[key]

type key

如果是空数组 跳出循环

for循环不需要定义大量的复杂逻辑,封装一个函数

注意:obj的key是字符串 传参会报错

循环引用会报undefined 解决方法:局部导入

最终结果需要将所有的数组提取到大数组 [[],[],[]]

使用faltten方法

static async getFavorList(list,uid) {
    // 最终返回结果 [[100:],[200:],[300:]]
    let FavorListObj = {
      100: [],
      200: [],
      300: []
    }
    // 分别向不同的type中添加 ids  现在的obj {100:[1,2,3],200:[1,2,3]..}
    list.forEach(item => {
      FavorListObj[item.type].push(item.art_id)
    })
    let ret = []
    for (let item in FavorListObj) {
      // 拿到每个key :item-type  拿到每个key下的ids FavorListobj[item]
      //进行in查询
      let itemX = parseInt(item)
      if (FavorListObj[item].length === 0) {
        continue
      }
      ret.push(await Favor._getlistByType(itemX, FavorListObj[item],uid))
    }
    // 需要借助loadsh 打散二维数组为一维数组
    return flatten(ret)
  }
  static async _getlistByType(type, ids,uid) {
    const find = {
      where: {
        id: {
          [Op.in]: ids
        }
      }
    }
    let result = []
    switch (type) {
      case 100:
        result= await movie.scope('bh').findAll(find)
        break
      case 200:
        result = await music.scope('bh').findAll(find)
        break
      case 300:
        result = await sentence.scope('bh').findAll(find)
        break
      default:
        break
    }
    // result 返回实体数组 [[],[],[]]
    result.forEach(async(item)=>{
      let like_status =await Favor.Userlike(uid,item.id,item.type)
      item.setDataValue('like_status',like_status)
    })
    return result
  }

# 获取热门图书

 static async getHotBooklist(list){
    // 获取所有的art_id
    let ids = []
    list.forEach((book)=>{
      ids.push(book.id)
    })
  const favors=  await Favor.scope('bh').findAll({
      art_id:{
        [Op.in]:ids
      },
      type:400,
    group:['art_id'],
    // 分组结果 [{art_id:1,count:},{}....]
    attributes:['art_id',[Sequelize.fn('COUNT','*'),'count']]
    })
    // 循环遍历所有的图书
    list.forEach((book)=>{
      Hotbook._setCount(book,favors)
    })
    return list
  }
  // 如果存在favor表中就把 count赋给book
  static _setCount(book,favors){
    let count = 0
    favors.forEach((favor)=>{
      if(book.id === favor.art_id){
        count = favor.get('count')
      }
    })
    book.setDataValue('count',count)
    return book
  }

# 评论

定义book模型 id fav_nums

从服务器请求数据 返回详情

图书搜索

定义校验器 三个参数 query :keyword,start count

start 可传可不传 传一个默认值 summary=1 鱼书搜索不返回概要信息

encodeURI(q) 将可能为中文的编码转换

# json序列化

再返回的字段中定义toJSON方法指定返回的字段,sequelize就定义在模型中 实例方法。

this.getDataValue()

toJSON(){
    return {
        content:this.getDataValue('content')
    }
}

原型链方法定义Model方法 删除dataValues的某些字段

# KOA-STATIC

处理静态资源 __dirname 项目目录

访问/static文件夹
const static = require('koa-static')
const path = require('path')
app.use(static(path.join(__dirname,'/static')))
// 这样localhost路径 就可以访问到static文件夹下的文件

添加配置:host:'http://localhost:3000/'

art模型替换image路径

# 自动无感知刷新令牌

_request(url, resolve, reject, data = {}, method = 'GET', noRefetch = false) {
    wx.request({
      url: api.url,
      method: method,
      data: data,
      header: {
        Authorization: wx.getStorageSync('token');
      },
      success: (res) => {
        const code = res.statusCode
          if (code === 403) {
            if (!noRefetch) {
              _refetch(
                url,
                resolve,
                reject,
                data,
                method
              )
            }
          }
        }
    })
  }
  _refetch(...param) {
    getTokenFromServer((token) => {
      this._request(...param, true);
    });
  }

# 配置webpack 支持ES6

  1. 安装webpack

    npm i -D webpack webpack-cli
    
  2. 安装 相关依赖

    "devDependencies": {
        "@babel/core": "^7.6.2",
        "@babel/node": "^7.6.2",
        "@babel/preset-env": "^7.6.2",
        "babel-loader": "^8.0.6",
        "clean-webpack-plugin": "^3.0.0",
        "cross-env": "^6.0.3",
        "nodemon": "^1.19.3",
        "npm-run-all": "^4.1.5",
        "rimraf": "^3.0.0",
        "terser-webpack-plugin": "^2.1.2",
        "webpack": "^4.41.0",
        "webpack-cli": "^3.3.9",
        "webpack-merge": "^4.2.2",
        "webpack-node-externals": "^1.7.2"
      }
    
  3. 根路径下创建webpack.config.js

    
    
Last update: 7/20/2021, 8:53:54 AM