文章同步于 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 代码编译的世界。