過年在家辦公期間,接到了一個需求,需要將目前的 微信小程序自定義組件 擴展到 支付寶小程序 平臺。關(guān)于需求的背景和歷史這邊就暫不多說了,就從上面已說明的內(nèi)容來看待這個需求吧。 接到需求的第一時間,筆者就思考,這不就是多端編譯嗎?話不多說,那就開搞吧。
背景介紹
由于筆者的項目是一個單純的微信小程序自定義組件,打包工具是rollup,所以,筆者的技術(shù)方案是編寫一個rollup插件,來支持多端編譯。關(guān)于rollup和rollup插件的寫法本次不作過多介紹,有興趣的可以看它的 官方文檔 ,這邊只是介紹一下核心的多端編譯流程。
流程介紹
微信小程序組件包含 *.json 、 *.js 、 *.wxml 、 *.wxss 這4個文件,要轉(zhuǎn)換成支付寶小程序,其中json文件和wxss文件比較簡單,前者原封不動,后者改一下后綴名就好了,主要要修改js和wxml兩個文件。
大致流程基本就是如下
差異整理
將代碼轉(zhuǎn)成AST樹
替換樹上的節(jié)點
根據(jù)新的AST樹生成代碼
acorn
對于js文件,要實現(xiàn)這些功能的話,業(yè)界已經(jīng)有一些出色的工具了。筆者選擇了 babel ,babel內(nèi)置acron作為javascript解釋器,生成符合estree標準的AST樹(可以在 astexplorer.net/ 中查看效果)。其次babel的封裝很漂亮,除了搭配webpack完成日常的構(gòu)建工作外,它還提供了 @babel/parser , @babel/generator , @babel/traverse , @babel/types 等優(yōu)秀的工具包,每個工具包都是單一職責,職責很明確,幫助實現(xiàn)以上的流程(其實rollup內(nèi)置了acron實例,不過babel會更好用一些)。 其中 @babel/parser 可以將js代碼解釋為AST樹, @babel/generator 將根據(jù)AST樹生成js代碼, @babel/traverse 支持高效地操作AST樹的節(jié)點, @babel/types 則提供一些判斷函數(shù),幫助開發(fā)者快速定位節(jié)點。
看一個簡單的示例
function sayHello() {
console.log('hello')
}
sayHello();
復制代碼
對于以上這段代碼,通過acron轉(zhuǎn)換后,得出的AST樹如下
{
"type": "Program",
"start": 0,
"end": 58,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 45,
"id": {
"type": "Identifier",
"start": 9,
"end": 17,
"name": "sayHello"
},
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 20,
"end": 45,
"body": [
{
"type": "ExpressionStatement",
"start": 23,
"end": 43,
"expression": {
"type": "CallExpression",
"start": 23,
"end": 43,
"callee": {
"type": "MemberExpression",
"start": 23,
"end": 34,
"object": {
"type": "Identifier",
"start": 23,
"end": 30,
"name": "console"
},
"property": {
"type": "Identifier",
"start": 31,
"end": 34,
"name": "log"
},
"computed": false
},
"arguments": [
{
"type": "Literal",
"start": 35,
"end": 42,
"value": "hello",
"raw": "'hello'"
}
]
}
}
]
}
},
{
"type": "ExpressionStatement",
"start": 47,
"end": 58,
"expression": {
"type": "CallExpression",
"start": 47,
"end": 57,
"callee": {
"type": "Identifier",
"start": 47,
"end": 55,
"name": "sayHello"
},
"arguments": []
}
}
],
"sourceType": "module"
}
復制代碼
對于這段js代碼,如果要替換它的方法名為 sayHi 、打印出的 hello 替換為 Hi ,通過babel,只需要這樣做就可以了。
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
import * as t from "@babel/types";
const code = `
function sayHello() {
console.log('hello')
}
sayHello();
`;
const transform = code => {
const ast = parse(code);
traverse(ast, {
enter(path) {
if (t.isIdentifier(path.node, { name: "sayHello" })) {
path.node.name = "sayHi";
}
if (t.isLiteral(path.node, { value: "hello" })) {
path.node.value = "Hi";
}
}
});
const output = generate(ast, {}, code);
return output;
};
console.log(transform(code).code);
復制代碼
也可以在 codeSandbox 中查看效果。
關(guān)于包的其它使用,可以查看 官方手冊 。
himalaya
對于wxml文件,筆者選擇了 himalaya-wxml ,它提供了 parse 和 stringify 兩個方法,前者將wxml解釋成AST樹,后者反之(可以在 jew.ski/himalaya/ 中查看效果)。通過 parse 將wxml代碼轉(zhuǎn)換成AST樹之后,接下去只需要手動遞歸遍歷AST樹去替換節(jié)點,再將其轉(zhuǎn)換回wxml代碼就可以完成工作了。
同樣,看一個簡單的示例
<div id='main'>
<span>hello world</span>
</div>
復制代碼
對于以上html代碼,通過 himalaya 轉(zhuǎn)換后,生成的AST樹如下
[
{
"type": "element",
"tagName": "div",
"attributes": [],
"children": [
{
"type": "text",
"content": "\n "
},
{
"type": "element",
"tagName": "span",
"attributes": [],
"children": [
{
"type": "text",
"content": "hello world"
}
]
},
{
"type": "text",
"content": "\n"
}
]
}
]
復制代碼
對于這段代碼html代碼,如果要替換它外層 div 的 id 為 container ,只需要這樣做就可以了。
import { parse, stringify } from "himalaya";
const code = `
<div id='main'>
<span>hello world</span>
</div>
`;
const traverse = ast => {
return ast.map(item => {
if (item.type === "element" && item.attributes) {
return {
...item,
attributes: item.attributes.map(attr => {
if (attr.key !== "id") {
return attr;
}
return {
...attr,
value: "container"
};
})
};
}
return item;
});
};
const transform = code => {
const ast = parse(code);
const json = traverse(ast);
return stringify(json);
};
console.log(transform(code));
復制代碼
也可以在 codeSandbox 中查看效果。
核心介紹
流程和工具介紹的差不多了,接下來就開始正題吧。 首先是整理差異,根據(jù)筆者的調(diào)研,微信小程序組件要轉(zhuǎn)換成支付寶小程序組件,大致有以下幾個改動(只是符合筆者的需求,如果不完全,歡迎補充):
wxml后綴名要改成axml
wxss后綴名要改成acss
wxml中的屬性wx-xxx要改成a-xxx
wxml中的事件屬性bindxxx要改成onXxx
生命周期attached要替換成onInit
生命周期detached要替換成didUnmount
生命周期pageLifetimes.show要替換成didMount
生命周期pageLifetimes要刪除
改后綴名的工作相對簡單,交給構(gòu)建工具,output配置里面指定一下就好了,重點是替換屬性。
轉(zhuǎn)換js部分代碼如下
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import * as t from '@babel/types';
function transformJs(code: string) {
const ast = parse(code);
let pp;
traverse(ast, {
enter(path) {
if (t.isIdentifier(path.node, {name: 'attached'})) {
path.node.name = 'onInit';
}
if (t.isIdentifier(path.node, {name: 'detached'})) {
path.node.name = 'didUnmount';
pp = path.parentPath;
}
if(t.isIdentifier(path.node.key, {name: 'show'})){
path.node.key.name = 'didMount';
pp.insertAfter(path.node);
}
},
exit(path) {
if(t.isIdentifier(path.node.key, {name: 'pageLifetimes'})){
path.remove();
}
}
});
const output = generate(ast, {}, code);
return output
}
export default transformJs
復制代碼
轉(zhuǎn)換wxml部分如下:
import { parse, stringify } from 'himalaya-wxml';
const traverseKey = (key: string) => {
if(key.startsWith('wx:')){
const postfix = key.slice(3);
return `a:${postfix}`;
}
if(key === 'catchtouchmove'){
return 'catchTouchMove';
}
if(key === 'bindtap'){
return 'onTap';
}
if(key === 'bindload'){
return 'onLoad';
}
if(key === 'binderror'){
return 'onError';
}
if(key === 'bindchange'){
return 'onChange';
}
return key
}
const traverseAst = (ast: any) => {
return ast.map(item => {
if(item.type !== 'element'){
return item;
}
let res = item;
if(item.attributes){
res = {
...item,
attributes: item.attributes.map(attr => ({
...attr,
key: traverseKey(attr.key)
}))
}
}
if(item.children){
res.children = traverseAst(item.children);
}
return res
});
}
const transformWxml = (code: string) => {
const ast = parse(code);
const json = traverseAst(ast);
return stringify(json)
}
export default transformWxml
復制代碼
以上,就擁有了兩個轉(zhuǎn)換函數(shù),再之后的工作,就是將這兩個函數(shù)運行在rollup里,就完成了將微信小程序組件轉(zhuǎn)換成支付寶小程序組件的功能。
總結(jié)
javascript作為前端最常用的語言,我們不僅要熟悉它,更要能操控它,通過javascript解釋器,我們就擁有了操控它的能力?;乇敬T源,鞏固基礎(chǔ),才能在寒冬之中保持內(nèi)心的平靜。
電話咨詢
在線咨詢
QQ咨詢
官方微信
TOP