# 代码生成器

# 概述

Vue是一个以数据驱动的MVVM框架,在开发大型应用时,推荐使用Vuex作数据状态管理。前后端分离开发模式协作时也面临着对接口响应模拟数据需求。 根据Vue推荐的开发实践,在开发一个相对规模较大的应用,底层与后端交互时,代码分为以下几个部分:

代码块 说明
api 抽取出API请求
store 状态管理vuex,包含actions、mutations、modules、 mutation-types
mock 模拟数据模板

在实践中,我们发现上述的代码重复率非常高,新增和修改都费力,并且是没技术含量的体力活。 但又必须要这样做,不适合以公共函数的形式重用,为了解决这个重复的体力活,我们开发了代码生成器,用工具来代替体力活。

对于前后端的数据交互都可以归纳为是对实体的 新增、修改、删除、查询。

代码生成器的原理是也是按照对实体的增、删、改、查规范,利用NodeJs根据接口的配置文件(包含请求路径、请求选项、状态管理、模拟数据)按指定模板创建符合Vue开发规范的js文件到工程指定目录下。

# 示例

以下通过一个用户的实体配置,讲解由代码生成器生成的代码文件

1、新建文件 /schemas/user.js 以下配置实现用户的增、删、改、查接口调用、状态管理,并响应模拟数据

module.exports = {
  vuex: true, // 是否采用vuex模式, 代码模式支持 vuex 和 mixin,false表示采用mixin模式

  // 实体模型
  model: {
    path: '/api/demo/user', // 请求实体api路径

    // 模拟实体属性字段数据
    columns: {
      id: '@guid',
      name: '@cname',
      email: '@email'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

TIP

新建实体配置文件必须要在/schemas/目录下,可以建目录。

2、运行命令 npm run codernpm run dev 生成代码,代码文件目录在 /.my/code/, 目录结构如下:

Alt text

# 约定

代码生成器生成的代码需要与后端接口有预先的约定

# 请求URL的约定

前后端分离开发最佳实践是采用RESTful的接口形式进行通信,在此我们对请求URL进行统一的规范约定

请求类型 前端约定的方法前缀 请求方法类型 URL格式 示例
新增 add POST http://[主机ip]:[端口]/[路径]/[实体名称] http://127.0.0.1:80/api/users
更新 update PATCH http://[主机ip]:[端口]/[路径]/[实体名称] http://127.0.0.1:80/api/users
删除 remove DELETE http://[主机ip]:[端口]/[路径]/[实体名称]/[主键ID] http://127.0.0.1:80/api/users/123
查询单个实体 get GET http://[主机ip]:[端口]/[路径]/[实体名称]/[主键ID] http://127.0.0.1:80/api/users/123
分页查询多个实体 fetch GET http://[主机ip]:[端口]/[路径]/[实体名称] http://127.0.0.1:80/api/users
批量删除 batch POST http://[主机ip]:[端口]/[路径]/[实体名称]/remove http://127.0.0.1:80/api/userss/remove

# 请求传参的约定

GET和POST两种请求方式的的传参约定

请求方式 传参形式 示例
GET Url查询字符串,中文需要转码 http://127.0.0.1:80/api/user/list?keyword=%E5%A7%93%E5%90%8D
DELETE Url查询字符串,中文需要转码 http://127.0.0.1:80/api/user/list?keyword=%E5%A7%93%E5%90%8D
POST JSON格式字符串,请求头需要加上Content—Type=application/json http://127.0.0.1:80/api/user/save
{"name":"kenny", "password":"123"}
PATCH JSON格式字符串,请求头需要加上Content—Type=application/json http://127.0.0.1:80/api/user/remove
{"name":"kenny", "password":"123"}

# 分页查询多个实体,请求参数约定

参数名 说明
limit 每页几条数据
page 页码,从1开始

# 响应数据的约定

请求类型 成功响应 异常响应
新增 {"code": 0, "data": {...model}, "msg": "成功"} {"code": 403, "msg": "权限不足"}
更新 {"code": 0, "data": true, "msg": "成功"} {"code": 403, "msg": "权限不足"}
删除 {"code": 0, "data": true, "msg": "成功"} {"code": 403, "msg": "权限不足"}
查询单个实体 {"code": 0, "data": {...model}, "msg": "成功"} {"code": 403, "msg": "权限不足"}
分页查询多个实体 {"code": 0, "data": {"list": [{...model},...], "page":1, "pages": 1000, "total": 10000), "msg": "成功"} {"code": 403, "msg": "权限不足"}
批量删除 {"code": 0, "data": true, "msg": "成功"} {"code": 403, "msg": "权限不足"}

TIP

新增的请求建议返回新增后的实体对象,便于前端实现增、删、改、查功能

# 分页查询多个实体 响应示例

{
  "msg": "成功",
  "code": 0,
  "data": {
    "limit": 10,
    "list": [{
      "appId": "9b2b8839bc8f4edbb9ef783cb9ea1d2f",
      "code": "navigation",
      "contextPath": "/navigation",
      "menus": null,
      "name": "后台管理",
      "orderNum": 1,
      "pkValue": "9b2b8839bc8f4edbb9ef783cb9ea1d2f",
      "remark": "3"
    }, {
      "appId": "384f5ee687f04cd9b4e6410350d4e07c",
      "code": "resource",
      "contextPath": "/resource",
      "menus": null,
      "name": "资源库",
      "orderNum": 2,
      "pkValue": "384f5ee687f04cd9b4e6410350d4e07c",
      "remark": null
    }, {
      "appId": "e9ce35e8731448de9ab0f94a1eaa4ff9",
      "code": "4",
      "contextPath": "3",
      "menus": null,
      "name": "12",
      "orderNum": 12,
      "pkValue": "e9ce35e8731448de9ab0f94a1eaa4ff9",
      "remark": "33"
    }],
    "page": 1,
    "pages": 1,
    "total": 3
  }
}
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

# 响应码code的约定

0:执行成功
401:未登录(需要重新登录)
403:权限不足
500:系统内部错误(非业务代码里显式抛出的异常,例如由于数据不正确导致空指针异常、数据库异常等等)
1
2
3
4

# 请求跨域问题解决方案

前后端分离开发模式,由于前后端可以进行单独部署,请求跨域问题很常见,解决方法主要有两种方式:

  1. 采用nginx代理
  2. 接口响应头开启Cross (推荐)

响应头需要做以下配置, 同时后端需要开放OPTIONS类型的请求,跨域带cookie的请求时,浏览器先试探性发送OPTIONS请求,成功后才发起真正的请求

 Access-Control-Allow-Credentials:true
 Access-Control-Allow-Origin:请求的host
1
2

如:

 Access-Control-Allow-Credentials:true
 Access-Control-Allow-Origin:http://www.fbknav.cn:98
1
2

TIP

建议后端代码不要写死Access-Control-Allow-Origin,可以从请求头中动态获取 Origin

如果接口不需跨域带cookie验证信息,可以简单设置

 Access-Control-Allow-Origin: *
1

# 全局配置

当后端提供的接口与约定的规范不一致的时候,可以通过配置进行相应的调整做适配,但建议还是按前端建议的约定规范。

TIP

注意:不是任何接口都能通过全局来适配, 要具体问题具体分析。 全局配置的文件路径:/my.config.js, 配置说明

# 常用的配置项

# pathPrefix

配置api请求地址前缀, String类型, 默认为空字符串,即请求和页面所在的服务器, 如果后台服务接口与页面部署的服务器不一样时,可以全局配置,也可以在实体配置prefix选项。

TIP

注意:配置pathPrefix的值必须要在 /src/config.js 中有定义,否则报错。

# methodTypeMap

数据请求类型对应http请求方法的映射, 默认值:

 {
    fetch: 'get',
    get: 'get',
    add: 'post',
    update: 'post',
    remove: 'get'
    batch: 'post'
  }
1
2
3
4
5
6
7
8

# methodSuffixMap

数据请求类型对应api地址的后缀映射, 默认值:

{
    fetch: '',
    get: '/:id',
    add: '',
    update: '',
    remove: '/:id',
    batch: '/remove'
}
1
2
3
4
5
6
7
8

# methodCommentMap

请求方法对应的中文注释

{
    fetch: '获取<%=cname%>列表',
    get: '获取<%=cname%>单条记录',
    add: '新增<%=cname%>',
    update: '更新<%=cname%>',
    remove: '删除<%=cname%>',
    batch: '批量删除<%=cname%>'
}
1
2
3
4
5
6
7
8

# batchEnabled

是否全局开启生成批量删除,默认值:true 这个设置是新增的,在老版本下缺少这个配置,就是false

# stateListName

store 或 mixin 状态保存列表的字段名称,和接口响应数据对应, 默认:list

# stateModelName

store 或 mixin 状态保存单个实体字段名称,默认:model

# statePageName

store 或 mixin 状态保当前页码的字段名称,和接口响应数据对应, 默认:page

# statePageSizeName

store 或 mixin 状态保存页大小的字段名称,和接口响应数据对应,默认:limit

# stateTotalName

store 或 mixin 状态保存数据总条数字段名称,和接口响应数据对应,默认:total

# mockDataName

Mock响应数据字段名称,默认:data

# mockCodeName

Mock响应状态字段名称,默认:code

# mockMsgName

Mock响应信息说明字段名称,默认:msg

# successCodeValue

Mock响应成功时的code值,默认:0

# 模型配置

一个标准的模型配置文件如下,建议相同的实体操作都在一个文件里配置。

module.exports = {
  vuex: false,             // 是否生成store,如果设置false,不生成store,只会生成mixin, 默认值false
  name: '用户信息',        // 模型名称,用来生成注释文档
  model: {
    path: '/api/users',   // 接口地址路径,必须
    title: '',            // 接口说明,用来生成注释文档
    prefix: '',           // 接口地址前缀,可选,默认为空, 名称必须要在 src/config.js中定义,否则报错
    methods: ['fetch', 'get', 'add', 'update', 'remove', 'batch'],  // 生成请求方法,默认全部,如需要自定义,设置为false
    transform: '',         // 响应数据转换函数,可选,函数名称必须要在 src/mapping/index.js 中定义,否则会报错
    options: {},          // ajax 参数选项,可选
    name:'',              // 自定义方法名称,methods为false,必须要设置
    columns: {},          // 模拟数据字段mock模板,可选
    template: '',         // Mock自定义模板函数名称, 名称必须在/src/mock/templates.js中能找到,设置了该值,columns将失效
    method: '',           // 自定义方法按那种方式生成store或mixin,可选, 在 methods为false时有效,可选值:fetch/get/add/update/remove
    state: '',             // 定义状态保存的名称,methods为false时并且method为空时,必须要设置state 和 name
    disabled: false      // 是否禁用该配置项, 设置为true,代码生成器将忽略该配置
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

TIP

上述配置,除了path是必须要填写之外,其他选项都是可选的, model支持 Array或Object类型配置

# 1、实现对一个实体进行增、删、改、查

module.exports = {
  vuex: true,
  model: {
    path: '/api/user'
  }
}
1
2
3
4
5
6

TIP

当请求响应的数据无组件或页面之间的共享需求时,可以使用mixin模式,把数据存储到组件内部data,而不是存储到store。启用mixin模式,只需设置vuex为false

# 2、实现对一个实体进行增、删、改、查,并且带模拟数据

module.exports = {
  vuex: true,
  model: {
    path: '/api/user',
    columns: {
      id: '@guid',
      name: '@cname'
    }
  }
}
1
2
3
4
5
6
7
8
9
10

# 3、只需要增、删、改、查中得某些操作,可以指定生成你需要的方法

module.exports = {
  vuex: true,
  model: {
    path: '/api/user',
    // 需要生成的方法,可以按需要设置
    methods: ['fetch', 'get', 'add', 'update', 'remove'],
    columns: {
      id: '@guid',
      name: '@cname'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 4、自定义方法配置

当有接口不合适归类为通用的增、删、改、查 或 接口响应数据格式较特别,可以使用自定义方法配置,如:用户登录

module.exports = {
  vuex: true,
  model: {
    path: '/api/user/login',
    methods: false,
    name: 'login',
    state: 'loginInfo',
    columns: {
      id: '@guid',
      name: '@cname',
      loginTime: '@date'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上述配置表示调用 login 方法 请求接口返回模拟数据,并存储到store对应实体对象的loginInfo属性里

TIP

methods为false时,必须要设置name

TIP

vuex模式,methods为false,并且method为空时,必须要设置state

TIP

method的值必须是fetch get add update remove batch 中的一个

# 5、同一个实体模型配置多个方法

model支持Array的配置项, 以下示例配置了user的增、删、改、查,登录、注销接口

module.exports = {
  vuex: true,
  model: [
    {
      path: '/api/users',
      columns: {
        id: '@guid',
        name: '@cname'
      }
    },
    {
      path: '/api/user/login',
      methods: false,
      state: 'loginInfo',
      name: 'login',
      columns: {
        id: '@guid',
        name: '@cname',
        loginTime: '@date'
      }
    },
    {
      path: '/api/user/logout',
      methods: false,
      state: 'logoutInfo',
      name: 'logout',
      columns: {
        logoutTime: '@date'
      }
    }
  ]
}

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

# 6、自定义Ajax参数选项

当api要求特殊,不能通过全局配置来解决时,可以通过配置 options 对 ajax的参数进行设置, 如:

module.exports = {
  vuex: true,
  model: {
    path: '/api/user/login',
    options: {
      method: 'post',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 7、自定义模拟数据模板

当需要模拟的数据不能通过定义列columns来满足需求时,可以自定义template, 如模拟一个树结构,需要编写一个模板函数,模板函数必须要在 /src/mock/templates.js 中定义,示范代码如下:

/**
 * 模拟一个树结构
 * @param Mock Mock实例
 * @param url 请求的url
 * @param query 查询参数对象
 * @returns {Object}
 */
export const createTree = function (Mock, url, query) {
  let root = Mock.mock({
    'list|30': [{
      'id|+1': 1,
      'label': '@ctitle',
      'parentId': null
    }]
  })
  let data = Mock.mock({
    'list|100': [{
      'id|+1': 31,
      'label': '@ctitle',
      'parentId': function () {
        return Math.floor(Math.random() * 50)
      }
    }]
  })
  return {code: 0, data: {list: root.list.concat(data.list)}, message: ''}
}
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

代码生成器配置

module.exports = {
  vuex: true,
  model: [{
    path: '/api/menus/tree',
    methods: false,
    name: 'getTree',
    state: 'tree',
    template: 'createTree' // 调用模板生成模拟数据
  }]
}

1
2
3
4
5
6
7
8
9
10
11

# 8、自定义数据转换函数

当需要后端接口响应的数据进行转换时,可以通过配置转换函数来处理。转换函数必须要在 /src/mapping/index.js 中定义

/**
 * 数据转换函数示例
 * @param json 原始数据,即借口响应的json
 * @param method 接口调用方法名称
 * @param data 请求发送的数据对象
 * @param params 请求的url参数对象
 * @returns {*}
 */
export function dataMap (json, method, data, params) {
  // 对原始数据进行修改后,return 出去
  return json
}

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

代码生成器配置

module.exports = {
  vuex: true,
  model: [{
    path: '/api/menus/tree',
    methods: false,
    name: 'getTree',
    state: 'tree',
    transform: 'dataMap' // 调用数据转换函数
  }]
}

1
2
3
4
5
6
7
8
9
10
11

# 9、指定请求接口地址前缀

请求接口地址前缀默认取全局配置的 pathPrefix ,如果有特殊接口,前缀与全局配置的不同时,可以在文件 /src/config.js 新增配置,然后在配置代码生成器。

module.exports = {
  vuex: true,
  model: {
    path: '/api/user',
    prefix: 'OTHER_PATH', // 设置与全局配置不同的前缀
    columns: {
      id: '@guid',
      name: '@cname'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

# 调用方式

代码生成器配置支持vuex 和 mixin 两种模式,以下分别对两种模式进行调用说明。

# vuex 模式调用

如代码生成器架构配置,新建文件 /schemas/user.js, 文件内容如下:

module.exports = {
  vuex: true,
  model: {
    path: '/api/user',
    columns: {
      id: '@guid',
      name: '@cname'
    }
  }
}
1
2
3
4
5
6
7
8
9
10

上述配置,生成代码后,创建了文件 /.my/code/store/user.js, 可以在vue组件内import需要调用的方法,并绑定到模板,如:

<template>
  <div>
      <p v-for="item in user.list"> {{item.name} } </p>
  </div>
</template>
1
2
3
4
5
import user from '$my/code/store/user'
export default {
  mixins: [user],
  created() {
    // 触发获取列表的 action
    this.fetchUser({
         data: {
            page: 1,
            limit: 10
          }
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# mixin 模式调用

如代码生成器架构配置,新建文件 /coder/schemas/user.js, 文件内容如下:

module.exports = {
  vuex: false,  // 启用mixin模式
  model: {
    path: '/api/user',
    columns: {
      id: '@guid',
      name: '@cname'
    }
  }
}
1
2
3
4
5
6
7
8
9
10

上述配置,生成代码后,创建了文件 /.my/code/mixin/user.js, 可以在vue组件内mixins方式继承 user 的方法获取数据,并绑定到模板,如:

<template>
  <div>
      <p v-for="item in user.list"> {{item.name} } </p>
  </div>
</template>

1
2
3
4
5
6
import user from '$my/code/mixin/user'
export default {
  mixins: [user], // 混合集成 user
  created() {
    // 调用获取列表方法
    this.fetchUser({
         data: {
            page: 1,
            limit: 10
          }
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 常见问题

# 1、POST数据时,后端接口接收不到前端传递的参数。

前端框架在默认情况下,POST数据是以 application/json 的方式传输JSON字符串。如果后端对传递的数据不是反序列化JSON的,需要对接收参数的代码进行改造。

如果后端接口有某些原因不能修改,可以在前端发送的请求头做处理。

代码生成器的配置以表单的形式提交数据实例:

module.exports = {
  vuex: true,
  model: {
    path: '/api/user/login',
    options: {
      method: 'post',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

TIP

表单提交二进制流用 'Content-type': 'multipart/form-data'

# 2、请求接口跨域报错

前后端分离开发模式,由于前后端可以进行单独部署,请求跨域问题很常见,解决方法主要有两种方式:

  1. 采用nginx代理
  2. 接口响应头开启Cross (推荐)

响应头需要做以下配置, 同时后端需要开放OPTIONS类型的请求,跨域带cookie的请求时,浏览器先试探性发送OPTIONS请求,成功后才发起真正的请求

 Access-Control-Allow-Credentials:true
 Access-Control-Allow-Origin:请求的host
1
2

如:

 Access-Control-Allow-Credentials:true
 Access-Control-Allow-Origin:http://www.fbknav.cn:98
1
2

TIP

建议后端代码不要写死Access-Control-Allow-Origin,可以从请求头中动态获取 Origin

如果接口不需跨域带cookie验证信息,可以简单设置

 Access-Control-Allow-Origin: *
1