如何实现 Vue 转小程序 (3)
这部分实现的是处理 vue 中 script 标签内 js 代码,将其转换为小程序可以解析的 js 文件代码
babel
我借助了 babel 将代码转为 ast 树,然后对 ast 树进行一些处理,最后再转换为代码
主要用了以下几个库:
- @babel/parser:将代码转为 ast 树
- @babel/traverse:用来遍历 ast 树
- @babel/types:可以用作验证、构造 ast 节点
- @babel/template:以模版的形式生成 ast 节点,适用于生成复杂 ast 节点
- @babel/generator:将 ast 树再转换为代码
更多关于 babel 的使用,可以查看官方的babel 手册,在编写代码的时候,可以使用https://astexplorer.net/这个网站,能清楚地明白代码的 ast 树构成
开始转换
首先,我们要明确我们要怎样改造 vue 的 script 代码,才能让它在小程序上运行:
- vue 中的 data 是一个函数,返回一个对象,而小程序中则直接是一个对象
- vue 中是在 component 属性中引入子组件,小程序是将子组件和路径写在 json 配置的 useComponents 中
- vue 中 props 的默认值关键字为 value,小程序为 default
- vue 和小程序的生命周期勾子函数名不一样
- vue 中的 name 属性,小程序并不存在,需要删除
- vue 中的代码使用 export default 输出,而小程序中则不需要,小程序使用的是直接用 compoent/page 包裹组件属性
由于,微信存在 json 配置文件,而在 vue 中并不存在,我在 vue 中自定义了一个 wx 属性,用来接收配置
下面就是一个简单的单文件 vue 中 script 标签内的代码:
import ComponentA from "../componentA";
export default {
name: "Hello Vue",
wx: {},
components: {
"component-a": ComponentA
},
props: {
propA: Number,
propB: {
type: Number,
default: 100
}
},
data: function() {
return {
name: "bowen"
};
},
mounted() {}
};
我们拿到这段代码的第一步当然是将它转化为 ast:
const parser = require("@babel/parser");
// script就是上文的代码
const ast = parser.parse(script, {
sourceType: "module"
});
我们可以直接通过上文提到的网站,清楚地看到它的 ast 结构:
再对 ast 进行遍历,先获取到 export default 这个语句,通过 t.isObjectExpression()方法判断 export default 出去的是否为一个对象:
const traverse = require("@babel/traverse")["default"];
const t = require("@babel/types");
const result = { wxConfig: {}, useComponents: {} };
traverse(ast, {
ExportDefaultDeclaration(rootPath) {
let declarationPath = rootPath.get("declaration");
if (t.isObjectExpression(declarationPath)) {
}
}
});
接下来就是对 vue 中定义的一些属性的处理:
const prop2handler = {
data: dataHandler,
props: propHandler,
wx: wxHandler,
components: componentsHandler,
name: nameHandler
};
...
if (t.isObjectExpression(declarationPath)) {
const properties = declarationPath.get("properties");
properties.forEach(property => {
const key = property.node.key.name;
const handler = prop2handler[key];
if (handler) {
handler(property, rootPath, result);
} else {
lifetimesHandler(property, rootPath);
}
});
}
我们拿到 export default 出去的这个对象的属性,并遍历它们,获取到 key(也就是 data、props 等等),匹配对应的 handler,如果有对应的 handler,传入这个属性的 path,export default 语句的 path 和声明的 result 对象;如果没找到对应的 handler,试着用生命周期的 handler 去处理它
各类 handler
dataHandler:
const DATA_ID = "__v2mp__data__";
handlers.dataHandler = (path, rootPath) => {
const dataFun = path.get("value").node;
const buildDataDec = template(`const %%id%% = (%%fun%%)()`);
const dataDec = buildDataDec({
id: t.identifier(DATA_ID),
fun: dataFun
});
rootPath.insertBefore(dataDec);
path.node.value = t.identifier(DATA_ID);
};
由于 vue 中 data 是一个方法,在小程序中我在 export default 语句前直接执行了这个方法,拿到返回值并作为 data 属性的值
propsHandler
handlers.propHandler = path => {
const props = path.get("value").get("properties");
props.forEach(prop => {
const propVal = prop.get("value");
if (t.isObjectExpression(propVal)) {
const props = propVal.get("properties");
props.forEach(prop => {
const keyName = prop.get("key").node.name;
if (keyName === "default") {
prop.get("key").node.name = "value";
}
});
}
});
};
将 vue 中的默认值属性 value 改为小程序中的 default
lifetimesHandler
const lifetimes = {
created: "created",
beforeMount: "attached",
mounted: "ready",
beforeDestroy: "detached"
};
handlers.lifetimesHandler = path => {
const lifetimesName = path.node.key.name;
wxLifetimes = lifetimes[lifetimesName];
if (wxLifetimes) {
path.get("key").replaceWith(t.identifier(wxLifetimes));
}
};
vue 的生命周期于小程序不同,我在转换的时候只处理了部分生命周期,更多的生命周期你可以自己尝试模拟
nameHandler
handlers.nameHandler = path => {
path.remove();
};
多余的属性直接删除
exportDefaultHanlder
我们在处理完各类属性后要将 vue 的 export default xxx 语句要改成小程序的 Component(xxx):
traverse(ast, {
ExportDefaultDeclaration(rootPath) {
let declarationPath = rootPath.get("declaration");
if (t.isObjectExpression(declarationPath)) {
...
}
exportDefaultHanlder(rootPath);
}
});
handlers.exportDefaultHanlder = path => {
const componentTemplate = template("Component(%%obj%%);");
let declarationPath = path.get("declaration");
if (t.isObjectExpression(declarationPath)) {
const component = componentTemplate({
obj: declarationPath.node
});
path.replaceWith(component);
}
};
componentsHandler
handlers.componentsHandler = (path, rootPath, result) => {
const components = path.get("value").get("properties");
const useComponents = {};
components.forEach(component => {
const key = component.node.key;
const componentName = key.value || key.name;
const componentPath = component.node.value.name;
useComponents[componentPath] = componentName;
});
result.useComponents = useComponents;
path.remove();
};
拿到 components 对象里的组件名和组件路径,并保存在 result 的 useComponents 属性中(是以组件路径为键名)
通常,组件路径是在之前就通过 import/require 语句引入的。在执行完这次 ast 树遍历后,result 里保存着我们的 useComponents 信息,这时,我们进行第二次遍历,处理 import 和 require 语句,判断其是否为引入组件,如果是,则删除该 import/requier 语句:
traverse(ast, {
ImportDeclaration(path) {
importHandler(path, result);
},
VariableDeclaration(path) {
variableHandler(path, result);
}
});
handlers.importHandler = (path, result) => {
let id, importPath;
path.traverse({
Identifier(path) {
id = path.node.name;
}
});
importPath = path.get("source").node.value;
if (result.useComponents[id]) {
result.useComponents[id] = importPath;
path.remove();
}
};
handlers.variableHandler = (path, result) => {
const declarations = path.get("declarations.0");
if (!t.isVariableDeclarator(declarations)) {
return;
}
const init = declarations.get("init");
if (!t.isCallExpression(init)) {
return;
}
const callee = init.get("callee");
if (callee.node.name !== "require") {
return;
}
let id, importPath;
id = declarations.get("id").node.name;
importPath = init.get("arguments.0").node.value;
if (result.useComponents[id]) {
result.useComponents[id] = importPath;
path.remove();
}
};
转换结果
上文的样例结果转换,输出结果为下:
const __v2mp__data__ = (function() {
return {
name: "bowen"
};
})();
Component({
props: {
propA: Number,
propB: {
type: Number,
value: 100
}
},
data: __v2mp__data__,
ready() {}
});