Babel 在提升前端效率的實踐

王志暢發表於2019-05-20

大綱

  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功能

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

參考

相關文章