低代码平台-路由生成

667 阅读4分钟

需求背景

以往在开发项目时,都是手动维护一张路由表,在低代码平台中,我们希望把这部分工作自动化,因此产生了路由自动生成这个需求。

路由表是一个数组,里面每项都是json对象,生成路由表就是对这个数组进行操作,产出一段路由表代码,最终写入前端页面的过程。

既然要产生代码,那数据库中存的也是代码片段,就不能用简单的JSON对象对其修改,需要用到Babel对代码进行操作,主要有三步:

解析代码(Parse)

使用@babel/parser将代码解析成AST(抽象语法树),语法树是由语法单元(注释、空白、字符串、括号、数字、标识符等)组成的数组,让计算机理解我们的代码

修改代码(Transform)

代码解析后用@babel/traverse遍历每个语法树节点,进行修改、删除、更新操作

生成代码(Generate)

修改后的语法树需要@babel/generator生成代码

假设有个空的路由表routes

const routes = [];

我们要插入如下路由

{
  "name": "home",
  "path": "/",
  "meta": {
    "title": "首页"
  }
}

然后通过name查找路由,并修改其meta属性中的title为“缺省页”

const routes = [
  {
    "name": "home",
    "path": "/",
    "meta": {
      "title": "缺省页"
    }
  }
];

最后将其删除。

本文将详细介绍路由的创建、修改、删除。

技术方案

技术选型

@babel/types:生成语法树节点

@babel/parser:解析代码片段

@babel/template:将代码片段转换成AST(抽象语法树)

@babel/traverse:对AST(抽象语法树)进行遍历

@babel/generator: 将AST(抽象语法树)转换成代码片段

主要流程

安装依赖

yarn install @babel/parser @babel/types @babel/traverse @babel/template @babel/generator

代码结构

const { parse } = require('@babel/parser');
const t = require('@babel/types');
const traverse = require('@babel/traverse').default;
const template = require('@babel/template').default;
const generate = require('@babel/generator').default;

const code = `
// 全局路由表
const routes = [];
`;

const action = '';  // 操作动作:add(更新)/update(修改)/remove(删除)
const name = '';  // 查找路由的name
const payload = '';  // 新增或修改时的值

const ast = parse(code, {
    sourceType: 'module',
});

// 生成路由
const genRoutesCode = ({ action, name, payload }) => {
        // 遍历器
	const visitor = {
            // 数组表达式节点
	    ArrayExpression(path) {},
            // 对象表达式节点
	    ObjectExpression(path) {}
	};

	// 对 ast 进行深度遍历
	traverse(ast, visitor);

	return generate(ast, {
	    jsescOption: { minimal: true }  // 防止生成代码时中文变成unicode
	}).code;
};

const result = genRoutesCode({
	action,
	name,
	payload
});

console.log(result);

首先看下我们的需求,插入路由要对routes数组进行操作,因此visitor遍历器中要对ArrayExpression类型节点进行遍历,而修改和删除都是对json对象的操作,所以visitor中定义了ObjectExpression。

这里的ArrayExpression和ObjectExpression是语法树中的节点类型,具体有哪些不用记,用的时候到astexplorer.net/ 中去看就可以了

插入路由

如上所述,插入路由需要对语法树中的ArrayExpression即数组表达式节点进行操作,因为我们新建的路由可能存在多级json结构,需要写一个buildAST方法,对每一级节点进行判断,如果当前的值类型为object时,则需递归调用,生成节点我们用了@babel/types这个工具。

// 数组表达式节点
ArrayExpression(path) {
  const { elements } = path.node;
  
  // 新增路由
  if (action === 'add') {
    // 构建语法树
    const buildAST = (source, tree) => {
      Object.entries(source).forEach(([key, val]) => {
        if (typeof val === 'string') {
          tree.push(t.objectProperty(t.identifier(key), t.stringLiteral(val)));
        }
        
        if (typeof val === 'number') {
          tree.push(t.objectProperty(t.identifier(key), t.NumericLiteral(val)));
        }
        
        if (typeof val === 'object') {
          const child = buildAST(val, []);
          
          tree.push(t.objectProperty(t.identifier(key), child));
        }
      });
      
      return t.objectExpression(tree);
    };
    
    const ast = buildAST(payload, [t.objectProperty(t.identifier('component'), t.identifier('window.sam.PageRender'))]);
    
    elements.push(routeAST);
}

可以看到,每一种类型的值都需要进行判断并生成节点操作,小范围的代码还可以接受,如果要插入一大段代码会很麻烦,好在babel提供了@babel/template解决这个问题,template可以构造不同类型的代码(statement声明/expression表达式/program程序),我们的json可以用expression方法构造,将我们的代码修改下

// 数组表达式节点
ArrayExpression(path) {
  const { elements } = path.node;
  
  // 新增路由
  if (action === 'add') {
    const routeBase = generate(template.expression(`DATA`)({
      DATA: JSON.stringify(payload)
    })).code;
    
    const routeBuild = template.expression(`{BASE,COMP}`);
    
    const routeAST = routeBuild({
      COMP: '"component":window.PageRender',
      BASE: routeBase.replace(/^{|}$/g, ''),
    });
    
    elements.push(routeAST);
  }
}

修改路由

首先我们要通过name在节点树中找到路由对应的节点,然后对其值进行修改。

// 对象表达式节点
ObjectExpression(path) {
    let route;

    // 查找路由
    path.node.properties.forEach(prop => {
        const propKey = prop.key.name || prop.key.value;
        
        if (propKey === 'name' && prop.value.value === name) {
            route = path;
        }
    });

    // 更新路由
    if (action === 'update' && route) {
        const modifer = (key, val, prop) => {
            const propKey = prop.key.name || prop.key.value;

            // 设置节点值
            if ((propKey === key) && typeof val !== 'object') {
                prop.value.value = val;
            }

            if ((propKey === key) && typeof val === 'object') {
                prop.value.properties.forEach(child => {
                    const childKey = child.key.name || child.key.value;

                    modifer(childKey, val[childKey], child);
                });
            }
        };

        // 遍历路由节点属性
        route.node.properties.forEach(prop => {
            Object.entries(payload).forEach(([key, val]) => {
                modifer(key, val, prop);
            });
        });
    }
}

删除节点

同上找到节点后,调用path上的remove方法将节点移除。

// 对象表达式节点
ObjectExpression(path) {
    let route;

    // 查找路由
    path.node.properties.forEach(prop => {
        const propKey = prop.key.name || prop.key.value;
        
        if (propKey === 'name' && prop.value.value === name) {
            route = path;
        }
    });

    // 删除路由
    if (action === 'remove' && route) {
        route.remove();
    }
}

完整示例

const { parse } = require('@babel/parser');
const t = require('@babel/types');
const traverse = require('@babel/traverse').default;
const template = require('@babel/template').default;
const generate = require('@babel/generator').default;

const code = `
// 全局路由表
window.sam.routes = [{
  name: 'home',
  path: '/',
  meta: {
    title: '首页'
  },
  component: window.sam.PageRender
}, {
  name: 'index',
  path: '/',
  meta: {
    title: '首页index'
  },
  component: window.sam.PageRender
}];
`;

const ast = parse(code, {
    sourceType: 'module',
});

/* const action = 'add'
const name = 'home';
const payload = {
  name: 'detail',
  path: '/detail',
  meta: {
    title: '详情页'
  }
}; */

/* const action = 'remove';
const name = 'index';
const payload = {}; */

const action = 'update';
const name = 'home';
const payload = {
    name: 'default',
    path: '/default',
    meta: {
        title: '缺省页'
    }
};

const transformRoutesAST = ({ action, name, payload }) => {
    const visitor = {
        ArrayExpression(path) {
            const { elements } = path.node;

            // 新增路由
            if (action === 'add') {
                /* const buildAST = (source, tree) => {
        Object.entries(source).forEach(([key, val]) => {
            if (typeof val === 'string') {
              tree.push(t.objectProperty(t.identifier(key), t.stringLiteral(val)));
            }
  
            if (typeof val === 'number') {
              tree.push(t.objectProperty(t.identifier(key), t.NumericLiteral(val)));
            }
  
            if (typeof val === 'object') {
              const child = buildAST(val, []);
  
              tree.push(t.objectProperty(t.identifier(key), child));
            }
          });
  
          return t.objectExpression(tree);
        };
        
        const ast = buildAST(payload, [t.objectProperty(t.identifier('component'), t.identifier('window.sam.PageRender'))]);
        */
                const routeBase = generate(template.expression(`DATA`)({
                    DATA: JSON.stringify(payload)
                })).code;

                const routeBuild = template.expression(`{BASE,COMP}`);

                const routeAST = routeBuild({
                    COMP: '"component":window.sam.PageRender',
                    BASE: routeBase.replace(/^{|}$/g, ''),
                });

                elements.push(routeAST);
            }
        },
        ObjectExpression(path) {
            let route;

            // 查找路由
            path.node.properties.forEach(prop => {
                const propKey = prop.key.name || prop.key.value;
                
                if (propKey === 'name' && prop.value.value === name) {
                    route = path;
                }
            });

            // 删除路由
            if (action === 'remove' && route) {
                route.remove();
            }

            // 更新路由
            if (action === 'update' && route) {
                const modifer = (key, val, prop) => {
                    const propKey = prop.key.name || prop.key.value;

                    // 设置节点值
                    if ((propKey === key) && typeof val !== 'object') {
                        prop.value.value = val;
                    }

                    if ((propKey === key) && typeof val === 'object') {
                        prop.value.properties.forEach(child => {
                            const childKey = child.key.name || child.key.value;

                            modifer(childKey, val[childKey], child);
                        });
                    }
                };

                // 遍历路由节点属性
                route.node.properties.forEach(prop => {
                    Object.entries(payload).forEach(([key, val]) => {
                        modifer(key, val, prop);
                    });
                });
            }
        }
    };

    // 对 ast 进行深度遍历
    traverse(ast, visitor);

    return generate(ast, {
        jsescOption: { minimal: true }
    }).code;
};

const result = transformRoutesAST({
    action,
    name,
    payload
});

console.log(result);