babel在提高前端效率的实践

大纲

  1. 遇到的问题场景及解决方案对比
  2. 什么是babel?
  3. 解决过程
  4. 目前遗留的问题
  5. 目前实现功能API
  6. 参考

遇到的问题场景及解决方案对比

咱们目前采用的是antd + react(umi)的框架作业务开发。在业务开发过程当中会有较多频繁出现而且类似度很高的场景,好比基于一个table的基础的增删改查,这个相信你们都很是熟悉。在接到一个新的业务需求的时候,相信有很多人会选择copy一份功能相似的代码而后基于这份代码去改造以知足当前业务,固然我目前也是这样作的~html

其实想把这块功能提取成一个公共组建的想法由来已久,最近开始作基础组件,便拿这个下手了。通过一周左右的时间完成了基础组件的编写。react

查看基础支持的 功能点API

基本的思路是经过json生成一些抽象配置,而后经过解析json的抽象配置+渲染器最终生成页面。json配置涵盖了目前80%的业务场景的基本需求,可是可扩展性很低。好比一些复杂的业务场景:表单的关联校验数据关联显示多级列表下钻等等功能。虽然经过一些较为复杂的处理能够把这些功能融入进来,但最终组件将会异常庞大难以维护。git

因此,我能不能经过这些json配置经过某种工具生成对应的代码?这样一来以上提到的问题就彻底不存在了,由于这和咱们本身写的代码彻底同样,工具只是帮咱们完成初始化的过程。因此后来想了不少办法,最初采用template string的方式,这种方式较为简单粗暴,无非经过string中嵌套变量的判断来输出code。可是在实际写的时候发现不少问题,好比github

  1. function的输出(JSON.stringify会将function忽略)
  2. 多层函数嵌套以后怎么获取最终渲染的节点code
  3. 嵌入变量怎么实现、umi-models-effects/reducer中额外的字典查询怎么生成等等..

最终学习了一些生成代码的工具好比angular-cli以及一些关于js生成代码的文章,主要是经过知乎上的这篇讨论了解到了你们是怎么处理这种问题的。最终决定采用babel的生态链来解决上述遇到的问题。express

咱们目前采用的方式是基于antd+react(umi)编写通用的CRUD模板,而后经过代码生成器解析json中的配置生成对应的代码,大体的流程是:json

React --> JavaScript AST ---> Code Generator --> Compiler --> Page后端

目前功能只是完成了初步版本,待应用在项目中使用一段时间稳定以后将会开源~

什么是babel?

Babel是一个工具链,主要用于编译ECMAScript 2015+代码转换为向后兼容的可运行在各类浏览器上的JavaScript。主要功能:api

  1. 语法转换
  2. 环境中缺乏的Polyfill功能
  3. 源代码转换
  4. 查看更多Babel功能


Understanding ASTs by Building Your Own Babel Plugin数组

如上提供了babel基本的流程及一篇介绍AST的文章。浏览器

个人理解中好比一段string类型code,首先经过babel.transform会将code转为一个包含AST(Abstract Syntax Tree)的Object,一样可使用@babel/generator将AST转为code完成逆向过程。
例如一段变量声明代码:

const a = 1;

在解析以后的结构为:

{
  "type": "Program",
  "start": 0,
  "end": 191,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 179,
      "end": 191,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 185,
          "end": 190,
          "id": {
            "type": "Identifier",
            "start": 185,
            "end": 186,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 189,
            "end": 190,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

首先类型为VariableDeclaration,首先他的类型是const,能够经过点击查看api其它还有letvar的值。其次是声明declarations部分,这里值为数组,由于咱们能够同时定义多个变量。数组中值的类型为VariableDeclarator,包含idinit两个参数,分别为变量名称以及变量值。id的类型为Identifier,译为修饰符便是变量名称。init类型为Literal,便是常量,通常经常使用的有stringLiteralnumericliteralbooleanliteral等。此时即完成了变量赋值的过程。

固然这只是很简单的语法转换,若是你们想学习更多关于转换及类型的知识,可参考以下两个官方连接:

解决过程

首先定义目录结构:

.
├── genCode // 代码生成器
|   ├── genDetail          // 须要新页面打开时单独的detail目录
|   └── genIndex           // 首页
|   └── genModels          // umi models
|   └── genServices        // umi services
|   └── genTableFilter     // table筛选区域
|   └── genTableForm       // 非新页面模式,新增/更新模态框
|   └── genUpsert          // 新页面模式下,新增/更新页面
|   └── genUtils           // 生成工具类
├── schema                 // 模型定义文件
|   ├── table              // 当前要生成的模型
|   └── ├──config.js       // 基础配置
|   └── └──dataSchema.js   // 列表、新增、更新配置
|   └── └──querySchema.js  // 筛选项配置
├── scripts                // 生成脚本
|   ├── generateCode.js    // 生成主文件
|   └── index.js           // 入口
|   └── utils.js           // 工具类
├── toCopyFiles            // 生成时须要拷贝的文件,好比less
└── index.js               // 主入口

主体流程为:

  1. 指定要生成代码的路径。
  2. 根据schema中当前json配置路径,依次调用genCode目录中各个模块的代码生成方法获取对应code。
  3. 在指定的路径下写入对应的文件。
  4. 执行eslint ${filePath} --fix格式化生成的代码。
  5. 根据配置对应复制toCopyFiles文件夹中依赖的less等文件到对应的文件夹。

其中主要模块为genCode文件夹中根据json配置生成代码的过程。
以genModels为例,首先提取可使用template string完成的部分,减小代码解析的工做量。

module.exports = (tableConfig) => {
  return `
        import { message } from 'antd';
        import { routerRedux } from 'dva/router'
        import { parse } from 'qs'
        ${dynamicImport(dicArray, namespace)}

        export default {
            namespace: '${namespace}',
            state: {
                ...
            },
            effects: {
                *fetch({ payload }, { call, put }) {
                    const response = yield call(queryData, payload);
                    if (response && response.errorCode === 0) {
                        yield put({
                            type: 'save',
                            payload: response.data,
                        });
                    } else {
                        message.error(response && response.errorMessage || '请求失败')
                    }
                },
                ...,
                ${dynamicYieldFunction(dicArray)}
            },

            reducers: {
                save(state, action) {
                    return {
                        ...state,
                        data: action.payload,
                    };
                },
                ...,
                ${dynamicReducerFunction(dicArray)}
            },
        };
    `
}

由于列表数据可能有字典项从后台获取值来对应显示,因此importeffectsreducers模块均有需根据配置动态生成的代码。
以dynamecImport为例:

function dynamicImport (dicArray, namespace) {
    // 基础api import
    let baseImport = [
      'queryData', 'removeData', 'addData', 'updateData', 'findById'
    ]
    // 判断json数据中是否有需从后台加载项
    if (dicArray && dicArray.length) {
      baseImport = baseImport.concat(dicArray.map(key => getInjectVariableKey(key)))
    }
    // 遍历生成依赖项
    const _importDeclarationArray = map(specifier => (
      _importDeclarationArray.push(t.importSpecifier(t.identifier(specifier), t.identifier(specifier)))
    ))
    // 定义importDeclaration
    const ast = t.importDeclaration(
      _importDeclarationArray,
      t.stringLiteral(`../services/${namespace}`)
    )
    // 经过@babel/generator 将ast生成code
    const { code } = generate(ast)

    return code
  }

其它代码生成逻辑相似,有不肯定如何生成的部分可参考上方提供的连接完成代码转换再去生成。

如有经过babel转换没法生成的代码,可经过正则来完成。

例如如下umi-models代码:

*__dicData({ payload }, { call, put }) {
      const response = yield call(__dicData, payload);
      if (response && response.errorCode === 0) {
        yield put({
          type: 'updateDic',
          payload: response.data,
        });
      } else {
        message.error(response && response.errorMessage || '请求失败')
      }
    }

基础代码可经过yieldExpression生成,可是转换以后无function以后的*符号,反复差了文档以后没有解决办法,最后只能将生成完的code利用正则替换来解决。
若是你们有遇到相似的问题欢迎讨论~

问题

  1. 目前使用的编辑器组件为braft-editor,可是结合antd使用initialValue不生效,必须使用setFieldsValue。可是使用useEffects时会默认添加props.form做为依赖而且props.form会不断变化而触发死循环,目前无奈只有禁用eslint react-hooks/exhaustive-deps。
useEffect(() => {
    props.form.setFieldsValue({
      editorArea: BraftEditor.createEditorState(current.editorArea),
      editorArea2: BraftEditor.createEditorState(current.editorArea2)
    });
  }, [current.editorArea, current.editorArea2]);
  1. 生成的代码怎么删除未使用的依赖?使用eslint --fix不会删除未使用的变量定义。
  2. 初始化以后的代码要修改怎么办?因当前方法只会完成代码初始化过程,之后修改的过程暂无思路解决。

功能API

参数规范参考react-antd-admin
功能配置包含三个基础配置文件:

config.json

配置列表

参数 必填 类型 默认值 说明
namespace true string null 命名空间
showExport false boolean true 是否显示导出
showCreate false boolean true 是否显示建立
showDetail false boolean true 是否显示查看
showUpdate false boolean true 是否显示修改
showDelete false boolean true 是否显示删除
newRouterMode false boolean false 在新的页面新增/编辑/查看详情。若包含富文本编辑器,建议此值设为true,富文本在模态框展现不是很是美观。
showBatchDelete false boolean true 是否显示批量删除,需multiSelection为 true
multiSelection false boolean true 是否支持多选
defaultDateFormat false string 'YYYY-MM-DD' 日期格式
upload false object null 上传相关配置,上传图片和上传普通文件分别配置。 详见下方upload属性
pagination false object null 分页相关配置, 详见下方pagination属性
dictionary false array null 须要请求的字典项,用于下拉框或treeSelect的值为从后端获取的状况,可在dataSchema 和querySchema中使用, 详见下方dictionary属性

upload

参数 必填 类型 默认值 说明
uploadUrl false string null 默认的上传接口.优先级image/fileApiUrl > uploadUrl > Global.apiPath
imageApiUrl false string null 默认的图片上传接口
fileApiUrl false string null 默认的文件上传接口
image false string '/uploadImage' 默认的上传图片接口
imageSizeLimit false number 1500 默认的图片大小限制, 单位KB
file false string '/uploadFile' 默认的上传文件接口
fileSizeLimit false number 10240 默认的文件大小限制, 单位KB

pagination

参数 必填 类型 默认值 说明
pageSize false number 10 每页显示数量
showSizeChanger false boolean false 是否能够改变pageSize
pageSizeOptions false array ['10', '20', '50', '100'] 指定每页能够显示多少条
showQuickJumper false boolean false 是否能够快速跳转至某页
showTotal false boolean true 是否显示总数

dictionary

参数 必填 类型 默认值 说明
key true string null 变量标识
url true string null 请求数据地址

dataSchema.json

配置列表

参数 必填 类型 默认值 说明
key true string null 惟一标识符
title true string null 显示名称
primary false boolean false 主键 若是不指定主键, 不能update/delete, 但能够insert;
若是指定了主键, insert/update时不能填写主键的值;
showType false string input 显示类型
input/textarea/inputNumber/datePicker/rangePicker/radio/select/checkbox/multiSelect/image/file/cascader/editor
disabled false boolean false 表单中这一列是否禁止编辑
addonBefore false string/ReactNode null showType 为input能够设置前标签
addonAfter false string/ReactNode null showType 为input能够设置后标签
placeholder false string null 默认提示文字
format false string null 日期类型的格式
showInTable false boolean true 这一列是否要在table中展现
showInForm false boolean true 是否在新增或编辑的表单中显示
validator false boolean null 设置校验规则, 参考https://github.com/yiminghe/a...
width false string/number null 列宽度
options false array null format:[{ key: '', value: '' }]或string。showType为cascader时,此字段暂不支持Array,数据只能经过异步获取。
min false number null 数字输入的最小值
max false number null 数字输入的最大值
accept false string null 上传文件格式限制
sizeLimit false number 20480 上传文件格式限制
url false string null 上传图片url。图片的上传接口, 能够针对每一个上传组件单独配置, 若是不单独配置就使用config.js中的默认值;若是这个url是http开头的, 就直接使用这个接口; 不然会根据config.js中的配置判断是否加上host
sorter false boolean false 是否排序
actions false array null 操做

actions

参数 必填 类型 默认值 说明
keys false array null 容许更新哪些字段, 若是不设置keys, 就容许更全部字段
name true string null 展现标题
type false string null update/delete/newLine/component

querySchema.json

配置列表

参数 必填 类型 默认值 说明
key true string null 惟一标识符
title true string null 显示名称
placeholder false string null 提示语
showType false string input 显示类型, 一些可枚举的字段, 好比type, 能够被显示为单选框或下拉框
input, 就是一个普通的输入框, 这时能够省略showType字段
目前可用的showType: input/inputNumber/datePicker/rangePicker/select/radio/checkbox/multiSelect/cascader
addonBefore false string/ReactNode null showType 为input能够设置前标签
addonAfter false string/ReactNode null showType 为input能够设置后标签
defaultValue false string/array/number null 多选的defaultValue是个数组
min false number null showType为 inputNumber 时可设置最小值
max false number null showType为 inputNumber 时可设置最大值
options false array null options的key要求必须是string, 不然会有warning
normal-format: [{"key": "", "value": ""}]
cascader-format: [{"value": "", "label": "", children: ["value": "", "label": "", children: []]}]
若是值为string,表明异步获取的数据,则获取当前命名空间下该key对应的值
defaultValueBegin false string null showType为 rangePicker 时可设置默认开始值
defaultValueEnd false string null showType为 rangePicker 时可设置默认结束值
placeholderBegin false string 开始日期 showType为 rangePicker 时可设置默认开始提示语
placeholderEnd false string 结束日期 showType为 rangePicker 时可设置默认结束提示语
format false string null 日期筛选格式
showInSimpleMode false boolean false 在简单查询方式下展现,若数据中有一项包含此字段且为true的值,则开启简单/复杂筛选切换

参考

相关文章
相关标签/搜索