自動化schema的研究

當前的一些問題

一直以來,中臺開發提效是咱們努力的方向。 最近看到有個分享利用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('.');
相關文章
相關標籤/搜索