Taro 技術揭祕之taro-cli

文章同步於 Github/Blog

前言

Taro 是由凹凸實驗室打造的一套遵循 React 語法規範的多端統一開發框架。javascript

使用 Taro,咱們能夠只書寫一套代碼,再經過 Taro 的編譯工具,將源代碼分別編譯出能夠在不一樣端(微信小程序、H五、App 端等)運行的代碼。實現 一次編寫,多端運行。 關於 Taro 的更多詳細的信息能夠看官方的介紹文章 Taro - 多端開發框架 ,或者直接前往 GitHub 倉庫 NervJS/taro 查看 Taro 文檔及相關資料。css

image

Taro 項目實現的功能強大,項目複雜而龐大,涉及到的方方面面(多端代碼轉換、組件、路由、狀態管理、生命週期、端能力的實現與兼容等等)多,對於大多數人來講,想要深刻理解其實現機制及原理,仍是比較困難的。html

Taro 技術揭祕系列文章將爲你逐步揭開 Taro 強大的功能以後的神祕面紗,帶領你深刻 Taro 內部,瞭解 Taro 是怎樣一步一步實現 一次編寫,多端運行 的宏偉目標,同時也但願藉此機會拋磚引玉,促進前端圈涌現出更多的,可以解決你們痛點的開源項目。前端

首先,咱們將從負責 Taro 腳手架初始化和項目構建的的命令行工具,也就是 Taro 的入口:@tarojs/cli 開始。vue

taro-cli 包

taro 命令

taro-cli 包位於 Taro 工程的 packages 目錄下,經過 npm install -g @tarojs/cli 全局安裝後,將會生成一個taro 命令。主要負責項目初始化、編譯、構建等。直接在命令行輸入 taro ,會看到以下提示:java

➜ 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 命令用法及做用。node

包管理與發佈

首先,咱們須要瞭解 taro-cli 包與 taro 工程的關係。linux

將 Taro 工程 clone 下來以後,咱們能夠看到工程的目錄結構以下,總體仍是比較簡單明瞭的。webpack

.
├── 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 項目主要是由一系列 npm 包組成,位於工程的 packages 目錄下。它的包管理方式和 Babel 項目同樣,將整個項目做爲一個 monorepo 來進行管理,而且一樣使用了包管理工具 Lernagit

Lerna 是一個用來優化託管在 git/npm 上的多 package 代碼庫的工做流的一個管理工具,可讓你在主項目下管理多個子項目,從而解決了多個包互相依賴,且發佈時須要手動維護多個包的問題。

關於 Lerna 的更多介紹能夠看官方文檔 Lerna:A tool for managing JavaScript projects with multiple packages

packages 目錄下十幾個包中,最經常使用的項目初始化與構建的命令行工具 taro-cli 就是其中一個。在 Taro 工程根目錄運行 lerna publish 命令以後,lerna.json 裏面配置好的全部的包會被髮布到 npm 上去。

目錄結構

taro-cli 包的目錄結構以下:

./
├── bin    // 命令行
│   ├── taro        // taro 命令
│   ├── taro-build        // taro build 命令
│   ├── taro-update        // taro update 命令
│   └── taro-init        // taro init 命令
├── package.json
├── node_modules
├── src
│   ├── build.js        // taro build 命令調用,根據 type 類型調用不一樣的腳本
│   ├── config
│   │   ├── babel.js    // Babel 配置
│   │   ├── babylon.js        // JavaScript 解析器 babylon 配置
│   │   ├── browser_list.js    // autoprefixer browsers 配置
│   │   ├── index.js        // 目錄名及入口文件名相關配置
│   │   └── uglify.js
│   ├── creator.js
│   ├── h5.js        // 構建h5 平臺代碼
│   ├── project.js    // taro init 命令調用,初始化項目
│   ├── rn.js        // 構建React Native 平臺代碼
│   ├── util        // 一系列工具函數
│   │   ├── index.js
│   │   ├── npm.js
│   │   └── resolve_npm_files.js
│   └── weapp.js        // 構建小程序代碼轉換
├── templates        // 腳手架模版
│   └── default
│       ├── appjs
│       ├── config
│       │   ├── dev
│       │   ├── index
│       │   └── prod
│       ├── editorconfig
│       ├── eslintrc
│       ├── gitignore
│       ├── index.js    // 初始化文件及目錄,copy模版等
│       ├── indexhtml
│       ├── npmrc
│       ├── pagejs
│       ├── pkg
│       └── scss
└── yarn-error.log

其中關鍵文件的做用已添加註釋說明,你們能夠先大概看看,有個初步印象。

經過上面的目錄樹能夠看出,taro-cli 工程的文件並不算多,主要目錄有:/bin/src/template,我已經在上面詳細標註了主要的目錄和文件的做用,至於具體的流程,我們接下來再分析。

用到的核心庫

  • tj/commander.js Node.js 命令行接口全面的解決方案,靈感來自於 Ruby's commander。能夠自動的解析命令和參數,合併多選項,處理短參等等,功能強大,上手簡單。
  • jprichardson/node-fs-extra 在nodejs的fs基礎上增長了一些新的方法,更好用,還能夠拷貝模板。
  • chalk/chalk 能夠用於控制終端輸出字符串的樣式。
  • SBoudrias/Inquirer.js NodeJs 命令行交互工具,通用的命令行用戶界面集合,用於和用戶進行交互。
  • sindresorhus/ora 加載中狀態表示的時候一個loading怎麼夠,再在前面加個小圈圈轉起來,成功了console一個success怎麼夠,前面還能夠給他加個小鉤鉤,ora就是作這個的。
  • SBoudrias/mem-fs-editor 提供一系列API,方便操做模板文件。
  • shelljs/shelljs ShellJS 是Node.js 擴展,用於實現Unix shell 命令執行。
  • Node.js child_process 模塊 用於新建子進程。子進程的運行結果儲存在系統緩存之中(最大200KB),等到子進程運行結束之後,主進程再用回調函數讀取子進程的運行結果。

taro init

taro init 命令主要的流程以下:

image

taro 命令入口

當咱們全局安裝 taro-cli 包以後,咱們的命令行裏就多了一個 taro 命令。

$ npm install -g @tarojs/cli

那麼 taro 命令是怎樣添加進去的呢,其緣由在於 package.json 裏面的 bin 字段;

"bin": {
    "taro": "bin/taro"
  },

上面代碼指定,taro 命令對應的可執行文件爲 bin/taro。npm 會尋找這個文件,在 [prefix]/bin 目錄下創建符號連接。在上面的例子中,taro會創建符號連接 [prefix]/bin/taro。因爲 [prefix]/bin 目錄會在運行時加入系統的 PATH 變量,所以在運行 npm 時,就能夠不帶路徑,直接經過命令來調用這些腳本。

關於prefix,能夠經過npm config get prefix獲取。

$ npm config get prefix
/usr/local

經過下列命令能夠更加清晰的看到它們之間的符號連接:

$ 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

taro 子命令

上面咱們已經知道 taro-cli 包安裝以後,taro 命令是怎麼和 /bin/taro 文件相關聯起來的, 那 taro init 和 taro build 又是怎樣和對應的文件關聯起來的呢?

命令關聯與參數解析

這裏就不得不提到一個有用的包:tj/commander.js Node.js 命令行接口全面的解決方案,靈感來自於 Ruby's commander。能夠自動的解析命令和參數,合併多選項,處理短參等等,功能強大,上手簡單。具體的使用方法能夠參見項目的 README。

更主要的,commander 支持 git 風格的子命令處理,能夠根據子命令自動引導到以特定格式命名的命令執行文件,文件名的格式是 [command]-[subcommand],例如:

taro init => taro-init
taro build => taro-build

/bin/taro 文件內容很少,核心代碼也就那幾行 .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)

command方法

用法:.command('init <path>', 'description')

command的 用法稍微複雜,原則上他能夠接受三個參數,第一個爲命令定義,第二個命令描述,第三個爲命令輔助修飾對象。

  • 第一個參數中可使用 <> 或者 [] 修飾命令參數
  • 第二個參數可選。

    • 當沒有第二個參數時,commander.js 將返回 Command 對象,如有第二個參數,將返回原型對象。
    • 當帶有第二個參數,而且沒有顯示調用 action(fn) 時,則將會使用子命令模式
    • 所謂子命令模式即,./pm./pm-install./pm-search等。這些子命令跟主命令在不一樣的文件中。
  • 第三個參數通常不用,它能夠設置是否顯示的使用子命令模式。
注意第一行 #!/usr/bin/env node,有個關鍵詞叫 Shebang,不瞭解的能夠去搜搜看。

參數解析及與用戶交互

前面提到過,commander 包能夠自動解析命令和參數,在配置好命令以後,還可以自動生產 help(幫助) 命令和 version(版本查看) 命令。而且經過program.args即可以獲取命令行的參數,而後再根據參數來調用不一樣的腳本。

但當咱們運行 taro init 命令後,以下所示的命令行交互又是怎麼實現的呢?

$ taro init taroDemo
Taro即將建立一個新項目!
Need help? Go and open issue: https://github.com/NervJS/taro/issues/new

Taro v0.0.50

? 請輸入項目介紹!
? 請選擇模板 默認模板

這裏使用的是SBoudrias/Inquirer.js 來處理命令行交互。

用法其實很簡單:

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

if (typeof conf.description !== 'string') {
      prompts.push({
        type: 'input',
        name: 'description',
        message: '請輸入項目介紹!'
      })
}

prompt()接受一個問題對象的數據,在用戶與終端交互過程當中,將用戶的輸入存放在一個答案對象中,而後返回一個Promise,經過then()獲取到這個答案對象。so easy!

藉此,新項目的名稱、版本號、描述等信息能夠直接經過終端交互插入到項目模板中,完善交互流程。

固然,交互的問題不只限於此,能夠根據本身項目的狀況,添加更多的交互問題。inquirer.js強大的地方在於,支持不少種交互類型,除了簡單的input,還有confirmlistpasswordcheckbox等,具體能夠參見項目的工程README

此外,你還在執行異步操做的過程當中,你還可使用 sindresorhus/ora 來添加一下 loading 效果。使用chalk/chalk 給終端的輸出添加各類樣式。

模版文件操做

最後就是模版文件操做了,主要分爲兩大塊:

  • 將輸入的內容插入到模板中
  • 根據命令建立對應目錄結構,copy 文件
  • 更新已存在文件內容

這些操做基本都是在 /template/index.js 文件裏。

這裏還用到了shelljs/shelljs 執行shell 腳本,如初始化 git git init,項目初始化以後安裝依賴npm install等。

拷貝模板文件

拷貝模版文件主要是使用 jprichardson/node-fs-extracopyTpl()方法,此方法使用ejs模板語法,能夠將輸入的內容插入到模版的對應位置:

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

更新已經存在的文件內容

更新已經存在的文件內容是很複雜的工做,最可靠的方法是把文件解析爲AST,而後再編輯。一些流行的 AST parser 包括:

  • Cheerio:解析HTML
  • Babylon:解析JavaScript
  • 對於JSON文件,使用原生的JSON對象方法。

使用 Regex 解析一個代碼文件是邪道,不要這麼幹,不要心存僥倖。

taro build

taro build 命令是整個 taro 項目的靈魂和核心,主要負責 多端代碼編譯(h5,小程序,React Native等)。

taro 命令的關聯,參數解析等和 taro init 實際上是如出一轍的,那麼最關鍵的代碼轉換部分是怎樣實現的呢?

這個部份內容過於龐大,須要單獨拉出來一篇講。不過這裏能夠先簡單提一下。

編譯工做流與抽象語法樹(AST)

Taro 的核心部分就是將代碼編譯成其餘端(H五、小程序、React Native等)代碼。通常來講,將一種結構化語言的代碼編譯成另外一種相似的結構化語言的代碼包括如下幾個步驟:

image

首先是 parse,將代碼 解析(Parse)抽象語法樹(Abstract Syntex Tree),而後對 AST 進行 遍歷(traverse)替換(replace)(這對於前端來講其實並不陌生,能夠類比 DOM 樹的操做),最後是 生成(generate),根據新的 AST 生成編譯後的代碼。

Babel 模塊

Babel 是一個通用的多功能的 JavaScript 編譯器,更確切地說是源碼到源碼的編譯器,一般也叫作 轉換編譯器(transpiler)。 意思是說你爲 Babel 提供一些 JavaScript 代碼,Babel 更改這些代碼,而後返回給你新生成的代碼。

此外它還擁有衆多模塊可用於不一樣形式的 靜態分析

靜態分析是在不須要執行代碼的前提下對代碼進行分析的處理過程 (執行代碼的同時進行代碼分析便是動態分析)。 靜態分析的目的是多種多樣的, 它可用於語法檢查,編譯,代碼高亮,代碼轉換,優化,壓縮等等場景。

Babel 其實是一組模塊的集合,擁有龐大的生態。Taro 項目的代碼編譯部分就是基於 Babel 的如下模塊實現的:

  • babylon Babylon 是 Babel 的解析器。最初是 從Acorn項目fork出來的。Acorn很是快,易於使用,而且針對非標準特性(以及那些將來的標準特性) 設計了一個基於插件的架構。
  • babel-traverse Babel Traverse(遍歷)模塊維護了整棵樹的狀態,而且負責替換、移除和添加節點。
  • babel-types Babel Types模塊是一個用於 AST 節點的 Lodash 式工具庫, 它包含了構造、驗證以及變換 AST 節點的方法。 該工具庫包含考慮周到的工具方法,對編寫處理AST邏輯很是有用。
  • babel-generator Babel Generator模塊是 Babel 的代碼生成器,它讀取AST並將其轉換爲代碼和源碼映射(sourcemaps)。
  • babel-template babel-template 是另外一個雖然很小但卻很是有用的模塊。 它能讓你編寫字符串形式且帶有佔位符的代碼來代替手動編碼, 尤爲是生成的大規模 AST的時候。 在計算機科學中,這種能力被稱爲準引用(quasiquotes)。

解析頁面 config 配置

在業務代碼編譯成小程序的代碼過程當中,有一步是將頁面入口 js 的 config 屬性解析出來,並寫入 *.json 文件,供小程序使用。那麼這一步是怎麼實現的呢,這裏將這部分功能的關鍵代碼抽取出來:

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

// 2. 遍歷,解析爲 JSON 對象
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 節點的 Lodash 式工具庫
            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))
    }
    if (node.type === 'NullLiteral') {
        return null
    }
    return node.value
}

// 3. 寫入對應目錄的 *.json 文件
fs.writeFileSync(outputPageJSONPath, JSON.stringify(configObj, null, 2))

經過以上代碼的註釋,能夠清晰的看到,經過以上三步,就能夠將工程裏面的 config 配置轉換成小程序對應的 json 配置文件。

可是,哪怕僅僅是這一小塊功能點,真正實現起來也沒那麼簡單,你還須要考慮大量的真實業務場景及極端狀況:

  • 應用入口app.js 和頁面入口 index.js 的 config 是否得單獨處理?
  • tabBar配置怎樣轉換且保證功能及交互一致?
  • 用戶的配置信息有誤怎樣提示?

更多代碼編譯相關內容,仍是放在下一篇吧。

總結

到此,taro-cli 的主要目錄結構,命令調用,項目初始化方式等基本都捋完了,有興趣的同窗能夠結合着工程的源代碼本身跟一遍,應該不會太費勁。

taro-cli 目前是將模版放在工程裏面的,每次更新模版都要同步更新腳手架。而 vue-cli 是將項目模板放在 git 上,運行的時候再根據用戶交互下載不一樣的模板,通過模板引擎渲染出來,生成項目。這樣將模板和腳手架分離,就能夠各自維護,即便模板有變更,只須要上傳最新的模板便可,而不須要用戶去更新腳手架就能夠生成最新的項目。 這個後期能夠歸入優化的範疇。

下一篇文章,咱們將一塊兒進入 Taro 代碼編譯的世界。

相關文章
相關標籤/搜索