携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情
事件篇
有段时间我在写一个vue项目,每个新建的页面包含一些固定的组件(比如导航栏等),设定一些固定的属性方法(比如从路由对象的meta中取页面标题)以及引用一些全局的对象等,每次都要进行复制粘贴,虽然不麻烦但是也挺繁琐,因此我打算写个程序直接新建页面的时候把它们一并都处理好,程序也比较简单,只要写好模板用nodejs的fs模块做文件处理就好了:
const PAGE_TEMPLATE = `
<template>
<div id="page">
<ss-head-navbar :title="title"></ss-head-navbar>
</div>
</template>
<script>
import app from "_u/app.js";
import NavBar from "_c/head-navbar/head-navbar";
export default {
name:"user-info",
components: {
"ss-head-navbar": NavBar
},
beforeMount() {
this.title = this.$route.meta.title;
},
data() {
return {
title: ""
}
}
}
</script>`
var fs = require("fs");
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
})
readline.question(`请输入页面文件路径(如:userInfo/userInfo.vue):`, path => {
var pathArray = path.split("/");
// 获取文件名和目录名
var fileName = pathArray[pathArray.length - 1];
var directoryName = path.replace(fileName, "");
// 获取扩展名
var extension = fileName.match(/\..*$/)?.[0];
if (!extension) {
// 如果没有后缀则加上后缀
path += ".vue"
}
else if (extension != ".vue") {
// 如果后缀名不是vue
console.warn("后缀名不是.vue,替换成.vue")
path.replace(/\..*$/, ".vue")
}
fs.writeFile(`src/pages/${path}`, PAGE_TEMPLATE, (err) => {
if (err) {
fs.mkdir(`src/pages/${directoryName}`, (err) => {
if (err) throw err
fs.writeFile(`src/pages/${path}`, PAGE_TEMPLATE, (err) => {
if (err) throw err;
})
})
};
console.log('文件已创建');
});
readline.close()
})
既然做到了这一步,那么是否可以继续进一步,将新建的页面写到路由里呢?
疑惑篇
这个项目用的是vue-router,也就是说路由实际上就是传入了一个类似这样的对象:
let routes = [
{
path: '/promotion/goods/list',
name: 'promotion_goods',
component: () => import('@/pages/promotion/goods/list.vue'),
meta: {
title: "消费列表页"
}
}
]
也就是说,新生成的页面需要在这里加入新的路由对象,将新页面的文件路径填入routes数组中。
这时候,就需要AST(抽象语法树)来帮忙了:
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于
if-condition-then这样的条件跳转语句,可以使用带有三个分支的节点来表示。
换句话说,抽象语法树就是代码的代码,用代码的形式来描述代码,从而做到“用代码写代码”的功能。
那么,这么一个JS对象,它的语法树是什么样的呢?可以用下面这个网站来查看:astexplorer.net/
简单探究了下掘金上AST相关的文章,解决方案就来了。
解决篇
也就是说,我们可以读取当前JS文件的AST树,解析之后在树上接上新加的节点,之后就可以再次生成新的JS代码了。
这里需要引入四个包:
- @babel/parser 用于解析原代码AST树
- @babel/traverse 用于遍历AST树
- @babel/type 用于添加指定类型的节点
- @babel/generator 用于根据AST树生成新的代码
const { parse } = require("@babel/parser");
const generate = require("@babel/generator");
const traverse = require("@babel/traverse");
const { objectExpression, objectProperty, stringLiteral,arrowFunctionExpression } = require("@babel/types");
const code = `
let routes = [
{
path: '/promotion/goods/list',
name: 'promotion_goods',
component: () => import('@/pages/promotion/goods/list.vue'),
meta: {
title: "消费页"
}
}
]
`
const ast = parse(code);
// 遍历当前代码的AST树
traverse.default(ast, {
// 只遍历其中的数组
ArrayExpression(path) {
// 在数组中根据结构添加对象
path.node.elements.push(objectExpression([
objectProperty(stringLiteral("path"), stringLiteral("/promotion/goods/detail")),
objectProperty(stringLiteral("name"), stringLiteral("promotion_detail")),
objectProperty(stringLiteral("component"), arrowFunctionExpression([],stringLiteral('@/pages/promotion/goods/detail.vue'))),
objectProperty(stringLiteral("name"), objectExpression([
objectProperty(stringLiteral("title"), stringLiteral(escape("消费详情页"))),
])),
]));
},
})
const output = generate.default(ast);
console.log(unescape(output.code));
最后生成的结果就是这样:
let routes = [{
path: '/promotion/goods/list',
name: 'promotion_goods',
component: () => import('@/pages/promotion/goods/list.vue'),
meta: {
title: "消费页"
}
}, {
"path": "/promotion/goods/detail",
"name": "promotion_detail",
"component": () => "@/pages/promotion/goods/detail.vue",
"name": {
"title": "消费详情页"
}
}];
其中比较困难的是生成新节点这部分:
objectExpression([
objectProperty(stringLiteral("path"), stringLiteral("/promotion/goods/detail")),
objectProperty(stringLiteral("name"), stringLiteral("promotion_detail")),
objectProperty(stringLiteral("component"), arrowFunctionExpression([], stringLiteral('@/pages/promotion/goods/detail.vue'))),
objectProperty(stringLiteral("name"), objectExpression([
objectProperty(stringLiteral("title"), stringLiteral(escape("消费详情页"))),
])),
])
这里需要对照着刚才那个页面中的AST树,找到对应节点的构造函数,然后通过层层嵌套,生成新的节点:
- 对象表达式对应
objectExpression - 对象中的一个键值对对应
objectProperty - 字符串对应
stringLiteral - 胖箭头函数对应
arrowFunctionExpression,注意构造函数第一个参数是数组,代表参数,后一个才是函数体
最终再按照之前的方式使用nodejs的fs对象对JS文件进行改写,一个简单的页面脚手架就完工了!