一直以來,中臺開發提效是咱們努力的方向。 最近看到有個分享利用babel插件來實現文本提取。既然能夠用來進行文本提取,那是否是也能夠用來進行配置點提取呢。javascript
目前手寫schema是開發遇到的一個痛點問題,至少在我看來是一個問題。在不參考示例schema的狀況下,開發過程手寫schema有必定的難度(除了標準schema的規範比較多。開發者腦海中須要清晰這份schema渲染出來的表單)在寫業務邏輯的同時,還要去編寫schema. 又引入了schema正確性的調試等工做。java
我認爲理想的狀況應該是,開發者在編寫組件時對scema這件事無感知,只須要遵循少許的規範來開發組件,按照開發通常組件的思路開發便可。node
想象下咱們開發組件時代碼是這樣的:react
import React from 'react'; import R from 'R'; const {record, getSchema, getNumber } = R; R.getNumber('數字')(value => <h1>title</h1>) R.getBoolen('是否')(value => <h1>title</h1>) R.getString('標題')(value => <h1>title</h1>) getNumber('數字2')(value => <h1>{value}</h1>) R.getSingle('單選功能', ['a','b','c'])(selected => { return [ R.when(selected.a, <a/>), R.when(selected.b, <b/>), R.when(selected.c, <c/>) ] }) // checkbox R.getMultiple('多選功能', ['a','b','c'])(selected => { return [ R.when(selected.a, <a/>), R.when(selected.b, <b/>), R.when(selected.c, <c/>) ].filter(s => !!s) }) // 複雜對象 const Good = R.record(R.getScheam({a:1, b: true, c:'3'})); // 可變數組 R.getArray('集合', Good)(goods => goods.map(renderGood))
我引入了一個外部依賴庫:R (暫且叫這個名字) R庫提供了一系列的方法來幫助咱們編寫帶配置功能業務代碼。每一個方法的使用高階函數,入參爲配置項名,返回一個渲染方法,開發者本身去實現。 好比個人組件須要一個標題由外部配置進來。那麼我能夠這樣寫:json
<div> <h1>{R.getString('標題')()}</h1> </div>
或者api
<div> {R.getString('標題')(v => <h1>{v}</h1>)} </div>
這樣咱們就完成了一個帶配置功能的組件的編寫。 編寫完成後,咱們使用babel插件 babel-plugin-schema
來提取配置項,生成咱們要的schema.json文件。 上述代碼運行後,生成的schema.json以下:數組
{ "標題":{ "type":"string", "title":"標題" } }
整個開發流程以下:緩存
用戶藉助R開發組件 --> 編譯時使用工具 --> 組件提交發布babel
其中編譯階段集成到腳手架,用戶無感知。能夠認爲只一個侵入,就是使用R工具來開發配置業務。函數
回來再來回顧下組件的開發過程: 代碼 R.getNumber('數字')(value => <h1>title</h1>)
能夠被分爲2部分,
第一部分是配置部分getNumber('數字')
,
第2部分是渲染部分(value => <h1>title</h1>)
配置部分:
R提供瞭如下的api,來完成不一樣的配置:
- getNumber <input type="number"/> - getString <input /> - getBoolen <input type="radio"/> - getSingle <input type="radio"/> - getMultiple <input type="checkbox"/> - getSchema 用來生成複合對象 - (getDate? getRange? 待擴展)
用戶只須要關心我須要在代碼中哪些地方插入配置,以及我配置的數據類型(bool?number?). 不須要再關心其它細節。
首先babel內核將代碼拆分紅ast, 在進行轉換時。插件介入,經過對特定的ast節點進行提取,將用戶定義的配置提取並緩存,最終生成json schema. 此過程爲靜態解析,相比使用正則:好處是更加靈活和準確,能夠追溯變量的最終引用。也就儘量得減小開發時規範約束,用戶能夠隨意寫正確的js代碼。
R會解析React組件中的props,並經過用戶定義的配置,獲取對應的配置值,而後調用用戶定義的渲染方法渲染出最終的頁面。因此用戶定義的配置便可做爲編譯時生成schema的依據,也可做爲渲染時獲取值的途徑。一次定義,2次使用。
一個要遵循的規範是,R不能夠被別名引用
// 正確 import R from 'R'; R.getSchema(''); // 錯誤 import R from 'R'; const S = R; S.getSchema('');
R的方法不要被同名變量引用,如下寫法可能會解析出來錯誤的schema
import R from 'R'; let myfun = R.getNumber; myfun('數字')(); myfun = R.getString; myfun('字符串')();
難點在於靜態解析部分,提取用戶的配置,理論上看,用戶的代碼咱們均可以訪問到,只要咱們的解析程序夠全面,老是能夠提取到正確和完成的配置。上述提到的2個規範也就能夠忽略。可是爲了提升解析到效率和準確性,下降解析程序的複雜度,仍是經過一些規範約束開發者的代碼風格,同時,經過規範,也提高了代碼的可維護性。
下面是解析的代碼,能夠更完善:
const glob = require('glob'); const transformFileSync = require('babel-core').transformFileSync; const fs = require('fs'); const _ = require('lodash'); function run (path){ glob('./src.js', {},(err, files) => { files.forEach(fileName => { if (fileName.includes('node_modules')) { return; } transformFileSync(fileName, { presets: ['babel-preset-es2015', 'babel-preset-stage-0', 'babel-preset-react'].map(require.resolve), plugins: [ require.resolve('babel-plugin-transform-decorators-legacy'), scan, ] }); }) console.log(JSON.stringify(result, null, 2)); }) } let R = ''; const result = {}; // R下的變量 const variables = []; function isRcallee(path, t){ const type = _.get(path, 'node.callee.type'); if(type == 'Identifier'){ const name = isRmember(_.get(path, 'node.callee.name')); const args = path.node.arguments; if(name){ parse(name, args); } } else if(type == 'MemberExpression'){ if(_.get(path, 'node.callee.object.name') == R){ const methodMame = path.node.callee.property.name; const args = path.node.arguments; parse(methodMame, args); }; } } function parse(methodMame, args){ if(methodMame == 'getNumber'){ const itemName = args[0].value; result[itemName] = { type: 'number', title: itemName, } } if(methodMame == 'getString'){ const itemName = args[0].value; result[itemName] = { type: 'string', title: itemName, } } if(methodMame == 'getBoolen'){ const itemName = args[0].value; result[itemName] = { type: 'boolean', title: itemName, } } if(methodMame == 'getSingle'){ const itemName = args[0].value; const items = args[1].elements.map(e => e.value); result[itemName] = { type: 'string', title: itemName, enum: items, } } if(methodMame == 'getMultiple'){ const itemName = args[0].value; const items = args[1].elements.map(e => e.value); result[itemName] = { type: 'array', title: itemName, items:{ type: "string", enum: items, } } } } function parseIdentifier(){ } function parseVariable(path) { const init = _.get(path, 'node.init'); const id = _.get(path, 'node.id') if(init && init.object && init.object.name== "R"){ variables.push({ key: id.name, value: init.property.name, }) } } // 方法是不是R成員 function isRmember(funName){ const fn = variables.find(v => v.key == funName); if(fn) { return fn.value } return ''; } function scan({types: t}) { return { visitor:{ ImportDeclaration: (path)=>{ if(_.get(path, 'node.source.value') == 'R'){ R = _.get(path, 'node.specifiers[0].local.name') } }, VariableDeclarator: (path) => { parseVariable(path); }, CallExpression: (path) => { isRcallee(path, t) }, } } } run('.');