過年(nian)在(zai)家辦公期間(jian),接到(dao)了一個需求(qiu),需要將目前(qian)的(de)(de) 微信小程序自定(ding)義組件 擴展到(dao) 支付寶小程序 平(ping)臺。關(guan)于需求(qiu)的(de)(de)背景(jing)和歷史這(zhe)邊就暫不(bu)(bu)多(duo)說了,就從上面已說明的(de)(de)內容來看待這(zhe)個需求(qiu)吧。 接到(dao)需求(qiu)的(de)(de)第一時(shi)間(jian),筆者就思考,這(zhe)不(bu)(bu)就是(shi)多(duo)端編譯嗎?話(hua)不(bu)(bu)多(duo)說,那(nei)就開搞吧。
由于(yu)筆(bi)者的項目(mu)是一個單(dan)純(chun)的微信小程(cheng)序自定義(yi)組件,打包工具是rollup,所以(yi),筆(bi)者的技術方案(an)是編寫一個rollup插件,來支(zhi)持多(duo)端編譯(yi)。關于(yu)rollup和(he)rollup插件的寫法(fa)本(ben)次不作過(guo)多(duo)介(jie)紹(shao),有興趣(qu)的可以(yi)看它的 官(guan)方文(wen)檔 ,這邊(bian)只是介(jie)紹(shao)一下核心的多(duo)端編譯(yi)流程(cheng)。
微信小程序組件包含 *.json 、 *.js 、 *.wxml 、 *.wxss 這4個(ge)(ge)文(wen)(wen)件,要轉換成(cheng)支付寶(bao)小程序,其中json文(wen)(wen)件和(he)wxss文(wen)(wen)件比較簡單,前(qian)者(zhe)原封(feng)不動,后(hou)者(zhe)改一下后(hou)綴名(ming)就(jiu)好(hao)了,主要要修(xiu)改js和(he)wxml兩個(ge)(ge)文(wen)(wen)件。
大(da)致流程基本(ben)就(jiu)是如下
對(dui)于js文件,要實(shi)現這些(xie)(xie)功能(neng)的(de)(de)話,業界已經有一(yi)些(xie)(xie)出(chu)色的(de)(de)工(gong)具了(le)(le)(le)。筆者(zhe)選擇(ze)了(le)(le)(le) ,babel內置acron作(zuo)(zuo)為(wei)(wei)javascript解釋(shi)器,生成(cheng)符(fu)合(he)estree標準的(de)(de)AST樹(shu)(shu)(可以(yi)(yi)在 astexplorer.net/ 中查看(kan)效(xiao)果(guo))。其(qi)(qi)次babel的(de)(de)封裝很漂(piao)亮,除了(le)(le)(le)搭配webpack完成(cheng)日常的(de)(de)構(gou)建工(gong)作(zuo)(zuo)外,它還(huan)提供了(le)(le)(le) @babel/parser , @babel/generator , @babel/traverse , @babel/types 等優秀的(de)(de)工(gong)具包,每個工(gong)具包都(dou)是單一(yi)職責,職責很明確,幫助(zhu)實(shi)現以(yi)(yi)上的(de)(de)流程(其(qi)(qi)實(shi)rollup內置了(le)(le)(le)acron實(shi)例,不過babel會(hui)更好用一(yi)些(xie)(xie))。 其(qi)(qi)中 @babel/parser 可以(yi)(yi)將js代碼(ma)解釋(shi)為(wei)(wei)AST樹(shu)(shu), @babel/generator 將根據AST樹(shu)(shu)生成(cheng)js代碼(ma), @babel/traverse 支持高效(xiao)地操作(zuo)(zuo)AST樹(shu)(shu)的(de)(de)節(jie)點, @babel/types 則提供一(yi)些(xie)(xie)判斷函數,幫助(zhu)開(kai)發者(zhe)快速定位節(jie)點。
看(kan)一個簡單的(de)示(shi)例
function sayHello() {
console.log('hello')
}
sayHello();
復制代碼
對于以上這段(duan)代碼,通(tong)過(guo)acron轉換(huan)后,得(de)出(chu)的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"
}
復制代碼
對(dui)于這段js代(dai)碼(ma),如果要(yao)替換它(ta)的(de)方法名為 sayHi 、打印(yin)出(chu)的(de) hello 替換為 Hi ,通過babel,只需要(yao)這樣做就可以(yi)了。
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);
復制代碼
也可(ke)以(yi)在 codeSandbox 中查看效果。
關(guan)于包的其它使用,可以查(cha)看 。
對于wxml文件,筆者選擇(ze)了 ,它提供了 parse 和 stringify 兩個方法(fa),前(qian)者將(jiang)wxml解釋(shi)成AST樹(shu),后者反之(可以(yi)(yi)在 jew.ski/himalaya/ 中查看效果)。通過 parse 將(jiang)wxml代(dai)碼轉(zhuan)換成AST樹(shu)之后,接下去(qu)只需(xu)要(yao)手動遞歸遍(bian)歷AST樹(shu)去(qu)替換節(jie)點(dian),再(zai)將(jiang)其轉(zhuan)換回wxml代(dai)碼就可以(yi)(yi)完成工作了。
同樣,看(kan)一個(ge)簡單(dan)的(de)示(shi)例(li)
<div id='main'> <span>hello world</span> </div> 復制代碼
對于以上html代碼,通過 himalaya 轉換后,生(sheng)成的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"
}
]
}
]
復制代碼
對于這段(duan)代碼html代碼,如(ru)果要替換(huan)它外層 div 的 id 為(wei) container ,只需要這樣做就可以了(le)。
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));
復制代碼
也可(ke)以在 codeSandbox 中(zhong)查看效(xiao)果。
流(liu)程和工具介紹(shao)的(de)(de)差不(bu)多了,接下來就開始(shi)正題(ti)吧。 首先是整理(li)差異,根(gen)據筆者(zhe)的(de)(de)調研,微信小(xiao)程序組件(jian)要轉換成支付(fu)寶小(xiao)程序組件(jian),大致(zhi)有以下幾個改動(dong)(只是符合筆者(zhe)的(de)(de)需求(qiu),如果(guo)不(bu)完全,歡迎補充(chong)):
改(gai)后綴名的工作相對(dui)簡單,交(jiao)給構建工具,output配置里面指定一下(xia)就好了,重(zhong)點是替換屬性。
轉換js部分代(dai)碼如(ru)下
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
復制代碼
轉換(huan)wxml部分如(ru)下:
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
復制代碼
以上,就(jiu)擁(yong)有了兩(liang)(liang)個轉換函數,再(zai)之(zhi)后的工作,就(jiu)是將(jiang)這(zhe)兩(liang)(liang)個函數運行在rollup里(li),就(jiu)完成(cheng)了將(jiang)微信(xin)小(xiao)程(cheng)序(xu)組件(jian)轉換成(cheng)支付寶小(xiao)程(cheng)序(xu)組件(jian)的功能。
javascript作為前(qian)端最常用的語(yu)言,我們不僅(jin)要(yao)熟悉它,更要(yao)能操控它,通過javascript解(jie)釋器,我們就擁有了操控它的能力。回本碩源,鞏固基(ji)礎,才能在寒冬之中保持內心的平(ping)靜。