注冊(cè)

React轉(zhuǎn)微信小程序:從React類定義到Component調(diào)用

2020-07-14
導(dǎo)讀:React轉(zhuǎn)微信小程序:從React類定義到Component調(diào)用 。這是本系列的第二篇,過(guò)去兩周,已經(jīng)有相當(dāng)成果出來(lái)。本文介紹其中一部分可靠的思路,這個(gè)比京東的taro更具可靠性。如果覺(jué)得看不過(guò)癮...

  React轉(zhuǎn)微信小程序:從React類定義到Component調(diào)用 。這是本系列的第二篇,過(guò)去兩周,已經(jīng)有相當(dāng)成果出來(lái)。本文介紹其中一部分可靠的思路,這個(gè)比京東的taro更具可靠性。如果覺(jué)得看不過(guò)癮,可以看anu的源碼,里面包含了miniapp的轉(zhuǎn)換器。

  微信小程序是面向配置對(duì)象編程,不暴露Page,App,Component等核心對(duì)象的原型,只提供三個(gè)工廠方法,因此無(wú)法實(shí)現(xiàn)繼承。App,Page,Component所在的JS的依賴處理也很弱智,你需要聲明在同一目錄下的json文件中。

  比如說(shuō)

  Component({

  properties: {},

  data: {},

  onClick: function(){}

  })

  properties與data都是同一個(gè)東西,properties只是用來(lái)定義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è)語(yǔ)句的內(nèi)容。學(xué)了babel,對(duì)JS語(yǔ)法更加熟悉了。這個(gè)語(yǔ)句在babel6中稱為ClassExpression,到babel7中又叫ClassDeclaration。babel有一個(gè)叫"babel-traverse"的包,可以將我們的代碼的AST,然后根據(jù)語(yǔ)法的成分進(jìn)行轉(zhuǎn)換(詳見(jiàn)這文章 https://yq.aliyun.com/articles/62671)。ClassDeclaration的參數(shù)為一個(gè)叫path的對(duì)象,我們通過(guò) path.node.superClass.name 就能拿到Component這個(gè)字樣。如果我們的類定義是下面的這樣,path.node.superClass.name 則為App。

  Class AAA extends App{

  constructor(props){

  super(props);

  this.state = {}

  }

  }

  App, Page, Component對(duì)應(yīng)的json差異很大,拿到這個(gè)可以方便我們區(qū)別對(duì)待。

  然后我們繼續(xù)定義一個(gè)ImportDeclaration處理器,將import語(yǔ)句去掉。

  定義ExportDefaultDeclaration與ExportNamedDeclaration處理器,將export語(yǔ)句去掉。

  到這里我不得不展示一下我的轉(zhuǎn)碼器的全貌了。我是通過(guò)rollup得到所有模塊的路徑與文件內(nèi)容,然后通過(guò)babel進(jìn)行轉(zhuǎn)譯。babel轉(zhuǎn)換是通過(guò)babel.transform。babel本來(lái)就有許多叫babel-plugin-transform-xxx的插件,它是專門處理那些es5無(wú)法識(shí)別的新語(yǔ)法。我們需要在這后面加上一個(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)異常簡(jiǎn)單,它繼承一個(gè)叫syntaxClassProperties的插件,這插件原來(lái)用來(lái)解析es6 class的屬性的,因?yàn)槲覀兊哪繕?biāo)也是抽取React類中的defaultProps, propsTypes靜態(tài)屬性。

  visitor的結(jié)構(gòu)很簡(jiǎn)單,就是各種JS語(yǔ)法的描述。

  const t = require("babel-types");

  module.exports = {

  ClassDeclaration: 抽取父類的名字與轉(zhuǎn)換構(gòu)造器,

  ClassExpression: 抽取父類的名字與轉(zhuǎn)換構(gòu)造器,

  ImportDeclaration(path) {

  path.remove() //移除import語(yǔ)句,小程序會(huì)自動(dòng)在外面包一層,變成AMD模塊

  },

  ExportDefaultDeclaration(path){

  path.remove() //AMD不認(rèn)識(shí)export語(yǔ)句,要?jiǎng)h掉,或轉(zhuǎn)換成module.exports

  },

  ExportNamedDeclaration(path){

  path.remove() //AMD不認(rèn)識(shí)export語(yǔ)句,要?jiǎng)h掉,或轉(zhuǎn)換成module.exports

  }

  }

  我再介紹一下visitor的處理器是怎么用的,處理器其實(shí)會(huì)執(zhí)行兩次。我們的AST樹(shù)每個(gè)節(jié)點(diǎn)會(huì)被執(zhí)行兩次,如果學(xué)過(guò)DFS的同學(xué)會(huì)明白,第一次訪問(wèn)后,做些處理,然后進(jìn)行它內(nèi)部的節(jié)點(diǎn),處理后再訪問(wèn)一次。于是visitor也可以這樣定義。

  ClassDeclaration:{

  enter(path){},

  exit(path){}

  }

  如果以函數(shù)形式定義,那么它只是作為enter來(lái)用。

  AST會(huì)從上到下執(zhí)行,我們先拿到類名的名字與父類的名字,我們定義一個(gè)modules的對(duì)象,保存信息。

  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;

  }

  },

  我們?cè)诘诙卧L問(wèn)這個(gè)類定義時(shí),要將類定義轉(zhuǎn)換為函數(shù)調(diào)用。即

  Class AAA extends Component ---> Component({})

  實(shí)現(xiàn)如下,將原來(lái)的類刪掉(因此才在exit時(shí)執(zhí)行),然后新建一個(gè)函數(shù)調(diào)用語(yǔ)句。我們可以通過(guò)babel-types這個(gè)句實(shí)現(xiàn)。具體看這里。比如說(shuō):

  const call = t.expressionStatement(

  t.callExpression(t.identifier("Component"), [ t.objectExpression([])])

  );

  path.replaceWith(call);

  就能產(chǎn)生如下代碼,將我們的類定義從原位置替換掉。

  Component({})

  但我們不能是一個(gè)空對(duì)象啊,因此我們需要收集它的方法。

  我們需要在visitors對(duì)象添加一個(gè)ClassMethod處理器,收集原來(lái)類的方法。類的方法與對(duì)象的方法不一樣,對(duì)象的方法叫成員表達(dá)式,需要轉(zhuǎn)換一下。我們首先弄一個(gè)數(shù)組,用來(lái)放東西。

  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)

  }

  }

  然后我們?cè)贑lassDeclaration或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(){}

  })

  到這里,我們開(kāi)始另一個(gè)問(wèn)題了。小程序雖然是抄React,但又想別出心裁,于是一些屬性與方法是不一樣的。比如說(shuō)data對(duì)應(yīng)state, setData對(duì)應(yīng)setState,早期的版本還有forceUpdate之類的。data對(duì)應(yīng)一個(gè)對(duì)象,你可以有千奇百怪的寫法。

  this.state ={ a: 1}

  this["state"] = {b: 1};

  this.state = {}

  this.state.aa = 1

  你想hold住這么多奇怪的寫法是很困難的,因此我們可以對(duì)constructor方法做些處理,然后其他方法做些約束,來(lái)減少轉(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)參這里,本文就不貼上來(lái)了。

  RubyLouvre/anu

  那this.setState怎么轉(zhuǎn)換成this.setData呢。這是一個(gè)函數(shù)調(diào)用,語(yǔ)法上稱之為**CallExpression**。我們?cè)趘isitors上定義同名的處理器。

  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類定義轉(zhuǎn)換成Component({})調(diào)用方式 成功了。剩下就是將import語(yǔ)句處理一下,因?yàn)橐〕绦蛑?,如果這個(gè)組件引用了其他組件,需要在其json中添加useComponens對(duì)象,將這些組件名及鏈接寫上去。換言之,小程序太懶了,處處都要手動(dòng)。有了React轉(zhuǎn)碼器,這些事可以省掉。

  其次是render方法的轉(zhuǎn)換,怎么變成一個(gè)wxml文件呢,`{}單花括號(hào)的內(nèi)容要轉(zhuǎn)換成`"{{}}"`雙引號(hào)+雙花括號(hào) ,wx:if, wx:for的模擬等等,且聽(tīng)下回分解。

重磅推薦:小程序開(kāi)店目錄

第一部分:小商店是什么

第二部分:如何開(kāi)通一個(gè)小商店

第三部分:如何登錄小商店

第四部分:開(kāi)店任務(wù)常見(jiàn)問(wèn)題

第五部分:小商店可以賣什么

第六部分:HiShop小程序特色功能

第七部分:小程序直播

第八部分:小程序收貨/物流

第九部分:小程序怎么結(jié)算

第十部分:小程序客服

第十一部分:電商創(chuàng)業(yè)

第十二部分:小程序游戲開(kāi)發(fā)