治电EggJS开发规范

2019-02-06  本文已影响62人  ZZES_ZCDC

1.编码规范

1.1 编码格式与语法

项目默认编码格式统一为UTF-8格式,语法采用ES6+语法

1.2 代码注释

注释符号后要有一个空格

1.2.1 函数/方法注释

函数/方法注释放置于函数/方法的上方,主要描述函数/方法功能以及参数类型,参数和返回值说明

/**
 * 功能
 * @param  {参数类型} 参数名 参数说明
 * @return {返回值类型} 返回值 返回值说明
 */

1.2.2 单行注释

对代码做简要说明
// 功能简要说明

1.3 代码分段及缩进

每段代码应放在一个代码块中。块内的代码都应该统一地缩进一个单位。

1.3.1 使用空格作为缩进

使用2个空格作为一个缩进单位。

1.3.2 代码块符号

代码块的开始符号要放在行尾,不可单独一行;代码块结束符号要单独一行。

function demo() { // 代码块开始符号
  // ...
} // 代码块结束符号

1.4 空白行分隔

不同功能或多个代码块之间,使用空白行分隔

/**
 * 方法1
 */
function demo1() {

} 

/**
 * 方法2
 */
function demo1() {
  ...... // 代码块1

  ...... // 代码块2
} 

1.5 命名规则

全部使用英文单词

1.5.1 文件命名

1.5.2 变量与常量命名

尽量使用const代替let
若变量需要改变才使用let
固定常量为全大写,其余使用首字母小写的驼峰命名法

1.5.3 函数/方法命名

使用首字母小写的驼峰命名

1.6 引号

一般情况使用单引号,若字符串拼接,使用"``"和"${}"

1.7 分号

不用分号

2.项目规范

库的安装和项目的初始化全部使用yarn

2.1 项目生成

npm install -g yarn egg-init egg-init 项目名 --type=simple
cd 项目名 yarn install

2.2 安装第三方库

$ yarn add 库名

2.3 项目运行

2.3.1 项目开发运行

$ yarn dev

2.3.2 项目部署运行

$ yarn start

2.3.3 项目docker运行

向package.json中的scripts键添加一个值

"scripts": {
  ......
  "docker": "egg-sequelize db:migrate && egg-scripts start",
  ......
}

2.4 项目目录

.
├── app.js
├── app
│   ├── router.js 
│   ├── controller            
│   ├── extend           
│   ├── middleware
│   ├── service
│   ├── public
│   ├── view
│   ├── router
│   ├── io     (自建socket.io目录)
│   │   ├── middleware
│   │   └── controller
│   └── model (自建Sequelize目录)
├── config
│   ├── plugin.js
│   ├── config.default.js
│   └── config.prod.js
├── migrations(自建Sequelize目录)
├── logs
└── test
    └── app
        ├── middleware
        └── controller

以上目录约定如下:

  1. app/router.js 用于配置URL路由规则。
  2. app/controller/ 用于解析用户输入,处理后返回响应结果。
  3. app/extend/ 用于框架内部对象的拓展(request,response,context,application)和工具类(helper)的编写。
  4. app/middleware/ 用于编写中间件。
  5. app/service/ 用于编写业务逻辑,如数据库操作的封装,api请求的封装等。
  6. app/public/ 用于放置静态文件。
  7. app/view/ 用于放置模板文件(可能不需要)。
  8. app/model/ 用于放置数据模型(若使用Sequelize)。
  9. app/router/ 用户放置分离的路由
  10. migrations/ 用与放置数据库迁移的文件。
  11. logs/ 日志存放目录。
  12. test/ 测试文件目录。
  13. app.js 用于自定义启动时的初始化工作。

2.5 项目相关文件说明

所有代码均在'use strict'严格模式下开发

2.5.1 extend

包含四个对象对应的文件,以及一个helper工具类

1.代码格式

'use strict'

module.exports = {
  /**
   * 方法说明
   * @param {参数类型} 参数名 参数说明
   * @return {返回值类型} 返回值名 返回值说明
   */
  方法名(参数) {
    // 处理逻辑
    return 返回值
  },
  get 属性名() {
    // 属性相关逻辑
    return 属性
  },
  set 属性名(值) {
    this.set(键, 值)
  },
}

2.application.js

对应Application对象

访问方式:

对应context对象

访问方式:

对应request.js对象

访问方式:

对应response.js对象

访问方式:

工具类,将请求成功和请求失败返回封装的函数以及错误码的封装写到里面

访问方式:

const { app } = this.ctx;
const ctx = app.createAnonymousContext();
ctx.helper.ROOTURL //此变量即可调用

封装状态码,将其解释写在helper里,方便调用

module.exports = {
  errorCode: {
    200: '请求成功。客户端向服务器请求数据,服务器返回相关数据',
    201: '资源创建成功。客户端向服务器提供数据,服务器创建资源',
    202: '请求被接收。但处理尚未完成',
    204: '客户端告知服务器删除一个资源,服务器移除它',
    206: '请求成功。但是只有部分回应',
    400: '请求无效。数据不正确,请重试',
    401: '请求没有权限。缺少API token,无效或者超时',
    403: '用户得到授权,但是访问是被禁止的。',
    404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
    406: '请求失败。请求头部不一致,请重试',
    410: '请求的资源被永久删除,且不会再得到的。',
    422: '请求失败。请验证参数',
    500: '服务器发生错误,请检查服务器。',
    502: '网关错误。',
    503: '服务不可用,服务器暂时过载或维护。',
    504: '网关超时。',
  }
}

在Controller中响应客户端时,使用

this.ctx.body = {
  code: 400,
  message: this.ctx.helper.errorCode[400],
  data:{
    error:'err'
  }
}

封装请求成功的方法

module.exports= { 
  success: ({ ctx, code=200, res=null }) => {
    ctx.status = 200
    ctx.body = {
      code: code,
      message: ctx.helper.errorCode[code],
      data: res
    }
  }
}

封装请求失败的方法

module.exports = {
  fail: ({ ctx, code=500, res=null }) => {
    ctx.status = 200
    ctx.body = {
      code: code,
      message: ctx.helper.errorCode[code],
      data: {
        error: res
      }
    }
  }
}

请求封装使用例子

ctx.helper.success({ ctx, code:200, res:'success' })
ctx.helper.fail({ ctx, code:500, res:'fail' })

2.5.2 配置文件

1.plugin.js

引入第三方插件

代码格式:

exports.插件名 = {
  enable: true,
  package: '库名'
}

例如:

exports.jwt = {
  enable: true,
  package: 'egg-jwt'
}

2.config.{{env}}.js
访问方式:

'use strict'

module.exports = appInfo => {
  const config = exports = {}
  // 全局变量
  config.变量名=''
  // 插件名
  config.插件名= {
    // 相关配置
  }
}

例如:

'use strict'

module.exports = appInfo => {
  const config = exports = {}
  // redis 配置
  config.redis = {
    client: {
      port: process.env.RS_PORT || 6379,
      host: process.env.RS_HOST || '0.0.0.0',
      password: process.env.RS_PASSWORD || '',
      db: 0,
    }
  }
}

中间件。对于一些错误拦截,请求处理,需要使用中间件完成。

配置方法:

config.middleware = ['demoMiddleware']
module.exports = app => {
  const demo = app.middleware.demoMiddleware()
  app.router.get('/url', demo, app.controller.demo)
}

代码格式:

'use strict'

module.exports = () => {
  return async function 方法名(ctx, next) {
    await next()
    // 相关逻辑
  }
}

例如:

'use strict'

module.exports = () => {
  return async function notFoundHandler(ctx, next) {
    await next()
    if (ctx.status === 404 && !ctx.body) {
      ctx.body = { error: 'Not Found' }
    }
  }
}

处理错误信息的中间件

'use strict'

module.exports = (option, app) => {
  return async function(ctx, next) {
    try {
      await next()
    } catch (err) {
      app.emit('error', err, this)
      const status = err.status || 500
      // 生产环境下不将错误内容返回给客户端
      const error = status === 500 && app.config.env === 'prod'
        ? '服务器错误,请联系管理员!'
        : err.message
      ctx.helper.fail({ctx, code:status, res:error})
      if(status === 422) {
        ctx.helper.fail({ctx, code:status, res:err.errors})
      }
    }
  }
}

2.5.4 Service

保持Controller中逻辑简洁,以及业务逻辑的独立性,抽象出的Service可以被多个Controller调用。比如封装数据库操作的方法,API请求封装,第三方服务调用等。

访问方式:

'use strict'

const Service = require('egg').Service

class 类名 extends Service {
  /**
   * 方法说明
   * @param {参数类型} 参数名 参数说明
   * @return {返回值类型} 返回值名 返回值说明
   */
  async 方法名(参数名) {
    // 方法逻辑
    return 返回值
  }
}

方法例子如下:

/**
 * 添加礼物
 * @param {string} livecode 编号
 * @return {int} giftnum 礼物数量
 */
async addGift(livecode) {
  const { app }  = this
  const giftnum = await this.ctx.model.query(`UPDATE live SET gift=gift+1 WHERE livecode='${livecode}'`)
  return giftnum
}

2.5.5 Controller

三种功能,处理restful接口用户传来的参数;模板渲染;请求代理

访问方式:

'use strict'

const Controller = require('egg').Controller

class 首字母大写的文件名+Controller extends Controller {
  async 方法名() {
    // 相关逻辑
    ctx.body = {
      // 返回给restful api接口的请求者
    }
  }
}

例如:

'use strict'

const Controller = require('egg').Controller

class HomeController extends Controller {
  async index() {
    const {ctx} = this
    if(ctx.isAuthenticated) {
      ctx.body = {
        user: ctx.user
      }
    } else {
      ctx.body = {
        user: 'fail'
      }
    }
  }
}
module.exports = HomeController

2.5.6 router.js路由文件

描述请求的URL与controller建立的联系。将各个路由分离开来,分别放入router文件夹,并在router.js中引入

代码格式:

'use strict'

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app
  router.操作('/URL路径', 中间件1, ...中间件n, controller.文件名.方法) 
}

例如:

'use strict'

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app
  const roleCheck = app.middleware.roleCheck()
  router.get('/user', app.jwt, roleCheck, controller.user.getAllUser)
}

带参路由,两种形式,以及获取参数的方式:

  1. 使用形如/uri/:id的uri,在Controller中使用ctx.request.params获取 例如:
// router.js
router.get('/user/:id', controller.user.info)

// controller
const { ctx } = this
const { id } = ctx.request.params
  1. 使用形如/uri?id=1&age=1的uri,在Controller中使用ctx.query获取 例如:
// router.js
router.get('/user?id=1&age=1', controller.user.msg)

// controller
const { ctx } = this
const { id, age } = ctx.query

2.6 安全配置

开发的时候关闭csrf,防止无法请求接口

// csrf关闭
config.security = {
  csrf: {
    enable: false
  }
}

2.7 Sequelize

2.7.1 安装

$ yarn add egg-sequelize mysql2

2.7.2 启用与配置

在plugin.js中启用Sequlize

exports.sequelize = {
  enable: true,
  package: 'egg-sequelize'
}

在config.{{env}}.js中配置数据库连接.账户相关的信息,开发状态下将信息填入config.local.js;部署环境下,将信息填入config.prod.js

config.sequelize = {
  dialect: 'mysql',
  database: process.env.DB_DATABASE || '数据库名',
  host: process.env.DB_HOST || 'IP地址',
  port: process.env.DB_PORT || '数据库端口号',
  username: process.env.DB_USER || '数据库用户名',
  password: process.env.DB_PASSWORD || '数据库密码',
  timezone: '+08:00'
}

在package.json中配置数据库迁移命令

"scripts": {
  ...
  "migrate:new": "egg-sequelize migration:create",
  "migrate:up": "egg-sequelize db:migrate",
  "migrate:down": "egg-sequelize db:migrate:undo"
}

开发过程中配置自动同步数据库(仅开发模式),在app.js中写入

module.exports = app => {
  if(app.config.env === 'local') {
    app.beforeStart(async () => {
      await app.model.sync({force:true})
    })
  }
}

2.7.3 model数据模型开发

文档参考:https://demopark.github.io/sequelize-docs-Zh-CN/models-definition.html

'use strict'

module.exports = app => {
  const {类型} = app.Sequelize
  const 首字母大写的表名 = app.model.define('表名', {
    字段名: {
      type: 类型,
      // 其他属性
      // 是否是唯一
      unique: true,
      // 定义主键
      primaryKey: true,
      // 自增
      autoIncrement: true,
      // 校验
      validate: {
        is: ["^[a-z]+$",'i'],     // 只允许字母
        is: /^[a-z]+$/i,          // 与上一个示例相同,使用了真正的正则表达式
        not: ["[a-z]",'i'],       // 不允许字母
        isEmail: true,            // 检查邮件格式 (foo@bar.com)
        isUrl: true,              // 检查连接格式 (http://foo.com)
        isIP: true,               // 检查 IPv4 (129.89.23.1) 或 IPv6 格式
        isIPv4: true,             // 检查 IPv4 (129.89.23.1) 格式
        isIPv6: true,             // 检查 IPv6 格式
        isAlpha: true,            // 只允许字母
        isAlphanumeric: true,     // 只允许使用字母数字
        isNumeric: true,          // 只允许数字
        isInt: true,              // 检查是否为有效整数
        isFloat: true,            // 检查是否为有效浮点数
        isDecimal: true,          // 检查是否为任意数字
        isLowercase: true,        // 检查是否为小写
        isUppercase: true,        // 检查是否为大写
        notNull: true,            // 不允许为空
        isNull: true,             // 只允许为空
        notEmpty: true,           // 不允许空字符串
        equals: 'specific value', // 只允许一个特定值
        contains: 'foo',          // 检查是否包含特定的子字符串
        notIn: [['foo', 'bar']],  // 检查是否值不是其中之一
        isIn: [['foo', 'bar']],   // 检查是否值是其中之一
        notContains: 'bar',       // 不允许包含特定的子字符串
        len: [2,10],              // 只允许长度在2到10之间的值
        isUUID: 4,                // 只允许uuids
        isDate: true,             // 只允许日期字符串
        isAfter: "2011-11-05",    // 只允许在特定日期之后的日期字符串
        isBefore: "2011-11-05",   // 只允许在特定日期之前的日期字符串
        max: 23,                  // 只允许值 <= 23
        min: 23,                  // 只允许值 >= 23
        isCreditCard: true,       // 检查有效的信用卡号码

        // 也可以自定义验证:
        isEven(value) {
          if (parseInt(value) % 2 != 0) {
            throw new Error('Only even values are allowed!')
            // 我们也在模型的上下文中,所以如果它存在的话, 
            // this.otherField会得到otherField的值。
          }
        }
      }
    }
  },{
    // 配置表名 
    tableName: '表名', 

    // 不添加时间戳属性 (updatedAt, createdAt)
    timestamps: true,

    // 不删除数据库条目,但将新添加的属性deletedAt设置为当前日期(删除完成时)。 
    // paranoid 只有在启用时间戳时才能工作
    paranoid: true,

    // 不使用驼峰样式自动添加属性,而是下划线样式,因此updatedAt将变为updated_at
    underscored: true,

    // 禁用修改表名; 默认情况下,sequelize将自动将所有传递的模型名称(define的第一个参数)转换为复数。 如果你不想这样,请设置以下内容
    freezeTableName: true,

    // 不使用createdAt
    createdAt: false,

    // 我想 updateAt 实际上被称为 updateTimestamp
    updatedAt: 'updateTimestamp',

  })
  首字母大写的表名.associate = function() {
    // 表关联
  }
  return 首字母大写的表名
}

例如:

'use strict'

module.exports = app => {
  const {STRING} = app.Sequelize
  const Role = app.model.define('role', {
    name: {
      type: STRING,
      unique: true,
      allowNull: false
    }
  }, {
    timestamps: true, 
    tableName: 'role',
    underscored: false
  })
  Role.associate = function() {
    app.model.Role.hasMany(app.model.UserRole)
  }
  return Role
}

2.7.4 migrations的使用

2.7.5 操作数据库

一般在Service中进行数据库操作,常用方法有findOne, findAll, create, destory, update等。文档参考:https://demopark.github.io/sequelize-docs-Zh-CN/models-usage.html

例子:

const result = await this.ctx.service.role.create({
  name: name
})

2.8 Redis

2.8.1 安装

$ yarn add egg-redis

2.8.2 启用与配置

在plugin.js中启用Sequlize

exports.redis = {
  enable: true,
  package: 'egg-redis'
}

在config.{{env}}.js中配置数据库连接

config.redis = {
  client: {
    port: process.env.RS_PORT || 'Redis主机端口号',
    host: process.env.RS_HOST || 'Redis主机地址',
    password: process.env.RS_PASSWORD || '',
    db: '缓存数据库名称',
  }
}

2.8.3 使用方法

具体方法和redis原生方法基本一致,原生api地址:https://www.cheatography.com/tasjaevan/
cheat-sheets/redis/

调用方法:

2.9 Socket.IO

2.9.1 安装

$ yarn add egg-socket.io uws

2.9.2 启用与配置

在plugin.js中启用Sequlize

exports.io = {
  enable: true,
  package: 'egg-socket.io'
}

在config.{{env}}.js中配置数据库连接

config.io = {
  init: {
    wsEngine: 'uws' // 使用uws
  },
  namespace: {
    '命名空间路径': {
      connectionMiddleware: ['中间件名称'], // 处理客户端连接与断开连接时的中间件
      packetMiddleware: ['中间件名称']      // 处理客户端发送信息到服务端时的中间件
    }
  },
  redis: {
    host: process.env.RS_HOST || 'Redis主机地址',
    port: process.env.RS_PORT || 'Redis主机端口号'
  }
}

2.9.3 文件格式

新建名为io的文件夹,并在其中分别建立两个文件夹controller和middleware,控制器和中间件的文件命名格式以及编码格式与eggjs的一样目录如下:

io
├── controller
└── middleware

2.9.4 Socket.IO路由配置

通过io.of设置命名空间,route()方法第一个参数是订阅的话题,第二个是使用的控制器

app.io.of('/').route('new message', io.controller.chat.newMessage)

2.9.5 使用方法

  1. 获取url参数
const {参数} = this.socket.handshake.query
  1. 加入房间
this.ctx.socket.join('房间') // 加入房间
  1. 获取客户端socketId
this.ctx.socket.id
  1. 获取当前房间所有客户端
this.app.io.of('/').adapter.clients([房间], (err, clients) => {
})
  1. 设置命名空间
const nsp = this.app.io.of('/')
  1. 推送数据

2.10 参数校验

2.10.1 安装

$ yarn add egg-validate

2.10.2 启用

在plugin.js中启用validate

exports.validate = {
  enable: true,
  package: 'egg-validate'
}

2.10.3 使用方法

validate有两个参数,第一个是需要校验的参数,另一个是需要校验的对象。

const { user } = this.ctx.request.body
this.ctx.validate({
  username: { type: 'string', required: true },
  password: { type: 'string', required: true }
}, user)

2.11 模板渲染(选用nunjucks)

2.11.1 安装

$ yarn add egg-view-nunjucks

2.11.2 启用与配置

在plugin.js中启用nunjucks

exports.nunjucks = {
  enable: true,
  package: 'egg-view-nunjucks'
}

在config.default.js中配置渲染引擎

  1. 对指定后缀文件使用模板引擎渲染
config.view = {
  mapping: {
    '.nj':'nunjucks'
  }
}
  1. 渲染时无需写文件后缀
config.view = {
  defaultExtension: '.nj'
}

2.11.3 使用方法

Context对象存在三个接口使用模板引擎,使用renderString时需要指定模板引擎,如果定义了defaultViewEngine这里可以省略:

  1. controller/home.js
async test() {
  const ctx = this.ctx
  await ctx.render('index',{data:[
    {id: 1, name: "test"},
    {id: 2, name: "test"},
    {id: 3, name: "test"},
    {id: 4, name: "test"},
    {id: 5, name: "test"}
  ]})
}
  1. router.js
router.get('/', controller.home.test);
  1. view/index.nj
<div class="wrap">
  <button class="btn btn-warning">test</button>
  {% for item in data %}
  <div>{{item.name}}</div>
  {% endfor%}
</div>

2.11.4 静态文件

  1. 到config.js中开启static,默认是注释掉的,egg-static属于内置插件
exports.static = true;
  1. 静态文件存放路径:app/public
  2. 引用方式

2.12 Git规范

2.12.1 分支类型

feature

2.12.2 分支命名

基本格式(全为英文)

例子:

2.12.3 开发流程

master 迁出develop分支 -> develop分支迁出feature功能分支 -> 提交pr审查代码 -> 提交QA -> 合并到develop -> 合并到master

2.12.4 提交格式

使用git cz代替git commit
插件地址:https://github.com/commitizen/cz-cli

  1. 安装
$ npm install -g commitizen
  1. 在项目中运行下面命令,使其支持 Angular 的 Commit message 格式
$ commitizen init cz-conventional-changelog --save --save-exact
  1. 使用
$ git add .
$ git cz
  1. 格式的选择
  1. 详细问题
    记录修改的文件名?

2.13 接口自测

2.13.1 软件

2.13.2 操作规范

对负责的模块单独建立文件夹,将接口存放进去。将接口请求后的数据格式与约定返回的数据格式做对比。

3.RESTful API规范

3.1 请求协议

3.2 请求方法

请求方法 功能
GET 获取资源
POST 新增资源
PUT 更新整个资源
PATCH 更新个别资源
DELETE 删除资源

3.3 状态码

3.3.1 成功状态码

状态码 定义
200 请求成功。客户端向服务器请求数据,服务器返回相关数据
201 资源创建成功。客户端向服务器提供数据,服务器创建资源
202 请求被接收。但处理尚未完成
204 客户端告知服务器删除一个资源,服务器移除它

3.3.2 错误状态码

状态码 错误描述
400 请求无效。数据不正确,请重试
401 请求没有权限。缺少API token,无效或者超时
403 请求未被授权。当前权限无法获取指定的资源
404 请求失败。请求资源不存在
406 请求失败。请求头部不一致,请重试
422 请求失败。请验证参数

3.3.3 服务器错误状态码

状态码 定义
500 服务器发生错误,请检查服务器
502 网关错误
503 服务不可用,服务器暂时过载或维护
504 网关超时

3.3.4 自定义状态码

具体而定

3.4 版本号

3.5 URL规范

RESTful API的所有操作都是针对特定资源进行的。资源就是URL所表示的,URL需要符合以下规范:

  1. /api/{资源名}/{描述名}
  2. /api/{资源名}/{对象id}/{描述名}
    例子:

3.6 请求体格式

{
  "data": {
    "键":"值"
  }
}

例子:

{
  "data": {
    "username":"klren",
    "password":"123456"
  }
}

3.7 返回体格式

正常返回

{
  "code": "状态码",
  "msg":"状态描述",
  "data":"具体内容"
}

例子:

{
  "code": 200,
  "msg": "success",
  "data": {
    "username": "klren",
    "email":"klren@qq.com"
  }
}

错误返回

{
  "code": "错误状态码",
  "msg": "错误信息",
  "data": {
    "error": "错误详情",
  }
}

例子:

{
  "code": "400",
  "msg": "数据格式错误",
  "data": {
    "error": "格式错误"
  }
}

3.8 过滤、分页与排序

采用'?'符号进行这些操作

3.8.1 过滤

使用唯一的查询参数进行过滤
例子:

3.8.2 排序

使用sort,后面跟着键名和排序方式
例子:

3.8.4 分页

使用limit和offset,后面跟具体数字。limit后面跟每页最多数据量,offset后面跟数据起始位。
请求例子:

{
  "code":200,
  "message":'success',
  "data":{
    "total": 0,
    "pageIndex": 1,
    "pageSize": 10,
    "list":[{},{},{}...]
  }
}

3.9 请求格式

上一篇 下一篇

猜你喜欢

热点阅读