Babel 在提高前端效率的實踐

大綱

  1. 遇到的問題場景及解決方案對比
  2. 什麼是babel?
  3. 解決過程
  4. 目前遺留的問題
  5. 目前實現功能API
  6. 參考

遇到的問題場景及解決方案對比

咱們目前採用的是antd + react(umi)的框架作業務開發。在業務開發過程當中會有較多頻繁出現而且類似度很高的場景,好比基於一個table的基礎的增刪改查,這個相信你們都很是熟悉。在接到一個新的業務需求的時候,相信有很多人會選擇copy一份功能相似的代碼而後基於這份代碼去改造以知足當前業務,固然我目前也是這樣作的~html

其實想把這塊功能提取成一個公共組建的想法由來已久,最近開始作基礎組件,便拿這個下手了。通過一週左右的時間完成了基礎組件的編寫。react

查看基礎支持的功能點APIgit

基本的思路是經過json生成一些抽象配置,而後經過解析json的抽象配置+渲染器最終生成頁面。json配置涵蓋了目前80%的業務場景的基本需求,可是可擴展性很低。好比一些複雜的業務場景:表單的關聯校驗數據關聯顯示多級列表下鑽等等功能。雖然經過一些較爲複雜的處理能夠把這些功能融入進來,但最終組件將會異常龐大難以維護。github

因此,我能不能經過這些json配置經過某種工具生成對應的代碼?這樣一來以上提到的問題就徹底不存在了,由於這和咱們本身寫的代碼徹底同樣,工具只是幫咱們完成初始化的過程。因此後來想了不少辦法,最初採用template string的方式,這種方式較爲簡單粗暴,無非經過string中嵌套變量的判斷來輸出code。可是在實際寫的時候發現不少問題,好比express

  1. function的輸出(JSON.stringify會將function忽略)
  2. 多層函數嵌套以後怎麼獲取最終渲染的節點code
  3. 嵌入變量怎麼實現、umi-models-effects/reducer中額外的字典查詢怎麼生成等等..

最終學習了一些生成代碼的工具好比angular-cli以及一些關於js生成代碼的文章,主要是經過知乎上的這篇討論瞭解到了你們是怎麼處理這種問題的。最終決定採用babel的生態鏈來解決上述遇到的問題。json

咱們目前採用的方式是基於antd+react(umi)編寫通用的CRUD模板,而後經過代碼生成器解析json中的配置生成對應的代碼,大體的流程是:後端

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

目前功能只是完成了初步版本,待應用在項目中使用一段時間穩定以後將會開源~數組

什麼是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的值,則開啓簡單/複雜篩選切換

參考

相關文章
相關標籤/搜索