Taro(二)

四、CLI 原理及不同端的運(yùn)行機(jī)制

4.1 taro-cli 包

4.1.1 Taro 命令

taro-cli 包位于 Taro 工程的 Packages 目錄下,通過(guò) npm install -g @tarojs/cli 全局安裝后,將會(huì)生成一個(gè) Taro 命令。主要負(fù)責(zé)項(xiàng)目初始化、編譯、構(gòu)建等。直接在命令行輸入 Taro ,會(huì)看到如下提示…

? taro
?? Taro v0.0.63


  Usage: taro <command> [options]

  Options:

    -V, --version       output the version number
    -h, --help          output usage information

  Commands:

    init [projectName]  Init a project with default templete
    build               Build a project with options
    update              Update packages of taro
    help [cmd]          display help for [cmd]...

里面包含了 Taro 所有命令用法及作用。

4.1.2 包管理與發(fā)布

  • 首先,我們需要了解 taro-cli 包與 Taro 工程的關(guān)系。
  • 將 Taro 工程 Clone 之后,可以看到工程的目錄結(jié)構(gòu)如下,整體結(jié)構(gòu)還是比較清晰的:
.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build
├── docs
├── lerna-debug.log
├── lerna.json        // Lerna 配置文件
├── package.json
├── packages
│   ├── eslint-config-taro
│   ├── eslint-plugin-taro
│   ├── postcss-plugin-constparse
│   ├── postcss-pxtransform
│   ├── taro
│   ├── taro-async-await
│   ├── taro-cli
│   ├── taro-components
│   ├── taro-components-rn
│   ├── taro-h5
│   ├── taro-plugin-babel
│   ├── taro-plugin-csso
│   ├── taro-plugin-sass
│   ├── taro-plugin-uglifyjs
│   ├── taro-redux
│   ├── taro-redux-h5
│   ├── taro-rn
│   ├── taro-rn-runner
│   ├── taro-router
│   ├── taro-transformer-wx
│   ├── taro-weapp
│   └── taro-webpack-runner
└── yarn.lock...

Taro 項(xiàng)目主要是由一系列 NPM 包組成,位于工程的 Packages 目錄下。它的包管理方式和 Babel 項(xiàng)目一樣,將整個(gè)項(xiàng)目作為一個(gè) monorepo 來(lái)進(jìn)行管理,并且同樣使用了包管理工具 Lerna

Packages 目錄下十幾個(gè)包中,最常用的項(xiàng)目初始化與構(gòu)建的命令行工具 Taro CLI 就是其中一個(gè)。在 Taro 工程根目錄運(yùn)行 lerna publish 命令之后,lerna.json 里面配置好的所有的包會(huì)被發(fā)布到 NPM 上

4.1.3 taro-cli 包的目錄結(jié)構(gòu)如下

./
├── bin        // 命令行
│   ├── taro              // taro 命令
│   ├── taro-build        // taro build 命令
│   ├── taro-update       // taro update 命令
│   └── taro-init         // taro init 命令
├── package.json
├── node_modules
├── src
│   ├── build.js        // taro build 命令調(diào)用,根據(jù) type 類型調(diào)用不同的腳本
│   ├── config
│   │   ├── babel.js        // Babel 配置
│   │   ├── babylon.js      // JavaScript 解析器 babylon 配置
│   │   ├── browser_list.js // autoprefixer browsers 配置
│   │   ├── index.js        // 目錄名及入口文件名相關(guān)配置
│   │   └── uglify.js
│   ├── creator.js
│   ├── h5.js       // 構(gòu)建h5 平臺(tái)代碼
│   ├── project.js  // taro init 命令調(diào)用,初始化項(xiàng)目
│   ├── rn.js       // 構(gòu)建React Native 平臺(tái)代碼
│   ├── util        // 一系列工具函數(shù)
│   │   ├── index.js
│   │   ├── npm.js
│   │   └── resolve_npm_files.js
│   └── weapp.js        // 構(gòu)建小程序代碼轉(zhuǎn)換
├── templates           // 腳手架模版
│   └── default
│       ├── appjs
│       ├── config
│       │   ├── dev
│       │   ├── index
│       │   └── prod
│       ├── editorconfig
│       ├── eslintrc
│       ├── gitignor...

通過(guò)上面的目錄樹可以發(fā)現(xiàn),taro-cli 工程的文件并不算多,主要目錄有:/bin、/src、/template

4.2 用到的核心庫(kù)

  • tj/commander.js Node.js - 命令行接口全面的解決方案
  • jprichardson/node-fs-extra - 在 Node.js 的 fs 基礎(chǔ)上增加了一些新的方法,更好用,還可以拷貝模板。
  • chalk/chalk - 可以用于控制終端輸出字符串的樣式
  • SBoudrias/Inquirer.js - Node.js 命令行交互工具,通用的命令行用戶界面集合,可以和用戶進(jìn)行交互
  • sindresorhus/ora - 實(shí)現(xiàn)加載中的狀態(tài)是一個(gè) Loading 加前面轉(zhuǎn)起來(lái)的小圈圈,成功了是一個(gè) Success 加前面一個(gè)小鉤鉤
  • SBoudrias/mem-fs-editor - 提供一系列 API,方便操作模板文件
  • shelljs/shelljs - ShellJS 是 Node.js 擴(kuò)展,用于實(shí)現(xiàn) Unix shell 命令執(zhí)行。

4.3 Taro Init

image

當(dāng)我們?nèi)职惭b taro-cli 包之后,我們的命令行里就有了 Taro 命令

  • 那么 Taro 命令是怎樣添加進(jìn)去的呢?其原因在于 package.json 里面的 bin 字段:
"bin": {
    "taro": "bin/taro"
}

上面代碼指定,Taro 命令對(duì)應(yīng)的可執(zhí)行文件為 bin/taro 。NPM 會(huì)尋找這個(gè)文件,在 [prefix]/bin 目錄下建立符號(hào)鏈接。在上面的例子中,Taro 會(huì)建立符號(hào)鏈接 [prefix]/bin/taro。由于 [prefix]/bin 目錄會(huì)在運(yùn)行時(shí)加入系統(tǒng)的 PATH 變量,因此在運(yùn)行 NPM 時(shí),就可以不帶路徑,直接通過(guò)命令來(lái)調(diào)用這些腳本。

  • 關(guān)于 prefix,可以通過(guò) npm config get prefix 獲取。
$ npm config get prefix
/usr/local

通過(guò)下列命令可以更加清晰的看到它們之間的符號(hào)鏈接…

$ ls -al `which taro`
lrwxr-xr-x  1 chengshuai  admin  40  6 15 10:51 /usr/local/bin/taro -> ../lib/node_modules/@tarojs/cli/bin/taro...

4.3.1 命令關(guān)聯(lián)與參數(shù)解析

這里就不得不提到一個(gè)有用的包:tj/commander.js ,Node.js 命令行接口全面的解決方案,靈感來(lái)自于 Ruby’s commander。可以自動(dòng)的解析命令和參數(shù),合并多選項(xiàng),處理短參等等,功能強(qiáng)大,上手簡(jiǎn)單

更主要的,commander 支持 Git 風(fēng)格的子命令處理,可以根據(jù)子命令自動(dòng)引導(dǎo)到以特定格式命名的命令執(zhí)行文件,文件名的格式是 [command]-[subcommand],例如

  • taro init => taro-init
  • taro build => taro-build
  • /bin/taro 文件內(nèi)容不多,核心代碼也就那幾行 .command() 命令:
#! /usr/bin/env node

const program = require('commander')
const {getPkgVersion} = require('../src/util')

program
  .version(getPkgVersion())
  .usage('<command> [options]')
  .command('init [projectName]', 'Init a project with default templete')
  .command('build', 'Build a project with options')
  .command('update', 'Update packages of taro')
  .parse(process.argv)...

通過(guò)上面代碼可以發(fā)現(xiàn),init,build ,update 等命令都是通過(guò).command(name, description)方法定義的,然后通過(guò) .parse(arg) 方法解析參數(shù)

4.3.2 參數(shù)解析及與用戶交互

  • commander 包可以自動(dòng)解析命令和參數(shù),在配置好命令之后,還能夠自動(dòng)生成 help(幫助)命令和 version(版本查看) 命令。并且通過(guò) program.args 便可以獲取命令行的參數(shù),然后再根據(jù)參數(shù)來(lái)調(diào)用不同的腳本。
  • 但當(dāng)我們運(yùn)行 taro init 命令后,如下所示的命令行交互又是怎么實(shí)現(xiàn)的呢?…
$ taro init taroDemo
Taro 即將創(chuàng)建一個(gè)新項(xiàng)目!
Need help? Go and open issue: https://github.com/NervJS/taro/issues/new

Taro v0.0.50

? 請(qǐng)輸入項(xiàng)目介紹!
? 請(qǐng)選擇模板 默認(rèn)模板...

這里使用的是 SBoudrias/Inquirer.js 來(lái)處理命令行交互。

用法其實(shí)很簡(jiǎn)單

const inquirer = require('inquirer')  // npm i inquirer -D

if (typeof conf.description !== 'string') {
      prompts.push({
        type: 'input',
        name: 'description',
        message: '請(qǐng)輸入項(xiàng)目介紹!'
      })
}...
  • prompt()接受一個(gè)問(wèn)題對(duì)象的數(shù)據(jù),在用戶與終端交互過(guò)程中,將用戶的輸入存放在一個(gè)答案對(duì)象中,然后返回一個(gè) Promise,通過(guò) then()獲取到這個(gè)答案對(duì)象。
    借此,新項(xiàng)目的名稱、版本號(hào)、描述等信息可以直接通過(guò)終端交互插入到項(xiàng)目模板中,完善交互流程。
  • 當(dāng)然,交互的問(wèn)題不僅限于此,可以根據(jù)自己項(xiàng)目的情況,添加更多的交互問(wèn)題。inquirer.js 強(qiáng)大的地方在于,支持很多種交互類型,除了簡(jiǎn)單的 input,還有 confirm、list、password、checkbox 等,具體可以參見項(xiàng)目的工程 README。
    此外,你在執(zhí)行異步操作的過(guò)程中,還可以使用 sindresorhus/ora 來(lái)添加一下 Loading 效果。使用 chalk/chalk 給終端的輸出添加各種樣式…

4.3.3 模版文件操作

最后就是模版文件操作了,主要分為兩大塊:

  • 將輸入的內(nèi)容插入到模板中
  • 根據(jù)命令創(chuàng)建對(duì)應(yīng)目錄結(jié)構(gòu),copy 文件
  • 更新已存在文件內(nèi)容

這些操作基本都是在 /template/index.js 文件里。
這里還用到了 shelljs/shelljs 執(zhí)行 shell 腳本,如初始化 Git: git init,項(xiàng)目初始化之后安裝依賴 npm install 等

拷貝模板文件

拷貝模版文件主要是使用 jprichardson/node-fs-extra 的 copyTpl()方法,此方法使用 ejs 模板語(yǔ)法,可以將輸入的內(nèi)容插入到模版的對(duì)應(yīng)位置:

this.fs.copyTpl(
  project,
  path.join(projectPath, 'project.config.json'),
  {description, projectName}
);...

4.4 Taro Build

  • taro build 命令是整個(gè) Taro 項(xiàng)目的靈魂和核心,主要負(fù)責(zé)多端代碼編譯(H5,小程序,React Native 等)。
  • Taro 命令的關(guān)聯(lián),參數(shù)解析等和 taro init 其實(shí)是一模一樣的,那么最關(guān)鍵的代碼轉(zhuǎn)換部分是怎樣實(shí)現(xiàn)的呢?…

4.4.1 編譯工作流與抽象語(yǔ)法樹(AST)

Taro 的核心部分就是將代碼編譯成其他端(H5、小程序、React Native 等)代碼。一般來(lái)說(shuō),將一種結(jié)構(gòu)化語(yǔ)言的代碼編譯成另一種類似的結(jié)構(gòu)化語(yǔ)言的代碼包括以下幾個(gè)步驟

image

首先是 Parse,將代碼解析(Parse)成抽象語(yǔ)法樹(Abstract Syntex Tree),然后對(duì) AST 進(jìn)行遍歷(traverse)和替換(replace)(這對(duì)于前端來(lái)說(shuō)其實(shí)并不陌生,可以類比 DOM 樹的操作),最后是生成(generate),根據(jù)新的 AST 生成編譯后的代碼…

4.4.2 Babel 模塊

Babel 是一個(gè)通用的多功能的 JavaScript 編譯器,更確切地說(shuō)是源碼到源碼的編譯器,通常也叫做轉(zhuǎn)換編譯器(transpiler)。 意思是說(shuō)你為 Babel 提供一些 JavaScript 代碼,Babel 更改這些代碼,然后返回給你新生成的代碼…

4.4.3 解析頁(yè)面 Config 配置

在業(yè)務(wù)代碼編譯成小程序的代碼過(guò)程中,有一步是將頁(yè)面入口 JS 的 Config 屬性解析出來(lái),并寫入 *.json 文件,供小程序使用。那么這一步是怎么實(shí)現(xiàn)的呢?這里將這部分功能的關(guān)鍵代碼抽取出來(lái):

// 1. babel-traverse方法, 遍歷和更新節(jié)點(diǎn)
traverse(ast, {
  ClassProperty(astPath) { // 遍歷類的屬性聲明
    const node = astPath.node
    if (node.key.name === 'config') { // 類的屬性名為 config
      configObj = traverseObjectNode(node)
      astPath.remove() // 將該方法移除掉
    }
  }
})

// 2. 遍歷,解析為 JSON 對(duì)象
function traverseObjectNode(node, obj) {
  if (node.type === 'ClassProperty' || node.type === 'ObjectProperty') {
    const properties = node.value.properties
      obj = {}
      properties.forEach((p, index) => {
        obj[p.key.name] = traverseObjectNode(p.value)
      })
      return obj
  }
  if (node.type === 'ObjectExpression') {
    const properties = node.properties
    obj = {}
    properties.forEach((p, index) => {
      // const t = require('babel-types')  AST 節(jié)點(diǎn)的 Lodash 式工具庫(kù)
      const key = t.isIdentifier(p.key) ? p.key.name : p.key.value
      obj[key] = traverseObjectNode(p.value)
    })
    return obj
  }
  if (node.type === 'ArrayExpression') {
    return node.elements.map(item => traverseObjectNode(item))
 ...

五、Taro 組件庫(kù)及 API 的設(shè)計(jì)與適配

5.1 多端差異

5.1.1 組件差異

小程序、H5 以及快應(yīng)用都可以劃分為 XML 類,React Native 歸為 JSX 類,兩種語(yǔ)言風(fēng)牛馬不相及,給適配設(shè)置了非常大的障礙。XML 類有個(gè)明顯的特點(diǎn)是關(guān)注點(diǎn)分離(Separation of Concerns),即語(yǔ)義層(XML)、視覺層(CSS)、交互層(JavaScript)三者分離的松耦合形式,JSX 類則要把三者混為一體,用腳本來(lái)包攬三者的工作…

不同端的組件的差異還體現(xiàn)在定制程度上

  • H5 標(biāo)簽(組件)提供最基礎(chǔ)的功能——布局、表單、媒體、圖形等等;
  • 小程序組件相對(duì) H5 有了一定程度的定制,我們可以把小程序組件看作一套類似于 H5 的 UI 組件庫(kù);
  • React Native 端組件也同樣如此,而且基本是?!敖M”專用的,比如要觸發(fā)點(diǎn)擊事件就得用 Touchable 或者 Text 組件,要渲染文本就得用 Text 組件(雖然小程序也提供了 Text 組件,但它的文本仍然可以直接放到 view 之類的組件里)…

5.1.2 API 差異

各端 API 的差異具有定制化、接口不一、能力限制的特點(diǎn)

  • 定制化:各端所提供的 API 都是經(jīng)過(guò)量身打造的,比如小程序的開放接口類 API,完全是針對(duì)小程序所處的微信環(huán)境打造的,其提供的功能以及外在表現(xiàn)都已由框架提供實(shí)現(xiàn),用戶上手可用,毋須關(guān)心內(nèi)部實(shí)現(xiàn)。
  • 接口不一:相同的功能,在不同端下的調(diào)用方式以及調(diào)用參數(shù)等也不一樣,比如 socket,小程序中用 wx.connectSocket 來(lái)連接,H5 則用 new WebSocket() 來(lái)連接,這樣的例子我們可以找到很多個(gè)。
  • 能力限制:各端之間的差異可以進(jìn)行定制適配,然而并不是所有的 API(此處特指小程序 API,因?yàn)槎喽诉m配是向小程序看齊的)在各個(gè)端都能通過(guò)定制適配來(lái)實(shí)現(xiàn),因?yàn)椴煌怂芴峁┑亩四芰Α按螽愋⊥?,這是在適配過(guò)程中不可抗拒、不可抹平的差異…

5.2 多端適配

5.2.1 樣式處理

H5 端使用官方提供的 WEUI 進(jìn)行適配,React Native 端則在組件內(nèi)添加樣式,并通過(guò)腳本來(lái)控制一些狀態(tài)類的樣式,框架核心在編譯的時(shí)候把源代碼的 class 所指向的樣式通過(guò) css-to-react-native 進(jìn)行轉(zhuǎn)譯,所得 StyleSheet 樣式傳入組件的 style 參數(shù),組件內(nèi)部會(huì)對(duì)樣式進(jìn)行二次處理,得到最終的樣式…

image

為什么需要對(duì)樣式進(jìn)行二次處理?

部分組件是直接把傳入 style 的樣式賦給最外層的 React Native 原生組件,但部分經(jīng)過(guò)層層封裝的組件則不然,我們要把容器樣式、內(nèi)部樣式和文本樣式離析。為了方便解釋,我們把這類組件簡(jiǎn)化為以下的形式:

<View style={wrapperStyle}>
  <View style={containerStyle}>
    <Text style={textStyle}>Hello World</Text>
  </View>
</View>

假設(shè)組件有樣式 margin-top、background-color 和 font-size,轉(zhuǎn)譯傳入組件后,就要把分別把它們傳到 wrapperStyle、containerStyle 和 textStyle,可參考 ScrollView 的 style 和 contentContainerStyle…

5.2.2 組件封裝

組件的封裝則是一個(gè)“仿制”的過(guò)程,利用端提供的原材料,加工成通用的組件,暴露相對(duì)統(tǒng)一的調(diào)用方式。我們用 <Button /> 這個(gè)組件來(lái)舉例,在小程序端它也許是長(zhǎng)這樣子的

<button
  size="mini"
  plain={{ plain }}
  loading={{ loading }}
  hover-class="you-hover-me"
></button>

如果要實(shí)現(xiàn) H5 端這么一個(gè)按鈕,大概會(huì)像下面這樣,在組件內(nèi)部把小程序的按鈕特性實(shí)現(xiàn)一遍,然后暴露跟小程序一致的調(diào)用方式,就完成了 H5 端一個(gè)組件的設(shè)計(jì)

<button
  {...omit(this.props, ['hoverClass', 'onTouchStart', 'onTouchEnd'])}
  className={cls}
  style={style}
  onClick={onClick}
  disabled={disabled}
  onTouchStart={_onTouchStart}
  onTouchEnd={_onTouchEnd}
>
  {loading && <i class='weui-loading' />}
  {children}
</button>...
  • 其他端的組件適配相對(duì) H5 端來(lái)說(shuō)會(huì)更曲折復(fù)雜一些,因?yàn)?H5 跟小程序的語(yǔ)言較為相似,而其他端需要整合特定端的各種組件,以及利用端組件的特性來(lái)實(shí)現(xiàn),比如在 React Native 中實(shí)現(xiàn)這個(gè)按鈕,則需要用到 <Touchable* />、<View />、<Text />,要實(shí)現(xiàn)動(dòng)畫則需要用上 <Animated.View />,還有就是相對(duì)于 H5 和小程序比較容易實(shí)現(xiàn)的 touch 事件,在 React Native 中則需要用上 PanResponder 來(lái)進(jìn)行“仿真”,總之就是,因“端”制宜,一切為了最后只需一行代碼通行多端!
  • 除了屬性支持外,事件回調(diào)的參數(shù)也需要進(jìn)行統(tǒng)一,為此,需要在內(nèi)部進(jìn)行處理,比如 Input 的 onInput 事件,需要給它造一個(gè)類似小程序相同事件的回調(diào)參數(shù),比如 { target: { value: text }, detail: { value: text } },這樣,開發(fā)者們就可以像下面這樣處理回調(diào)事件,無(wú)需關(guān)心中間發(fā)生了什么…
function onInputHandler({ target, detail }) {
  console.log(target.value, detail.value);
}

六、JSX 轉(zhuǎn)換微信小程序模板的實(shí)現(xiàn)

6.1 代碼的本質(zhì)

不管是任意語(yǔ)言的代碼,其實(shí)它們都有兩個(gè)共同點(diǎn)

  • 它們都是由字符串構(gòu)成的文本
  • 它們都要遵循自己的語(yǔ)言規(guī)范

第一點(diǎn)很好理解,既然代碼是字符串構(gòu)成的,我們要修改/編譯代碼的最簡(jiǎn)單的方法就是使用字符串的各種正則表達(dá)式。例如我們要將 JSON 中一個(gè)鍵名 foo 改為 bar,只要寫一個(gè)簡(jiǎn)單的正則表達(dá)式就能做到:

jsonStr.replace(/(?<=")foo(?="\s*:)/i, 'bar')...

編譯就是把一段字符串改成另外一段字符串

6.2 Babel

JavaScript 社區(qū)其實(shí)有非常多 parser 實(shí)現(xiàn),比如 Acorn、Esprima、Recast、Traceur、Cherow 等等。但我們還是選擇使用 Babel,主要有以下幾個(gè)原因

  • Babel 可以解析還沒(méi)有進(jìn)入 ECMAScript 規(guī)范的語(yǔ)法。例如裝飾器這樣的提案,雖然現(xiàn)在沒(méi)有進(jìn)入標(biāo)準(zhǔn)但是已經(jīng)廣泛使用有一段時(shí)間了;
  • Babel 提供插件機(jī)制解析 TypeScript、Flow、JSX 這樣的 JavaScript 超集,不必單獨(dú)處理這些語(yǔ)言;
  • Babel 擁有龐大的生態(tài),有非常多的文檔和樣例代碼可供參考;
  • 除去 parser 本身,Babel 還提供各種方便的工具庫(kù)可以優(yōu)化、生成、調(diào)試代碼…

Babylon( @babel/parser)

Babylon 就是 Babel 的 parser。它可以把一段符合規(guī)范的 JavaScript 代碼輸出成一個(gè)符合 Esprima 規(guī)范的 AST。 大部分 parser 生成的 AST 數(shù)據(jù)結(jié)構(gòu)都遵循 Esprima 規(guī)范,包括 ESLint 的 parser ESTree。這就意味著我們熟悉了 Esprima 規(guī)范的 AST 數(shù)據(jù)結(jié)構(gòu)還能去寫 ESLint 插件

我們可以嘗試解析 n * n 這句簡(jiǎn)單的表達(dá)式:

import * as babylon from "babylon";
const code = `n * n`;
babylon.parse(code);...

最終 Babylon 會(huì)解析成這樣的數(shù)據(jù)結(jié)構(gòu):


image

你也可以使用 ASTExploroer 快速地查看代碼的 AST

Babel-traverse (@babel/traverse)

babel-traverse 可以遍歷由 Babylon 生成的抽象語(yǔ)法樹,并把抽象語(yǔ)法樹的各個(gè)節(jié)點(diǎn)從拓?fù)鋽?shù)據(jù)結(jié)構(gòu)轉(zhuǎn)化成一顆路徑(Path)樹,Path 表示兩個(gè)節(jié)點(diǎn)之間連接的響應(yīng)式(Reactive)對(duì)象,它擁有添加、刪除、替換節(jié)點(diǎn)等方法。當(dāng)你調(diào)用這些修改樹的方法之后,路徑信息也會(huì)被更新。除此之外,Path 還提供了一些操作作用域(Scope) 和標(biāo)識(shí)符綁定(Identifier Binding) 的方法可以去做處理一些更精細(xì)復(fù)雜的需求??梢哉f(shuō) babel-traverse 是使用 Babel 作為編譯器最核心的模塊…

讓我們嘗試一下把一段代碼中的 n _ n 變?yōu)?x _ x

import * as babylon from "@babel/parser";
import traverse from "babel-traverse";

const code = `function square(n) {
  return n * n;
}`;

const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {
      path.node.name = "x";
    }
  }
});...

Babel-types(@babel/types)

babel-types 是一個(gè)用于 AST 節(jié)點(diǎn)的 Lodash 式工具庫(kù),它包含了構(gòu)造、驗(yàn)證以及變換 AST 節(jié)點(diǎn)的方法。 該工具庫(kù)包含考慮周到的工具方法,對(duì)編寫處理 AST 邏輯非常有用。例如我們之前在 babel-traverse 中改變標(biāo)識(shí)符 n 的代碼可以簡(jiǎn)寫為:

import traverse from "babel-traverse";
import * as t from "babel-types";

traverse(ast, {
  enter(path) {
    if (t.isIdentifier(path.node, { name: "n" })) {
      path.node.name = "x";
    }
  },
});

可以發(fā)現(xiàn)使用 babel-types 能提高我們轉(zhuǎn)換代碼的可讀性,在配合 TypeScript 這樣的靜態(tài)類型語(yǔ)言后,babel-types 的方法還能提供類型校驗(yàn)的功能,能有效地提高我們轉(zhuǎn)換代碼的健壯性和可靠性…

6.3 實(shí)踐例子

以一個(gè)簡(jiǎn)單 Page 頁(yè)面為例:

import Taro, { Component } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'

class Home extends Component {

  config = {
    navigationBarTitleText: '首頁(yè)'
  }

  state = {
    numbers: [1, 2, 3, 4, 5]
  }

  handleClick = () => {
    this.props.onTest()
  }

  render () {
    const oddNumbers = this.state.numbers.filter(number => number & 2)
    return (
      <ScrollView className='home' scrollTop={false}>
        奇數(shù):
        {
          oddNumbers.map(number => <Text onClick={this.handleClick}>{number}</Text>)
        }
        偶數(shù):
        {
          numbers.map(number => number % 2 === 0 && <Text onClick={this.handleClick}>{number}</Text>)
        }
      </ScrollView>
    )
  }
}...

6.3.1 設(shè)計(jì)思路

  • Taro 的結(jié)構(gòu)主要分兩個(gè)方面:運(yùn)行時(shí)和編譯時(shí)。運(yùn)行時(shí)負(fù)責(zé)把編譯后到代碼運(yùn)行在本不能運(yùn)行的對(duì)應(yīng)環(huán)境中,你可以把 Taro 運(yùn)行時(shí)理解為前端開發(fā)當(dāng)中 polyfill。舉例來(lái)說(shuō),小程序新建一個(gè)頁(yè)面是使用 Page 方法傳入一個(gè)字面量對(duì)象,并不支持使用類。如果全部依賴編譯時(shí)的話,那么我們要做到事情大概就是把類轉(zhuǎn)化成對(duì)象,把 state 變?yōu)?data,把生命周期例如 componentDidMount 轉(zhuǎn)化成 onReady,把事件由可能的類函數(shù)(Class method)和類屬性函數(shù)(Class property function) 轉(zhuǎn)化成字面量對(duì)象方法(Object property function)等等。
  • 但這顯然會(huì)讓我們的編譯時(shí)工作變得非常繁重,在一個(gè)類異常復(fù)雜時(shí)出錯(cuò)的概率也會(huì)變高。但我們有更好的辦法:實(shí)現(xiàn)一個(gè) createPage 方法,接受一個(gè)類作為參數(shù),返回一個(gè)小程序 Page 方法所需要的字面量對(duì)象。這樣不僅簡(jiǎn)化了編譯時(shí)的工作,我們還可以在 createPage 對(duì)編譯時(shí)產(chǎn)出的類做各種操作和優(yōu)化。通過(guò)運(yùn)行時(shí)把工作分離了之后,再編譯時(shí)我們只需要在文件底部加上一行代碼 Page(createPage(componentName)) 即可…
image
  • 回到一開始那段代碼,我們定義了一個(gè)類屬性 config,config 是一個(gè)對(duì)象表達(dá)式(Object Expression),這個(gè)對(duì)象表達(dá)式只接受鍵值為標(biāo)識(shí)符(Identifier)或字符串,而鍵名只能是基本類型。這樣簡(jiǎn)單的情況我們只需要把這個(gè)對(duì)象表達(dá)式轉(zhuǎn)換為 JSON 即可。另外一個(gè)類屬性 state 在 Page 當(dāng)中有點(diǎn)像是小程序的 data,但它在多數(shù)情況不是完整的 data。這里我們不用做過(guò)多的操作,babel 的插件 transform-class-proerties 會(huì)把它編譯到類的構(gòu)造器中。函數(shù) handleClick 我們交給運(yùn)行時(shí)處理,有興趣的同學(xué)可以跳到 Taro 運(yùn)行時(shí)原理查看具體技術(shù)細(xì)節(jié)。
  • 再來(lái)看我們的 render()函數(shù),它的第一行代碼通過(guò) filter 把數(shù)字?jǐn)?shù)組的所有偶數(shù)項(xiàng)都過(guò)濾掉,真正用來(lái)循環(huán)的是 oddNumbers,而 oddNumbers 并沒(méi)有在 this.state 中,所以我們必須手動(dòng)把它加入到 this.state。和 React 一樣,Taro 每次更新都會(huì)調(diào)用 render 函數(shù),但和 React 不同的是,React 的 render 是一個(gè)創(chuàng)建虛擬 DOM 的方法,而 Taro 的 render 會(huì)被重命名為 _createData,它是一個(gè)創(chuàng)建數(shù)據(jù)的方法:在 JSX 使用過(guò)的數(shù)據(jù)都在這里被創(chuàng)建最后放到小程序 Page 或 Component 工廠方法中的 data。最終我們的 render 方法會(huì)被編譯為…
_createData() {
  this.__state = arguments[0] || this.state || {};
  this.__props = arguments[1] || this.props || {};

  const oddNumbers = this.__state.numbers.filter(number => number & 2);
  Object.assign(this.__state, {
    oddNumbers: oddNumbers
  });
  return this.__state;
}...

6.3.2 WXML 和 JSX

在 Taro 里 render 的所有 JSX 元素都會(huì)在 JavaScript 文件中被移除,它們最終將會(huì)編譯成小程序的 WXML。每個(gè) WXML 元素和 HTML 元素一樣,我們可以把它定義為三種類型:Element、Text、Comment。其中 Text 只有一個(gè)屬性: 內(nèi)容(content),它對(duì)應(yīng)的 AST 類型是 JSXText,我們只需要將前文源碼中對(duì)應(yīng)字符串的奇數(shù)和偶數(shù)轉(zhuǎn)換成 Text 即可。而對(duì)于 Comment 而言我們可以將它們?nèi)壳宄?,不參與 WXML 的編譯。Element 類型有它的名字(tagName)、children、屬性(attributes),其中 children 可能是任意 WXML 類型,屬性是一個(gè)對(duì)象,鍵值和鍵名都是字符串。我們將把重點(diǎn)放在如何轉(zhuǎn)換成為 WXML 的 Element 類型。

首先我們可以先看 <View className='home'>,它在 AST 中是一個(gè) JSXElement,它的結(jié)構(gòu)和我們定義 Element 類型差不多。我們先將 JSXElement 的 ScrollView 從駝峰式的 JSX 命名轉(zhuǎn)化為短橫線(kebab case)風(fēng)格,className 和 scrollTop 的值分別代表了 JSXAttribute 值的兩種類型:StringLiteral 和 JSXExpressionContainer,className 是簡(jiǎn)單的 StringLiteral 處理起來(lái)很方便,scrollTop 處理起來(lái)稍微麻煩點(diǎn),我們需要用兩個(gè)花括號(hào){} 把內(nèi)容包起來(lái)…

接下來(lái)我們?cè)偎伎家幌旅恳粋€(gè) JSXElement 出現(xiàn)的位置,你可以發(fā)現(xiàn)其實(shí)它的父元素只有幾種可能性:return、循環(huán)、條件(邏輯)表達(dá)式。而在上一篇文章中我們提到,babel-traverse 遍歷的 AST 類型是響應(yīng)式的——也就是說(shuō)只要我們按照 JSXElement 父元素類型的順序窮舉處理這幾種可能性,把各種可能性大結(jié)果應(yīng)用到 JSX 元素之后刪除掉原來(lái)的表達(dá)式,最后就可以把一個(gè)復(fù)雜的 JSX 表達(dá)式轉(zhuǎn)換為一個(gè)簡(jiǎn)單的 WXML 數(shù)據(jù)結(jié)構(gòu)?!?/p>

我們先看第一個(gè)循環(huán):

oddNumbers.map(number => <Text onClick={this.handleClick}>{number}</Text>);

Text 的父元素是一個(gè) map 函數(shù)(CallExpression),我們可以把函數(shù)的 callee: oddNumbers 作為 wx:for 的值,并把它放到 state 中,匿名函數(shù)的第一個(gè)參數(shù)是 wx:for-item 的值,函數(shù)的第二個(gè)參數(shù)應(yīng)該是 wx:for-index 的值,但代碼中沒(méi)有傳所以我們可以不管它。然后我們把這兩個(gè) wx: 開頭的參數(shù)作為 attribute 傳入 Text 元素就完成了循環(huán)的處理。而對(duì)于 onClick 而言,在 Taro 中 on 開頭的元素參數(shù)都是事件,所以我們只要把 this. 去掉即可。Text 元素的 children 是一個(gè) JSXExpressionContainer,我們按照之前的處理方式處理即可。最后這行我們生成出來(lái)的數(shù)據(jù)結(jié)構(gòu)應(yīng)該是這樣…

{
  type: 'element',
  tagName: 'text',
  attributes: [
    { bindtap: 'handleClick' },
    { 'wx:for': '{{oddNumbers}}' },
    { 'wx:for-item': 'number' }
  ],
  children: [
    { type: 'text', content: '{{number}}' }
  ]
}...

有了這個(gè)數(shù)據(jù)結(jié)構(gòu)生成一段 WXML 就非常簡(jiǎn)單了

再來(lái)看第二個(gè)循環(huán)表達(dá)式:

numbers.map(number => number % 2 === 0 && <Text onClick={this.handleClick}>{number}</Text>)...

它比第一個(gè)循環(huán)表達(dá)式多了一個(gè)邏輯表達(dá)式(Logical Operators),我們知道 expr1 && expr2 意味著如果 expr1 能轉(zhuǎn)換成 true 則返回 expr2,也就是說(shuō)我們只要把 number % 2 === 0 作為值生成一個(gè)鍵名 wx:if 的 JSXAttribute 即可。但由于 wx:if 和 wx:for 同時(shí)作用于一個(gè)元素可能會(huì)出現(xiàn)問(wèn)題,所以我們應(yīng)該生成一個(gè) block 元素,把 wx:if 掛載到 block 元素,原元素則全部作為 children 傳入 block 元素中。這時(shí) babel-traverse 會(huì)檢測(cè)到新的元素 block,它的父元素是一個(gè) map 循環(huán)函數(shù),因此我們可以按照第一個(gè)循環(huán)表達(dá)式的處理方法來(lái)處理這個(gè)表達(dá)式。

這里我們可以思考一下 this.props.text || this.props.children 的解決方案。當(dāng)用戶在 JSX 中使用 || 作為邏輯表達(dá)式時(shí)很可能是 this.props.text 和 this.props.children 都有可能作為結(jié)果返回。這里 Taro 將它編譯成了 this.props.text ? this.props.text: this.props.children,按照條件表達(dá)式(三元表達(dá)式)的邏輯,也就是說(shuō)會(huì)生成兩個(gè) block,一個(gè) wx:if 和一個(gè) wx:else:

<block wx:if="{{text}}">{{text}}</block>
<block wx:else>
    <slot></slot>
</block>

原文:https://yolkpie.net/2020/10/17/Taro(%E4%BA%8C)/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • taro-convert 是一個(gè)命令行工具,其作用是將原生微信小程序轉(zhuǎn)換成 taro 標(biāo)準(zhǔn)代碼。其主要是通過(guò)src...
    微微笑的蝸牛閱讀 3,086評(píng)論 0 1
  • 七、小程序運(yùn)行時(shí) 為了使 Taro 組件轉(zhuǎn)換成小程序組件并運(yùn)行在小程序環(huán)境下, Taro 主要做了兩個(gè)方面的工作:...
    yolkpie閱讀 1,863評(píng)論 0 0
  • 1. 基礎(chǔ)知識(shí) 1.1 this指針 this指針大概是javascript中最令初學(xué)者困惑的語(yǔ)法了,簡(jiǎn)單說(shuō),th...
    shtonyteng閱讀 4,853評(píng)論 0 3
  • 前言 本文首發(fā)簡(jiǎn)書,源碼看到哪就更新到哪。 項(xiàng)目概覽 lerna taro 用 lerna 做多模塊管理,lern...
    coolzilj閱讀 2,283評(píng)論 0 3
  • 背景 Taro是繼Wepy、Mpvue之后,為解決終端碎片化問(wèn)題的又一框架 Taro安裝 Taro 是一個(gè)基于 N...
    ZoranLee閱讀 551評(píng)論 0 1

友情鏈接更多精彩內(nèi)容