taro-build
taro-build:命令行工具,将 react 代码转换成 weapp/swan/alipay/tt/h5/quickapp/rn 几种类型的应用。
其参数包括如下:
type,待转换的类型,包括 weapp/swan/alipay/tt/h5/quickapp/rn。
watch,监听模式,可监测文件改动,自动做转换。
page,编译页面。
component,编译组件。
ui,编译 taro ui 库。
ui-index,指定 taro ui 库的索引文件路径。
plugin,编译 taro 插件,微信小程序所用。
port,指定端口。
release,发布快应用 quickapp。
在这里我们需要梳理转换为 rn 的流程,因此 type 传入 rn即可。
launch.json 的配置如下:
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"/**"
],
"program": "${workspaceFolder}/bin/taro-build",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"cwd": "${workspaceFolder}/myApp",
"args": [
"--type","rn"
]
}
]
}
cwd 为待转换 taro 项目路径。
转换过程
主要代码逻辑在 src/rn.ts -> build() 中,转换输出目录为 rn_tmp。步骤如下:
检查依赖
检查 package.json 中是否有 rn 相关依赖,即检查 dependencies 中是否包含 react-native。若没有,更新 package.json 文件添加如下依赖,并进行安装。
{
"@tarojs/components-rn": "^${version}",
"@tarojs/taro-rn": "^${version}",
"@tarojs/taro-router-rn": "^${version}",
"@tarojs/taro-redux-rn": "^${version}",
"react": "16.3.1",
"react-native": "0.55.4",
"redux": "^4.0.0",
"tslib": "^1.8.0"
}
代码与样式转换
主要工作由 Compiler.buildTemp() 来实现。
其遍历 taro 工程下的所有文件,调用 processFile 进行处理,处理流程如下:
如果为样式文件,不做处理。
如果为 js 文件,则会进行 ast的处理和依赖样式的转换。
其余类型文件,比如 html,则直接 copy 到 rn_temp 文件夹。
如果是 watch 模式,则会在本地启动一个 rn 打包服务器。
打包为 bundle,调用 react-native 的 cli.js 实现。
下面着重讲第二步,分为「代码转换」和「样式转换」。
代码转换
其主要调用 transformJSCode,返回生成的代码和引用的样式文件路径集合,处理如下:
调用 wxTransformer 转换成 ast,其会将 ts 代码编译成 js 代码后,在 babel 中设置一系列的插件返回ast。
遍历 ast,进行如下处理:
处理 ClassDeclaration/ClassExpression
即处理类的声明,获取当前类名。分为以下两种情况:
类继承自 Taro.xx,即 Taro 的预置组件。注意这里 Taro 的值是从下面的 ImportDeclaration 中获得的。即 import Taro from '@tarojs/taro。
类继承自 Component/PureComponent。
如果当前类无类名,即为 export default class extends xxx 的情况,则默认加上类名 _TaroComponentClass,并修改 ast。最终记录类名。
处理 ExpressionStatement
预处理 require 样式文件,将其改成 import 的方式。
处理 ImportDeclaration
处理 import 的样式文件,保存其路径
处理 js/ts 文件,重新设置其相对路径
如果从 @tarojs/taro 中导入,改成 @tarojs/taro-rn
import Taro, { getEnv, Component } from '@tarojs/taro
注意这里有两种类型,可以结合 ast 自行查看。
ImportDefaultSpecifier:默认导出类型,非 {} 包裹,这里为 Taro。
ImportSpecifier:{} 包裹,这里为 Component 和 getEnv。
这里会记录下 ImportDefaultSpecifier中 Taro 的值,在后面处理中会用到,因为需要 import。
同时会检查导入的变量是否属于 taroApis 。上述例子中,getEnv 就是 taroApi,则需要导入。
变量需要被使用才会被 import,转换后如下:
import { Component } from "@tarojs/taro-rn";
import { getEnv } from "@tarojs/taro-rn";
可能有人会疑惑为啥 import Taro from '@tarojs/taro-rn'; 没有生成。其实这句的导入是在后面过程才处理。
如果从 @tarojs/redux 中导入,改成 react-redux-rn
如果从 @tarojs/mobx 中导入,改成 @tarojs/mobx-rn
如果从 @tarojs/components 中导入,改成 @tarojs/components-rn
处理 ClassProperty
主要处理入口文件的 config 属性:
处理 pages,记录路径,然后移除节点
处理 tabBar,记录 icon 路径,然后移除节点
将 config 设置为 static,由于再次生成 ast 时使用了 babel-plugin-transform-class-properties 插件,所以最终表现为:
App.config = { window: { xx: 'yy'}
最终只剩下 windows 相关的属性。
处理 ClassMethod
注意这里仅处理入口文件 app.js。
标记是否有constructor/componentDidMount/componentDidShow/componentDidHide/componentWillUnmount
遍历 render方法的语法树,生成 JSXElement 的代码。
比如 App.js 中 render 方法如下:
render () {
return (
)
}
那么代码为 。
处理 ExportDefaultDeclaration
如果为入口文件,标记有 export default。
处理 JSXElement
标记有 JSX。
处理 JSXOpeningElement
处理 相关,获取 store 名称。
处理 Program 的 exit。
ClassMethod
主要处理入口文件。
在 constructor 中插入语句 Taro._$app = this
在 componentDidMount 中插入语句 this.componentDidShow(),前提为 componentDidShow 存在。
在 componentWillUnmount 中插入语句 this.componentDidHide(),前提为 componentDidHide 存在。
在render 中插入,根据是否有 pages,provider 等生成最终的结构。
比如有 pages 生成如下:
render() {
return
;
}
ClassBody
处理一些异常情况。
若有componentDidShow,但没有 componentDidMount 方法,则插入 componentDidMount 方法,并添加语句 this.componentDidShow && this.componentDidShow()。
若有componentDidHide,但没有 componentWillUnmount 方法,则插入 componentWillUnmount 方法,并添加语句 this.componentDidHide && this.componentDidHide()。
若没有constructor 方法,则添加如下代码
constructor() {
super(...arguments);
Taro._$app = this;
}
CallExpression
如果有直接调用 Taro.render(),则进行移除。
如果有 JSX,则导入 import React from 'react'
如果有使用 Taro,则导入 import Taro from @tarojs/taro-rn'
入口文件处理
导入依赖的页面文件路径。
导入 tarbBarIcon 路径,如果有设置 tarBar 的话。
末尾插入一些额外的代码,比如页面路由/ 初始化 api 能力/ px 转换等。
// router 相关
const RootStack = TaroRouter.initRouter([['pages/index/index', pagesIndexIndex]], Taro, App.config);
// api 能力
Taro.initNativeApi(Taro);
// pxTransform
Taro.initPxTransform({
"designWidth": 750,
"deviceRatio": {
"640": 1.17,
"750": 1,
"828": 0.905
}
});
// 默认导出
export default App;
注意点
上述操作对 ast 修改完成后,会调用 babel.transformFromAst,设置一系列插件重新生成 ast,再转换为最终代码。
// 将 jsx 中样式的写法转换为 stylesheet
babel-plugin-transform-jsx-to-stylesheet
// 使用 class property
babel-plugin-transform-class-properties
// 使用装饰器
babel-plugin-transform-decorators-legacy
// 移除无用 import
babel-plugin-danger-remove-unused-import
// 定义常量信息,这里用来设置环境,process.env.TARO_ENV
babel-plugin-transform-define
转换完成后,会发现如下代码:
let App = class App extends Component {}
声明的 class 被赋值给了变量 App。这其实是通过步骤 1 中的插件 babel-plugin-transform-decorators-legacy 完成的。
转换样式
调用 compileDepStyles,主要处理逻辑在 StyleProcess 中。
读取样式文件,进行 scss/sass/less 的预处理后,返回 css string。
使用 postcss 插件 pxtransform 进行单位转换,具体计算操作在 createPxReplace。
调用 taro-css-to-react-native 进行样式的处理。
app.scss:
.node {
background-color: red;
}
转换之后生成app_styles.js:
import { StyleSheet, Dimensions } from 'react-native'
// 一般app 只有竖屏模式,所以可以只获取一次 width
const deviceWidthDp = Dimensions.get('window').width
const uiWidthPx = 375
function scalePx2dp (uiElementPx) {
return uiElementPx * deviceWidthDp / uiWidthPx
}
export default StyleSheet.create({
"node": {
"backgroundColor": "red"
}
})
校验生成的 rn 样式属性的正确性。
生成 rn 样式文件xx_styles.js。
在转换后我们可以发现,原本引入的是 xx.scss 样式文件,现在变为了 import xxStyleSheet from "./xx_styles"。那么这一步是在哪做的呢?
其实是在上一步提到的,再次重新生成 ast 的过程中,设置 babel-plugin-transform-jsx-to-stylesheet 插件起的作用。
原始:
import './index.scss'
转换后:
import indexStyleSheet from "./index_styles";
var _styleSheet = indexStyleSheet;
;
生成工程文件
生成如下文件:
index.js,入口文件,添加下列代码。
import {AppRegistry} from 'react-native';
import App from './app';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => App);
app.json,工程配置
主要是将 package.json 中的 name 值取出,放入到 app.json中。
{
"name": "myApp"
}