eggjs搭建开发环境

插件的使用

Egg 使用 Koa2 中间件

  1. 在 config 文件夹 config.default.js中设置

    1
    2
    // add your config here
    config.middleware = ["jwt"];
  2. 然后在 app/middleware 目录下面(没有则新建) 新建一个上面数组里的同名文件,比如 jwt.js ,然后写入中间件内容即可。

    1
    2
    // jwt.js
    module.exports = require("koa-jwt");

☆ 不清楚这种插件是否也是像this.app.**这么使用,并且 config.js 里面配置的插件参数是否可用。

使用插件解决本地开发跨域问题(csrf)

  1. npm i egg-security egg-cors -S

  2. ```js
    // plugin.js
    exports.security = {
    enable: true,
    package: “egg-security”
    };

    exports.cors = {
    enable: true,
    package: “egg-cors”
    };

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    运行时有黄色警告可能是`npm i`的时候自动将 security 插件一起装了,提示重复配置,注释了就行。

    3. ```js
    // config.default.js
    config.security = {
    csrf: false,
    ctoken: false,
    domainWhiteList: ["http://127.0.0.1:7001", "http://192.168.1.101:7001"]
    };
    config.cors = {
    origin: "*",
    allowMethods: "GET,HEAD,PUT,POST,DELETE,PATCH"
    };

使用 Sequelize 操作 MySQL 数据库

  1. 添加 mysql 路径 export PATH=${PATH}:/usr/local/mysql/bin

  2. source ~/.bash_profile 或者 source ~/.zshrc

  3. mysql -u root -p 输入密码进入

  4. 创建一个数据库create database demo;

  5. ```js
    const Sequelize = require(“sequelize”);
    const sequelize = new Sequelize(“study01”, “root”, “965516531”, {
    host: “localhost”,
    dialect: “mysql”,
    pool: {

    max: 5,
    min: 0,
    acquire: 30000,
    idle: 10000
    

    },
    operatorsAliases: false
    });

    const User = sequelize.define(“user”, {
    username: Sequelize.STRING,
    birthday: Sequelize.DATE
    });

    sequelize
    .sync()
    .then(() =>

    User.create({
      username: "demo",
      birthday: new Date(1980, 6, 20)
    })
    

    )
    .then(jane => {

    console.log(jane.toJSON());
    

    });

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    使用此 js 文件测试数据库是否连接成功。如果使用 mysql8 版本创建数据库,选择使用宽松模式而不是强密码模式,因为大部分第三方库还未对强密码模式进行适配。

    如果使用TS的egg-mysql,需要在src/typings.d.ts里面添加上一段ts的定义,不然会报`类型“Application”上不存在属性“mysql”。`。
    ```ts
    import 'egg';

    declare module 'egg' {

    interface mysql {
    get(tableName: String, find: {}): Promise
    query(sql: String, values: Any[]): Promise
    }
    interface Application {
    mysql: mysql;
    }
    }

2021-11更新

使用egg-sequesize链接更为简单,只需要按照文档配置一下,然后写对应的model就可以使用了。注意的是可能需要将时间戳禁用掉

1
2
3
4
5
6
7
8
9
10
11
config.sequelize = {
dialect: 'mysql',
host: '',
port: 3306,
database: '',
username: 'root',
password: '',
define: {
timestamps: false, // 禁用时间戳
},
}

使用 egg-redis 连接 redis 数据库

  1. 安装 brew install redis
  2. 启动 brew services start redis
  3. 停止 brew services stop redis
  4. 或者使用 redis 自带的启动停止redis-server redis-server --port 6380 redis-cli shutdown
  5. 用法是 this.app.redis 这样使用

redis 常用命令

  1. 使用redis-cli进入 redis 客户端
  2. keys *获取所有的 key 值
  3. get key获取 key 对应的值
  4. flushall清除所有数据库的所有数据

使用 egg-mongoose 连接 mongo 数据库

npm i egg-mongoose --save

1
2
3
4
5
// plugin.js
exports.mongoose = {
enable: true,
package: "egg-mongoose"
};
1
2
3
4
5
6
7
8
9
10
11
12
// {app_root}/config/config.default.js
exports.mongoose = {
url: "mongodb://127.0.0.1/example",
options: {}
};
// recommended
exports.mongoose = {
client: {
url: "mongodb://127.0.0.1/example",
options: {}
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// {app_root}/app/model/user.js
module.exports = app => {
const mongoose = app.mongoose;
const Schema = mongoose.Schema;

const UserSchema = new Schema({
userName: { type: String },
password: { type: String }
});

return mongoose.model("User", UserSchema);
};

// {app_root}/app/controller/user.js
let nuser = ctx.model.User.create({
userName: "",
phoneNum: ctx.request.body.phoneNum,
password: ctx.request.body.password,
email: ""
});

一开始建集合之后填入数据老是报一个错E11000 duplicate key error collection,解决方法之一是删了集合重新弄。

mongoose 使用技巧

  1. 一个值多个字段进行查询

    1
    2
    3
    4
    5
    6
    7
    let user = await ctx.model.User.find({
    $or: [
    { phoneNum: parseInt(ctx.request.body.username) },
    { userName: ctx.request.body.username },
    { email: ctx.request.body.username }
    ]
    });
  2. 查询返回指定字段
    find 方法第一个参数表示查询条件,第二个参数用于控制返回字段,第三个参数用于配置查询参数,第四个参数是回调。如果使用第三个参数而不用第二个参数,那么设置为 null。
    如果指定某些字段返回那么设置那个字段值为 1,如果指定某些字段不返回设置值为 0
    let user: any = await ctx.model.User.find({ _id: ok.id }, { password: 0 }),

思否用法
官方文档
知乎用法

导入数据

在数据文件目录使用命令导入数据表 mongoimport -d meituan -c areas areas.dat meituan 是数据库名字 areas 是集合(表名) areas.dat 是数据文件。

使用 egg-jwt 进行 Token 的分发

  1. npm install egg-jwt --save
  2. ```js
    // {app_root}/config/plugin.js
    exports.jwt = {
    enable: true,
    package: “egg-jwt”
    };
    1
    2
    3
    4
    5
    3. ```js
    // {app_root}/config/config.default.js
    exports.jwt = {
    secret: "123456" //自己设置的值
    };
  3. 分发 Tokenconst token = app.jwt.sign(userToken, secret, {expiresIn: '1h'})
  4. 校验 Tokne const token = ctx.header.authorization
  5. 解密 Token let payload = await app.jwt.verify(token.split(' ')[1], secret) // // 解密,获取payload

参考资料
参数设置

JWT Tokne 的刷新和 sso 单点登录问题

使用 Token 进行权限验证,为了防止重放攻击所以一般 token 设置一个过期时间。假如设置 1 小时过期时间,而用户在一小时内不停操作,但是 token 失效,这时候用户就被迫进行重新登录,这是不行的,所以需要 token 刷新。
假设一个设备登录 此 id 生成一个 token 正常:验证没问题 超时:redis 找这个 token 的数据 查里面的长 tokne 刷新时间 长 token 过期 app 登录 ❌
假设设备登录获取一个 token 时效 30d 未过期登录 查 redis 里面的 id 验证是不是一个 token 不是的话 说明这是老旧的未过期的 tokne  然后直接让让 app 重新登录 登录的时候服务端刷新 token 保证最新 这是 sso 单点登录
 但是假如这个 app 在 29 点最后时刻在使用 这时候 token 过期了 服务端返回 token 过期提示 然后 app 根据状态吗重新请求 token ❎
直接重新登录 但是这个 token 设置的可以很长
这个地方我也奇怪为什么有两个 tokne 的说法:
JWT 为什么要设置 2 个 token?

参考资料

服务器设置了 cookie,浏览器却找不到?

使用 UUID 包进行 uuid 的创建

使用 crypto-js 进行密码的加密

使用crypto-js模块进行密码加密。

  1. npm i -save crypto-js
  2. let CryptoJS = require('crypto-js')
  3. CryptoJS.MD5(***).toString()

crypto-js

使用 egg-validate 进行数据的校验

  1. npm i egg-validate --save
1
2
3
4
5
// config/plugin.js
exports.validate = {
enable: true,
package: "egg-validate"
};
1
2
3
4
5
// config/config.default.js
exports.validate = {
// convert: false,
// validateRoot: false,
};

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const rule = {
phoneNum: "int",
email: {
type: "email",
required: false,
default: ""
},
userName: {
type: "string",
required: false,
default: ""
},
password: {
type: "password"
}
};

const error = this.app.validator.validate(rule, ctx.request.body);

若是 error 不为空,那么就是校验未通过,若是为空就是校验通过。
配置项及校验方式等写法参考 parameter:
parameter

使用 passport 来进行用户的(第三方)登录鉴权

不过这个插件的使用,好像是基于使用 egg 来渲染页面然后进行鉴权的方式,而 spa 大多使用 Token 的方式来进行验证。

参考资料

在 Mac 下安装 MySQL
Mac 电脑安装及终端命令使用 mysql

TSLint 设置

  1. 引入一些没有 d.ts 文件的模块时提示不允许使用 require。解决办法:"no-var-requires": false,

  2. 必须使用单引号,jsx 中必须使用双引号,去掉 singel 就可以

    1
    2
    3
    4
    5
    "quotemark": [
    true,
    "single",
    "jsx-double"
    ],
  3. 对尾随逗号的校验

    1
    2
    3
    4
    5
    6
    7
    8
    9
    "trailing-comma": [true, { //对尾随逗号的校验
    "multiline": {
    "objects": "ignore",
    "arrays": "ignore",
    "functions": "ignore",
    "typeLiterals": "ignore"
    },
    "esSpecCompliant": true //是否允许尾随逗号出现在剩余变量中
    }]
  4. 行尾是否以空格结尾(设置成 false 就不用必须加分号)

    1
    2
    3
    4
    5
        "no-trailing-whitespace": [// 不允许空格结尾
    true,
    "ignore-comments",
    "ignore-jsdoc"
    ],

优质资源

《Node.js 实战(egg+vue)》

  1. 书里还有发送邮件的方式 暂时不看

egg-shell-decorators(蛋壳)

Egg.js 路由装饰器,让你的开发更敏捷~

自带路由解析和 Swagger。
蛋壳

  1. npm install egg-shell-decorators -S

  2. ```ts
    // app/router.ts
    import { Application } from “egg”;
    import { EggShell } from “egg-shell-decorators”;

    export default (app: Application) => {
    EggShell(app, { prefix: “/“, quickStart: true });
    };

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    3. demo

    ```ts
    // app/controller/user.ts
    import { Controller } from "egg";
    import {
    Get,
    IgnoreJwtAll,
    Description,
    TagsAll
    } from "egg-shell-decorators";

    @TagsAll("用户")
    @IgnoreJwtAll
    export default class SubOrderController extends Controller {
    @Get("/:id")
    @Description("根据id获取用户详情")
    public listUser({ params: { id } }) {
    return {
    id
    };
    }
    }
  3. 添加 swagger-ui
    node-swagger-ui
    作者提供的汉化版 swagger-ui 地址,并且附带使用 express 启动的 index.js 文件。
    将整个项目 clone 下来,放到 app 目录下面 api-docs 文件夹里面并npm i

  4. router.js 改造

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    import { Application } from "egg";
    import { EggShell } from "egg-shell-decorators";

    export default (app: Application) => {
    EggShell(app, {
    prefix: "/",
    quickStart: true,
    swaggerOpt: {
    open: true,
    title: "示例",
    version: "1.0.0",
    host: "127.0.0.1",
    port: 7001,
    schemes: ["http"],
    paths: {
    outPath: "./api-docs/public/json/main.json",
    definitionPath: "app/definitions",
    swaggerPath: "app/swagger"
    },
    tokenOpt: {
    default: "manager",
    tokens: {
    manager: "123",
    user: "321"
    }
    }
    }
    });
    };

    ok 这样使用装饰器写的路由便会自动生成可以被 swagger 使用的 json 文档。

  5. 在 api-docs 目录里面使用 pm2 启动 index.js,pm2 start index.js,没装 pm2 的话 安装一下npm i pm2 -g

  6. localhost:3001 完工
    demo

  7. 如果你想尽量少些装饰器,使得 controller 看起来不那么臃肿,那么你也可以分开写。将模型写到app/definitions,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // app/definitions/user.json
    {
    "User": {
    "type": "object",
    "properties": {
    "userName": {
    "type": "string",
    "description": "姓名"
    },
    "phoneNum": {
    "type": "integer",
    "format": "int32",
    "description": "手机号码"
    },
    "email": {
    "type": "string",
    "description": "邮箱"
    },
    "password": {
    "type": "string",
    "description": "密码"
    }
    }
    }
    }

    swagger 的 req 和 res 框,写在app/swagger里面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // app/swagger/user.json
    {
    "/SignUp": {
    "post": {
    "description": "用户注册",
    "parameters": [
    {
    "name": "body",
    "in": "body",
    "required": true,
    "schema": {
    "$ref": "User"
    }
    }
    ],
    "responses": {
    "type": "object",
    "schema": {
    "$ref": "User"
    }
    }
    }
    },
    "/VerificationCode": {
    "get": {
    "description": "发送验证码"
    }
    }
    }

    保持和 controller 文件夹里面的文件名一致,因为使用了路由映射。
    controller/user.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    import { Controller } from "egg";
    import { Post, Get } from "egg-shell-decorators";
    export default class UserController extends Controller {
    @Get("/VerificationCode")
    public async verificationCode() {
    let code = new Date().getTime();
    return {
    phoneNum: this.ctx.query.phoneNum || "",
    code,
    msg: "验证码发送成功!"
    };
    }
    @Post("/SignUp")
    public async singup() {
    const { ctx, app } = this;
    const rule = {
    phoneNum: "int",
    email: {
    type: "email",
    required: false,
    default: ""
    },
    userName: {
    type: "string",
    required: false,
    default: ""
    },
    password: {
    type: "password"
    }
    };
    const error = app.validator.validate(rule, ctx.request.body);
    if (error) {
    return {
    code: -1,
    msg: error
    };
    } else {
    let user = await ctx.model.User.find({
    phoneNum: ctx.request.body.phoneNum
    });
    if (user.length) {
    return {
    code: -1,
    msg: "账号已经注册!"
    };
    } else {
    let nuser = await ctx.model.User.create(ctx.request.body);
    if (!nuser) {
    return {
    code: -1,
    msg: nuser
    };
    } else {
    return {
    code: 0,
    msg: "注册成功!"
    };
    }
    }
    }
    }
    }
  8. QuickStart 模式会自动帮助我们处理响应体,但这会导致多一层数据嵌套,可以选择在配置里将 QuickStart

  9. 如果使用 egg-jwt 做 token 校验,我们可以使用IgnoreJwt装饰器对当前路由进行忽略校验,使用IgnoreJwtAll可以对 controller 都进行忽略校验。

  10. ★★★ 有个问题注意下 蛋壳不是洋葱圈模型而是类似于注入,因此无法在中间件进行拦截,所以上面的 egg-jwt 校验也无法生效,需要手动用 egg 原生的中间件进行校验。 作者大大说找时间解决,waining…

EggJS 项目部署

typescript代码下的项目部署

  1. npm install --production 可以执行这个命令也可以不执行,执行这个命令只会安装生产用的安装包并压缩。如果不执行这个命令,可以提高本地压缩效率,但是需要生产解压缩之后装一下 ets(npm i) 用来跑 npm run tsc,因为需要将ts文件构建成js文件。
  2. 拿到编译构建完之后的项目,使用npm start即可在生产环境运行此项目。具体的命令配置可以参考 文档
  3. 使用npm stop可以停止服务运行

    总结

做登录的时候有一个重要的问题,就是 token 的刷新问题。具体解决方法见上面 jwt 模块。

Author: XavierShi
Link: https://blog.xaviershi.com/2018/12/03/EggJS开发最佳实践/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.