# 代码生成器
# 概述
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'
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TIP
新建实体配置文件必须要在/schemas/目录下,可以建目录。
2、运行命令 npm run coder
或 npm run dev
生成代码,代码文件目录在 /.my/code/
, 目录结构如下:
# 约定
代码生成器生成的代码需要与后端接口有预先的约定
# 请求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
}
}
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:系统内部错误(非业务代码里显式抛出的异常,例如由于数据不正确导致空指针异常、数据库异常等等)
2
3
4
# 请求跨域问题解决方案
前后端分离开发模式,由于前后端可以进行单独部署,请求跨域问题很常见,解决方法主要有两种方式:
- 采用nginx代理
- 接口响应头开启Cross (推荐)
响应头需要做以下配置, 同时后端需要开放OPTIONS类型的请求,跨域带cookie的请求时,浏览器先试探性发送OPTIONS请求,成功后才发起真正的请求
Access-Control-Allow-Credentials:true
Access-Control-Allow-Origin:请求的host
2
如:
Access-Control-Allow-Credentials:true
Access-Control-Allow-Origin:http://www.fbknav.cn:98
2
TIP
建议后端代码不要写死Access-Control-Allow-Origin,可以从请求头中动态获取 Origin
如果接口不需跨域带cookie验证信息,可以简单设置
Access-Control-Allow-Origin: *
# 全局配置
当后端提供的接口与约定的规范不一致的时候,可以通过配置进行相应的调整做适配,但建议还是按前端建议的约定规范。
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'
}
2
3
4
5
6
7
8
# methodSuffixMap
数据请求类型对应api地址的后缀映射, 默认值:
{
fetch: '',
get: '/:id',
add: '',
update: '',
remove: '/:id',
batch: '/remove'
}
2
3
4
5
6
7
8
# methodCommentMap
请求方法对应的中文注释
{
fetch: '获取<%=cname%>列表',
get: '获取<%=cname%>单条记录',
add: '新增<%=cname%>',
update: '更新<%=cname%>',
remove: '删除<%=cname%>',
batch: '批量删除<%=cname%>'
}
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,代码生成器将忽略该配置
}
}
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'
}
}
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'
}
}
}
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'
}
}
}
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'
}
}
}
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'
}
}
]
}
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'
}
}
}
}
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: ''}
}
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' // 调用模板生成模拟数据
}]
}
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
}
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' // 调用数据转换函数
}]
}
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'
}
}
}
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'
}
}
}
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>
2
3
4
5
import user from '$my/code/store/user'
export default {
mixins: [user],
created() {
// 触发获取列表的 action
this.fetchUser({
data: {
page: 1,
limit: 10
}
})
}
}
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'
}
}
}
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>
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
}
})
}
}
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'
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
TIP
表单提交二进制流用 'Content-type': 'multipart/form-data'
# 2、请求接口跨域报错
前后端分离开发模式,由于前后端可以进行单独部署,请求跨域问题很常见,解决方法主要有两种方式:
- 采用nginx代理
- 接口响应头开启Cross (推荐)
响应头需要做以下配置, 同时后端需要开放OPTIONS类型的请求,跨域带cookie的请求时,浏览器先试探性发送OPTIONS请求,成功后才发起真正的请求
Access-Control-Allow-Credentials:true
Access-Control-Allow-Origin:请求的host
2
如:
Access-Control-Allow-Credentials:true
Access-Control-Allow-Origin:http://www.fbknav.cn:98
2
TIP
建议后端代码不要写死Access-Control-Allow-Origin,可以从请求头中动态获取 Origin
如果接口不需跨域带cookie验证信息,可以简单设置
Access-Control-Allow-Origin: *