Babel 在提升前端效率的实践

4,703 阅读10分钟

大纲

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

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

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

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

查看基础支持的功能点API

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

所以,我能不能通过这些json配置通过某种工具生成对应的代码?这样一来以上提到的问题就完全不存在了,因为这和我们自己写的代码完全一样,工具只是帮我们完成初始化的过程。所以后来想了很多办法,最初采用template string的方式,这种方式较为简单粗暴,无非通过string中嵌套变量的判断来输出code。但是在实际写的时候发现很多问题,比如

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

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

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

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

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

什么是babel?

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

  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/async-validator#rules
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的值,则开启简单/复杂筛选切换

参考