一直以来,中台开发提效是咱们努力的方向。 最近看到有个分享利用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('.');