文章同步於 Github/Blog
Taro 是由凹凸實驗室打造的一套遵循 React 語法規範的多端統一開發框架。javascript
使用 Taro,咱們能夠只書寫一套代碼,再經過 Taro 的編譯工具,將源代碼分別編譯出能夠在不一樣端(微信小程序、H五、App 端等)運行的代碼。實現 一次編寫,多端運行。 關於 Taro 的更多詳細的信息能夠看官方的介紹文章 Taro - 多端開發框架 ,或者直接前往 GitHub 倉庫 NervJS/taro 查看 Taro 文檔及相關資料。css
Taro 項目實現的功能強大,項目複雜而龐大,涉及到的方方面面(多端代碼轉換、組件、路由、狀態管理、生命週期、端能力的實現與兼容等等)多,對於大多數人來講,想要深刻理解其實現機制及原理,仍是比較困難的。html
Taro 技術揭祕
系列文章將爲你逐步揭開 Taro 強大的功能以後的神祕面紗,帶領你深刻 Taro 內部,瞭解 Taro 是怎樣一步一步實現 一次編寫,多端運行 的宏偉目標,同時也但願藉此機會拋磚引玉,促進前端圈涌現出更多的,可以解決你們痛點的開源項目。前端
首先,咱們將從負責 Taro 腳手架初始化和項目構建的的命令行工具,也就是 Taro 的入口:@tarojs/cli 開始。vue
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 來進行管理,而且一樣使用了包管理工具 Lerna。git
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
,我已經在上面詳細標註了主要的目錄和文件的做用,至於具體的流程,我們接下來再分析。
taro init 命令主要的流程以下:
當咱們全局安裝 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-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('init <path>', 'description')
command的 用法稍微複雜,原則上他能夠接受三個參數,第一個爲命令定義,第二個命令描述,第三個爲命令輔助修飾對象。
第二個參數可選。
./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
,還有confirm
、list
、password
、checkbox
等,具體能夠參見項目的工程README。
此外,你還在執行異步操做的過程當中,你還可使用 sindresorhus/ora 來添加一下 loading 效果。使用chalk/chalk 給終端的輸出添加各類樣式。
最後就是模版文件操做了,主要分爲兩大塊:
這些操做基本都是在 /template/index.js
文件裏。
這裏還用到了shelljs/shelljs 執行shell 腳本,如初始化 git git init
,項目初始化以後安裝依賴npm install
等。
拷貝模版文件主要是使用 jprichardson/node-fs-extra 的copyTpl()
方法,此方法使用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 項目的靈魂和核心,主要負責 多端代碼編譯(h5,小程序,React Native等)。
taro 命令的關聯,參數解析等和 taro init
實際上是如出一轍的,那麼最關鍵的代碼轉換部分是怎樣實現的呢?
這個部份內容過於龐大,須要單獨拉出來一篇講。不過這裏能夠先簡單提一下。
Taro 的核心部分就是將代碼編譯成其餘端(H五、小程序、React Native等)代碼。通常來講,將一種結構化語言的代碼編譯成另外一種相似的結構化語言的代碼包括如下幾個步驟:
首先是 parse,將代碼 解析(Parse)
成 抽象語法樹(Abstract Syntex Tree)
,而後對 AST 進行 遍歷(traverse)
和 替換(replace)
(這對於前端來講其實並不陌生,能夠類比 DOM 樹的操做),最後是 生成(generate)
,根據新的 AST 生成編譯後的代碼。
Babel 是一個通用的多功能的 JavaScript 編譯器
,更確切地說是源碼到源碼的編譯器,一般也叫作 轉換編譯器(transpiler)
。 意思是說你爲 Babel 提供一些 JavaScript 代碼,Babel 更改這些代碼,而後返回給你新生成的代碼。
此外它還擁有衆多模塊可用於不一樣形式的 靜態分析
。
靜態分析是在不須要執行代碼的前提下對代碼進行分析的處理過程 (執行代碼的同時進行代碼分析便是動態分析)。 靜態分析的目的是多種多樣的, 它可用於語法檢查,編譯,代碼高亮,代碼轉換,優化,壓縮等等場景。
Babel 其實是一組模塊的集合,擁有龐大的生態。Taro 項目的代碼編譯部分就是基於 Babel 的如下模塊實現的:
在業務代碼編譯成小程序的代碼過程當中,有一步是將頁面入口 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 配置文件。
可是,哪怕僅僅是這一小塊功能點,真正實現起來也沒那麼簡單,你還須要考慮大量的真實業務場景及極端狀況:
更多代碼編譯相關內容,仍是放在下一篇吧。
到此,taro-cli
的主要目錄結構,命令調用,項目初始化方式等基本都捋完了,有興趣的同窗能夠結合着工程的源代碼本身跟一遍,應該不會太費勁。
taro-cli
目前是將模版放在工程裏面的,每次更新模版都要同步更新腳手架。而 vue-cli 是將項目模板放在 git 上,運行的時候再根據用戶交互下載不一樣的模板,通過模板引擎渲染出來,生成項目。這樣將模板和腳手架分離,就能夠各自維護,即便模板有變更,只須要上傳最新的模板便可,而不須要用戶去更新腳手架就能夠生成最新的項目。 這個後期能夠歸入優化的範疇。
下一篇文章,咱們將一塊兒進入 Taro 代碼編譯的世界。