自动化schema的研究

当前的一些问题

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

转载于:https://my.oschina.net/u/867090/blog/1574883

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值