這是本系列的第二篇,過去兩周,已經(jīng)有相當(dāng)成果出來。本文介紹其中一部分可靠的思路,這個(gè)比京東的taro更具可靠性。如果覺得看不過癮,可以看anu的源碼,里面包含了miniapp的轉(zhuǎn)換器。 微信小程序是面向配置對象編程,不暴露Page,App,Component等核心對象的原型,只提供三個(gè)工廠方法,因此無法實(shí)現(xiàn)繼承。App,Page,Component所在的JS的依賴處理也很弱智,你需要聲明在同一目錄下的json文件中。 比如說 Component({ properties: {}, data: {}, onClick: function(){} }) properties與data都是同一個(gè)東西,properties只是用來定義data中的數(shù)據(jù)的默認(rèn)值與類型,相當(dāng)于React的defaultProps與propTypes。如何轉(zhuǎn)換呢? import {Component} form "./wechat" Class AAA extends Component{ constructor(props){ super(props); this.state = {} } static propTypes = {} static defaultProps = {} onClick(){} render(){} } export AAA;
首先我們要提供一個(gè)wechat.js文件,里面提供Component, Page, App 這幾個(gè)基類,現(xiàn)在只是空實(shí)現(xiàn),但已經(jīng)足夠了,保證它在調(diào)試不會(huì)出錯(cuò)。我們要的是`Class AAA extends Component`這個(gè)語句的內(nèi)容。學(xué)了babel,對JS語法更加熟悉了。這個(gè)語句在babel6中稱為ClassExpression,到babel7中又叫ClassDeclaration。babel有一個(gè)叫"babel-traverse"的包,可以將我們的代碼的AST,然后根據(jù)語法的成分進(jìn)行轉(zhuǎn)換(詳見這文章 https://yq.aliyun.com/articles/62671)。ClassDeclaration的參數(shù)為一個(gè)叫path的對象,我們通過 path.node.superClass.name 就能拿到Component這個(gè)字樣。如果我們的類定義是下面的這樣,path.node.superClass.name 則為App。 Class AAA extends App{ constructor(props){ super(props); this.state = {} } } App, Page, Component對應(yīng)的json差異很大,拿到這個(gè)可以方便我們區(qū)別對待。 然后我們繼續(xù)定義一個(gè)ImportDeclaration處理器,將import語句去掉。 定義ExportDefaultDeclaration與ExportNamedDeclaration處理器,將export語句去掉。 到這里我不得不展示一下我的轉(zhuǎn)碼器的全貌了。我是通過rollup得到所有模塊的路徑與文件內(nèi)容,然后通過babel進(jìn)行轉(zhuǎn)譯。babel轉(zhuǎn)換是通過babel.transform。babel本來就有許多叫babel-plugin-transform-xxx的插件,它是專門處理那些es5無法識(shí)別的新語法。我們需要在這后面加上一個(gè)新插件叫miniappPlugin
// https://github.com/RubyLouvre/anu/blob/master/packages/render/miniapp/translator/transform.js const syntaxClassProperties = require("babel-plugin-syntax-class-properties") const babel = require('babel-core') const visitor = require("./visitor"); var result = babel.transform(code, { babelrc: false, plugins: [ 'syntax-jsx', // "transform-react-jsx", 'transform-decorators-legacy', 'transform-object-rest-spread', miniappPlugin, ] }) function miniappPlugin(api) { return { inherits: syntaxClassProperties, visitor: visitor }; } miniappPlugin的結(jié)構(gòu)異常簡單,它繼承一個(gè)叫syntaxClassProperties的插件,這插件原來用來解析es6 class的屬性的,因?yàn)槲覀兊哪繕?biāo)也是抽取React類中的defaultProps, propsTypes靜態(tài)屬性。 visitor的結(jié)構(gòu)很簡單,就是各種JS語法的描述。 const t = require("babel-types"); module.exports = { ClassDeclaration: 抽取父類的名字與轉(zhuǎn)換構(gòu)造器, ClassExpression: 抽取父類的名字與轉(zhuǎn)換構(gòu)造器, ImportDeclaration(path) { path.remove() //移除import語句,小程序會(huì)自動(dòng)在外面包一層,變成AMD模塊 }, ExportDefaultDeclaration(path){ path.remove() //AMD不認(rèn)識(shí)export語句,要?jiǎng)h掉,或轉(zhuǎn)換成module.exports }, ExportNamedDeclaration(path){ path.remove() //AMD不認(rèn)識(shí)export語句,要?jiǎng)h掉,或轉(zhuǎn)換成module.exports } } 我再介紹一下visitor的處理器是怎么用的,處理器其實(shí)會(huì)執(zhí)行兩次。我們的AST樹每個(gè)節(jié)點(diǎn)會(huì)被執(zhí)行兩次,如果學(xué)過DFS的同學(xué)會(huì)明白,第一次訪問后,做些處理,然后進(jìn)行它內(nèi)部的節(jié)點(diǎn),處理后再訪問一次。于是visitor也可以這樣定義。 ClassDeclaration:{ enter(path){}, exit(path){} } 如果以函數(shù)形式定義,那么它只是作為enter來用。 AST會(huì)從上到下執(zhí)行,我們先拿到類名的名字與父類的名字,我們定義一個(gè)modules的對象,保存信息。 enter(path) { let className = path.node.superClass ? path.node.superClass.name : ""; let match = className.match(/\.?(App|Page|Component)/); if (match) { //獲取類的組件類型與名字 var componentType = match[1]; if (componentType === "Component") { modules.componentName = path.node.id.name; } modules.componentType = componentType; } }, 我們在第二次訪問這個(gè)類定義時(shí),要將類定義轉(zhuǎn)換為函數(shù)調(diào)用。即 Class AAA extends Component ---> Component({}) 實(shí)現(xiàn)如下,將原來的類刪掉(因此才在exit時(shí)執(zhí)行),然后新建一個(gè)函數(shù)調(diào)用語句。我們可以通過babel-types這個(gè)句實(shí)現(xiàn)。具體看這里。比如說: const call = t.expressionStatement( t.callExpression(t.identifier("Component"), [ t.objectExpression([])]) ); path.replaceWith(call); 就能產(chǎn)生如下代碼,將我們的類定義從原位置替換掉。 Component({}) 但我們不能是一個(gè)空對象啊,因此我們需要收集它的方法。 我們需要在visitors對象添加一個(gè)ClassMethod處理器,收集原來類的方法。類的方法與對象的方法不一樣,對象的方法叫成員表達(dá)式,需要轉(zhuǎn)換一下。我們首先弄一個(gè)數(shù)組,用來放東西。 var methods = [] module.exports= { ClassMethod: { enter(path){ var methodName = path.node.key.name var method = t.ObjectProperty( t.identifier(methodName), t.functionExpression( null, path.node.params, path.node.body, path.node.generator, path.node.async ) ); methods.push(method) } } 然后我們在ClassDeclaration或ClassExpression的處理器的exit方法中改成: const call = t.expressionStatement( t.callExpression(t.identifier("Component"), [ t.objectExpression(methods)]) ); path.replaceWith(call); 于是函數(shù)定義就變成 Component({ constructor:function(){}, render:function(){}, onClick: function(){} })
到這里,我們開始另一個(gè)問題了。小程序雖然是抄React,但又想別出心裁,于是一些屬性與方法是不一樣的。比如說data對應(yīng)state, setData對應(yīng)setState,早期的版本還有forceUpdate之類的。data對應(yīng)一個(gè)對象,你可以有千奇百怪的寫法。 this.state ={ a: 1} this["state"] = {b: 1}; this.state = {} this.state.aa = 1 你想hold住這么多奇怪的寫法是很困難的,因此我們可以對constructor方法做些處理,然后其他方法做些約束,來減少轉(zhuǎn)換的成本。什么處理constructor呢,我們可以定義一個(gè)onInit方法,專門劫持constructor方法,將this.state變成this.data。 function onInit(config){ if(config.hasOwnProperty("constructor")){ config.constructor.call(config); } config.data = config.state|| {}; delete config.state return config; } Component(onInit({ constructor:function(){}, render:function(){}, onClick: function(){} })) 具體實(shí)現(xiàn)參這里,本文就不貼上來了。 那this.setState怎么轉(zhuǎn)換成this.setData呢。這是一個(gè)函數(shù)調(diào)用,語法上稱之為**CallExpression**。我們在visitors上定義同名的處理器。 CallExpression(path) { var callee = path.node.callee || Object; if ( modules.componentType === "Component" ) { var property = callee.property; if (property && property.name === "setState") { property.name = "setData"; } } }, |
工作日 8:30-12:00 14:30-18:00
周六及部分節(jié)假日提供值班服務(wù)