当前的一些问题
一直以来,中台开发提效是我们努力的方向。 最近看到有个分享利用babel插件来实现文本提取。既然可以用来进行文本提取,那是不是也可以用来进行配置点提取呢。
目前手写schema是开发遇到的一个痛点问题,至少在我看来是一个问题。在不参考示例schema的情况下,开发过程手写schema有一定的难度(除了标准schema的规范比较多。开发者脑海中需要清晰这份schema渲染出来的表单)在写业务逻辑的同时,还要去编写schema. 又引入了schema正确性的调试等工作。
我认为理想的情况应该是,开发者在编写组件时对scema这件事无感知,只需要遵循少量的规范来开发组件,按照开发一般组件的思路开发即可。
改变一下思路
想象下我们开发组件时代码是这样的:
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库提供了一系列的方法来帮助我们编写带配置功能业务代码。每个方法的使用高阶函数,入参为配置项名,返回一个渲染方法,开发者自己去实现。 比如我的组件需要一个标题由外部配置进来。那么我可以这样写:
<div>
<h1>{R.getString('标题')()}</h1>
</div>
或者
<div>
{R.getString('标题')(v => <h1>{v}</h1>)}
</div>
这样我们就完成了一个带配置功能的组件的编写。 编写完成后,我们使用babel插件 babel-plugin-schema
来提取配置项,生成我们要的schema.json文件。 上述代码运行后,生成的schema.json如下:
{
"标题":{
"type":"string",
"title":"标题"
}
}
整个开发流程如下:
用户借助R开发组件 --> 编译时使用工具 --> 组件提交发布
其中编译阶段集成到脚手架,用户无感知。可以认为只一个侵入,就是使用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('.');