babel在提升前端效率的实践

4次阅读

共计 8642 个字符,预计需要花费 22 分钟才能阅读完成。

大纲

  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 等。此时即完成了变量赋值的过程。

当然这只是很简单的语法转换,如果大家想学习更多关于转换及类型的知识,可参考如下两个官方链接:

  • babel-types
  • ast 转换工具

解决过程

首先定义目录结构:

.
├── 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配置基本属性
  • dataSchema.json配置列表及新增修改字段
  • querySchema.json配置筛选区域字段

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

参考

  • react-antd-admin
  • AST 语法转换器
  • babel-types-api
正文完
 0