過年在家辦公期間,接到了一個需求,需要將目前的 微信小程序自定義組件 擴展到 支付寶小程序 平臺。關于需求的背景和歷史這邊就暫不多說了,就從上面已說明的內容來看待這個需求吧。 接到需求的第一時間,筆者就思考,這不就是多端編譯嗎?話不多說,那就開搞吧。
由于筆者的項目是一個單純的微信小程序自定義組件,打包工具是rollup,所以,筆者的技術方案是編寫一個rollup插件,來支持多端編譯。關于rollup和rollup插件的寫法本次不作過多介紹,有興趣的可以看它的 官方文檔 ,這邊只是介紹一下核心的多端編譯流程。
微信小程序組件包含 *.json 、 *.js 、 *.wxml 、 *.wxss 這4個文件,要轉換成支付寶小程序,其中json文件和wxss文件比較簡單,前者原封不動,后者改一下后綴名就好了,主要要修改js和wxml兩個文件。
大致流程基本就是如下
對于js文件,要實現這些功能的話,業界已經有一些出色的工具了。筆者選擇了 babel ,babel內置acron作為javascript解釋器,生成符合estree標準的AST樹(可以在 astexplorer.net/ 中查看效果)。其次babel的封裝很漂亮,除了搭配webpack完成日常的構建工作外,它還提供了 @babel/parser , @babel/generator , @babel/traverse , @babel/types 等優秀的工具包,每個工具包都是單一職責,職責很明確,幫助實現以上的流程(其實rollup內置了acron實例,不過babel會更好用一些)。 其中 @babel/parser 可以將js代碼解釋為AST樹, @babel/generator 將根據AST樹生成js代碼, @babel/traverse 支持高效地操作AST樹的節點, @babel/types 則提供一些判斷函數,幫助開發者快速定位節點。
看一個簡單的示例
function sayHello() { console.log('hello') } sayHello(); 復制代碼
對于以上這段代碼,通過acron轉換后,得出的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 中查看效果。
關于包的其它使用,可以查看 官方手冊 。
對于wxml文件,筆者選擇了 himalaya-wxml ,它提供了 parse 和 stringify 兩個方法,前者將wxml解釋成AST樹,后者反之(可以在 jew.ski/himalaya/ 中查看效果)。通過 parse 將wxml代碼轉換成AST樹之后,接下去只需要手動遞歸遍歷AST樹去替換節點,再將其轉換回wxml代碼就可以完成工作了。
同樣,看一個簡單的示例
<div id='main'> <span>hello world</span> </div> 復制代碼
對于以上html代碼,通過 himalaya 轉換后,生成的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 中查看效果。
流程和工具介紹的差不多了,接下來就開始正題吧。 首先是整理差異,根據筆者的調研,微信小程序組件要轉換成支付寶小程序組件,大致有以下幾個改動(只是符合筆者的需求,如果不完全,歡迎補充):
改后綴名的工作相對簡單,交給構建工具,output配置里面指定一下就好了,重點是替換屬性。
轉換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 復制代碼
轉換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 復制代碼
以上,就擁有了兩個轉換函數,再之后的工作,就是將這兩個函數運行在rollup里,就完成了將微信小程序組件轉換成支付寶小程序組件的功能。
javascript作為前端最常用的語言,我們不僅要熟悉它,更要能操控它,通過javascript解釋器,我們就擁有了操控它的能力。回本碩源,鞏固基礎,才能在寒冬之中保持內心的平靜。