这是本系列的第二篇,过去两周,已经有相当成果出来。本文介绍其中一部分可靠的思路,这个比京东的taro更具可靠性。如果觉得看不过瘾,可以看anu的源码,里面包含了miniapp的转换器。
微信小程序是面向配置对象编程,不暴露Page,App,Component等核心对象的原型,只提供三个工厂方法,因此无法实现继承。App,Page,Component所在的JS的依赖处理也很弱智,你需要声明在同一目录下的json文件中。
比如说
Component({
properties: {},
data: {},
onClick: function(){}
})
import {Component} form "./wechat"
Class AAA extends Component{
constructor(props){
super(props);
this.state = {}
}
static propTypes = {}
static defaultProps = {}
onClick(){}
render(){}
}
export AAA;
Class AAA extends App{
constructor(props){
super(props);
this.state = {}
}
}
App, Page, Component对应的json差异很大,拿到这个可以方便我们区别对待。
然后我们继续定义一个ImportDeclaration处理器,将import语句去掉。
定义ExportDefaultDeclaration与ExportNamedDeclaration处理器,将export语句去掉。
到这里我不得不展示一下我的转码器的全貌了。我是通过rollup得到所有模块的路径与文件内容,然后通过babel进行转译。babel转换是通过babel.transform。babel本来就有许多叫babel-plugin-transform-xxx的插件,它是专门处理那些es5无法识别的新语法。我们需要在这后面加上一个新插件叫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的结构异常简单,它继承一个叫syntaxClassProperties的插件,这插件原来用来解析es6 class的属性的,因为我们的目标也是抽取React类中的defaultProps, propsTypes静态属性。
visitor的结构很简单,就是各种JS语法的描述。
const t = require("babel-types");
module.exports = {
ClassDeclaration: 抽取父类的名字与转换构造器,
ClassExpression: 抽取父类的名字与转换构造器,
ImportDeclaration(path) {
path.remove() //移除import语句,小程序会自动在外面包一层,变成AMD模块
},
ExportDefaultDeclaration(path){
path.remove() //AMD不认识export语句,要删掉,或转换成module.exports
},
ExportNamedDeclaration(path){
path.remove() //AMD不认识export语句,要删掉,或转换成module.exports
}
}
ClassDeclaration:{
enter(path){},
exit(path){}
}
如果以函数形式定义,那么它只是作为enter来用。
AST会从上到下执行,我们先拿到类名的名字与父类的名字,我们定义一个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;
}
},
我们在第二次访问这个类定义时,要将类定义转换为函数调用。即
Class AAA extends Component ---> Component({})
实现如下,将原来的类删掉(因此才在exit时执行),然后新建一个函数调用语句。我们可以通过babel-types这个句实现。具体看 这里。比如说:
const call = t.expressionStatement(
t.callExpression(t.identifier("Component"), [ t.objectExpression([])])
);
path.replaceWith(call);
Component({})
但我们不能是一个空对象啊,因此我们需要收集它的方法。
我们需要在visitors对象添加一个ClassMethod处理器,收集原来类的方法。类的方法与对象的方法不一样,对象的方法叫成员表达式,需要转换一下。我们首先弄一个数组,用来放东西。
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);
于是函数定义就变成
Component({
constructor:function(){},
render:function(){},
onClick: function(){}
})
this.state ={ a: 1}
this["state"] = {b: 1};
this.state = {}
this.state.aa = 1
你想hold住这么多奇怪的写法是很困难的,因此我们可以对constructor方法做些处理,然后其他方法做些约束,来减少转换的成本。什么处理constructor呢,我们可以定义一个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(){}
}))
具体实现参这里,本文就不贴上来了。
RubyLouvre/anu
那this.setState怎么转换成this.setData呢。这是一个函数调用,语法上称之为**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";
}
}
},
至少,将React类定义转换成Component({})
调用方式 成功了。剩下就是将import语句处理一下,因为要小程序中,如果这个组件引用了其他组件,需要在其json中添加useComponens对象,将这些组件名及链接写上去。换言之,小程序太懒了,处处都要手动。有了React转码器,这些事可以省掉。
其次是render方法的转换,怎么变成一个wxml文件呢,`{}单花括号的内容要转换成
`"{{}}"`双引号+双花括号 ,wx:if, wx:for的模拟等等,且听下回分解。