引言
在如今不断增长的小程序市场中,小程序的数量迅速增多。这是因为小程序具有诸多优势,例如轻量化、便捷性和良好的用户体验,吸引了越来越多的开发者和企业加入这一领域。随着小程序的普及,各个行业都纷纷推出自己的小程序,以满足用户的多样化需求。
然而,正是因为小程序市场的多样性和快速发展,每个小程序客户端的 Api 差异也变得十分显著。不同的小程序平台为了满足自身的特殊需求和功能定位,往往会对 Api 进行定制和调整。这导致了各个小程序客户端之间的 Api 存在差异,不同平台的开发者需要针对不同的 Api 进行开发和适配。
对于开发者来说,针对不同平台重新开发一套小程序应用将变成一场无尽的噩梦。开发者需要熟悉并掌握每个客户端的api差异,编写大量重复的代码,并进行平台特定的调试和适配工作。这不仅增加了开发的工作量和时间成本,还容易导致错误和兼容性问题。
在这样的背景下,Taro 的出现为开发者提供了一种解决方案。它通过提供一套统一的开发框架和组件,使开发者能够编写一套代码,同时在多个小程序平台上运行。Taro 的编译工具能够将开发者的代码转换为不同平台所需的代码,从而实现跨平台的开发和适配,减轻了开发者的负担,提高了开发效率。
Taro是一套遵循 React 语法规范的多端统一开发框架(ps:Vue 语法也支持)。主要用于构建跨平台的小程序、H5和移动应用。市面上还存在其他的多端框架,包括但不限于:
uni-app是 DCloud 推出的一款基于 Vue.js 的跨平台开发框架,可用于构建微信小程序、支付宝小程序、H5、App等多个平台的应用。
项目地址:?DCloud - HBuilder、HBuilderX、uni-app、uniapp、5+、5plus、mui、wap2app、流应用、HTML5、小程序开发、跨平台App、多端框架
React Native 是由 Facebook 开发的框架,用于构建原生移动应用。它使用JavaScript和React语法,允许开发者通过一套代码同时在 iOS 和 Android 上构建应用。
Flutter是由 Google 开发的UI工具包,用于构建跨平台的移动、Web 和桌面应用。它使用 Dart 编程语言,提供了丰富的UI组件和渲染能力。
Weex 是由阿里巴巴开发的跨平台开发框架,使用 Vue.js 语法,用于构建移动应用。它支持在 iOS、Android和 Web 上运行。
NativeScript 是由 Progress 开发的开源框架,用于构建原生移动应用。它支持使用 JavaScript 或 TypeScript 编写代码,并提供了访问原生 Api 的能力。
在上述的这些中,只有uni-app是支持小程序场景的,它占据了多端框架的半壁江山。
概括来讲,Taro的主要特点和优势,参照官方说法:“使用 Taro,我们可以只书写一套代码,再通过 Taro 的编译工具,将源代码分别编译出可以在不同端(微信小程序、H5、App 端等)运行的代码。”
这里需要解释一下“编译时配置”机制。官方说的“一次编译”,并不是真的打一个 dist 包,能跑遍所有的平台。
而是根据你想要运行的平台,用对应的指令,打出适合该平台运行的包。
举个例子:
微信小程序?编译命令?yarn?build:weapp
百度小程序?编译命令?yarn?build:swan
支付宝小程序?编译命令?yarn?build:alipay
H5?编译命令?yarn?build:h5
RN?编译命令?yarn?build:rn?--platform?ios
……
所以我们需要真正关心的,其实是针对目标的平台,Taro 都做了哪些事。下面以微信小程序为例子:
Taro 框架内置了对应的编译器和构建工具,在 @tarojs/plugin-platform-weapp 微信小程序平台插件中。在此注册微信小程序平台的配置项。
//taro-weapp/src/index.ts
//在此注册微信小程序平台
ctx.registerPlatform({
??name:?'weapp',
??useConfigName:?'mini',
??async?fn?({?config?})?{
????const?program?=?new?Weapp(ctx,?config,?options?||?{})
????await?program.start()
??}
})
预先定义一个名为微信小程序的 Template 模版类,该类继承自 UnRecursiveTemplate。其主要功能是处理 Taro 框架中的模板相关操作,并根据特定需求进行定制。
//taro-weapp/src/template.ts
export?class?Template?extends?UnRecursiveTemplate?{
??...
??
??//构建wxs模板
??buildXsTemplate?()?{
????return?'<wxs?module="xs"?src="./utils.wxs"?/>'
??}
??//创建小程序组件
??createMiniComponents?(components):?any?{
????const?result?=?super.createMiniComponents(components)
????//?PageMeta?&?NavigationBar
????this.transferComponents['page-meta']?=?result['page-meta']
????this.transferComponents['navigation-bar']?=?result['navigation-bar']
????delete?result['page-meta']
????delete?result['navigation-bar']
????return?result
??}
??//替换属性名称
??replacePropName?(name:?string,?value:?string,?componentName:?string,?componentAlias)?{
????...
??}
??//构建wxs模板中与焦点相关的方法,根据插件选项判断是否启用键盘附件功能,并返回相应的字符串
??buildXSTepFocus?(nn:?string)?{
????...
??}
??//修改模板结果的方法,根据节点名称和插件选项对模板进行修改。
??modifyTemplateResult?=?(res:?string,?nodeName:?string,?_,?children)?=>?{
????...
??}
??//构建页面模板的方法,根据基础路径和页面配置生成页面模板字符串。
??buildPageTemplate?=?(baseTempPath:?string,?page)?=>?{
????...
??}
}
Taro 的编译工具,根据所选择的平台,转换成对应平台所需的代码。使用 ctx.applyPlugins ,去调用相应平台的插件处理函数,其中 platform 参数指定对应的平台:
//taro-cli/src/build.ts
...
await?ctx.applyPlugins(hooks.ON_BUILD_START)
await?ctx.applyPlugins({
??name:?platform,
??opts:?{
????config:?{
??????...config,
??????isWatch,
??????mode:?isProduction???'production'?:?'development',
??????blended,
??????isBuildNativeComp,
??????newBlended,
??????async?modifyWebpackChain?(chain,?webpack,?data)?{
????????await?ctx.applyPlugins({
??????????name:?hooks.MODIFY_WEBPACK_CHAIN,
??????????initialVal:?chain,
??????????opts:?{
????????????chain,
????????????webpack,
????????????data
??????????}
????????})
??????},
...
除此之外呢,代码转换过程,还涉及:
语法转换:Taro 支持使用类似于 React 的 JSX 语法进行开发,它将 JSX 代码转换为不同平台所支持的语法,如小程序的 WXML、React Native 的组件等。
样式转换:Taro 支持使用 CSS 预处理器编写样式,例如 Sass、Less 等。编译过程中,Taro 将这些样式文件转换为不同平台所支持的样式表,如小程序的 WXSS、H5 的 CSS 等。
在编译过程中,Taro 还会执行:
静态资源处理:Taro 会处理项目中的静态资源文件,如图片、字体等,将其转换为适用于不同平台的格式,并进行压缩和优化。
文件复制:Taro 会将一些不需要编译的文件直接复制到输出目录中,如项目配置文件、静态页面等。
文件合并与分割:Taro 会根据配置和代码中的引用关系,将多个文件进行合并或分割,以提高代码加载性能。
代码压缩与混淆:Taro 可以对生成的代码进行压缩和混淆,以减小文件体积和提高执行效率。
不通平台的api或多或少,总有一些差异。Taro如何实现api的适配和差异化处呢?
Taro 通过适配层和条件编译等机制实现 api 的适配和差异化处理。
它提供了一套统一的 api 接口,开发者可以在代码中使用这些 api,而 Taro 在编译过程中会将这些 api 转换为适用于各个平台的具体实现。
以getLocation
?为例。
如果我们要使用定位功能,在 Taro 中只需要在项目中使用 Taro 提供的 api?getLocation
?:
Taro.getLocation().then(res?=>?{
??console.log(res.latitude,?res.longitude);
});
在编译过程中,Taro 会根据目标平台的差异,将这段代码转换为适用于不同平台的具体实现。
对于微信小程序来说,转换为微信小程序的?wx.getLocation
,同时保留原始的参数和回调函数:
wx.getLocation().then(res?=>?{
??console.log(res.latitude,?res.longitude);
});
而对于支付宝小程序而言,Taro 则会将其转换为支付宝小程序的?my.getLocation
,同样保留原始的参数和回调函数:
my.getLocation().then(res?=>?{
??console.log(res.latitude,?res.longitude);
});
如此,Taro 在编译过程中根据目标平台的差异,将统一的 api 转换为各个平台所支持的具体 api。在这段代码中,processApis 函数接收一个 api 集合作为参数,并对其中的每个api进行处理:
//shared/native-apis.ts
function?processApis?(taro,?global,?config:?IProcessApisIOptions?=?{})?{
??...
??apis.forEach(key?=>?{
????if?(_needPromiseApis.has(key))?{
??????const?originKey?=?key
??????taro[originKey]?=?(options:?Record<string,?any>?|?string?=?{},?...args)?=>?{
????????let?key?=?originKey
????????//?第一个参数?options?为字符串,单独处理
????????if?(typeof?options?===?'string')?{
??????????...
????????}
????????//?改变?key?或?option?字段,如需要把支付宝标准的字段对齐微信标准的字段
????????if?(config.transformMeta)?{
??????????...
????????}
????...
????????//?为页面跳转相关的 api 设置一个随机数作为路由参数。为了给 runtime 区分页面。
????????setUniqueKeyToRoute(key,?options)
????????// Promise 化:将原本的异步回调形式转换为返回Promise对象的形式,使api的调用更加方便且符合现代JavaScript的异步处理方式。
????????const?p:?any?=?new?Promise((resolve,?reject)?=>?{
??????????obj.success?=?res?=>?{
????????????config.modifyAsyncResult?.(key,?res)
????????????options.success?.(res)
????????????if?(key?===?'connectSocket')?{
??????????????resolve(
????????????????Promise.resolve().then(()?=>?task???Object.assign(task,?res)?:?res)
??????????????)
????????????}?else?{
??????????????resolve(res)
????????????}
??????????}
??????????obj.fail?=?res?=>?{
????????????options.fail?.(res)
????????????reject(res)
??????????}
??????????obj.complete?=?res?=>?{
????????????options.complete?.(res)
??????????}
??????????if?(args.length)?{
????????????task?=?global[key](obj,?...args)
??????????}?else?{
????????????task?=?global[key](obj)
??????????}
????????})
????????//?给?promise?对象挂载属性
????????if?(['uploadFile',?'downloadFile'].includes(key))?{
??????????...
????????}
????????return?p
??????}
????}?else?{
??????...
????}
??})
??...
}
ps:虽然 Taro 提供了一套统一的 api 接口,但某些平台可能不支持特定的功能或特性。可能需要使用条件编译来调用平台特定的 api,以处理特定平台的差异。
当我们使用 Taro 去编写多端项目,需要使用 Taro 提供的?View
?等Taro组件。
因为,这些Taro组件,在不同平台上会被转换为相应的原生组件或元素。
举个例子,下面的代码中,我们使用Taro提供的Image,View,Text组件创建视图:
import?Taro?from?'@tarojs/taro';
import?{?View,?Text,?Image?}?from?'@tarojs/components';
function?MyComponent()?{
??return?(
????<View>
??????<Text>Hello</Text>
??????<Image?src="path/to/image.png"?/>
????</View>
??);
}
在编译生成过程中,Taro 会根据目标平台的差异将组件转换为适用于各个平台的具体组件。比如View
?组件会被转换为微信小程序的?view
?组件。对H5来说,View
?组件会被转换为?<div>
?元素。
在微信小程序中:
<view>
??<text>Hello</text>
??<image?src="path/to/image.png"></image>
</view>
在 H5 中:
<div>
??<span>Hello</span>
??<img?src="path/to/image.png"?/>
</div>
这样,我们可以使用相同的代码编写视图,也就是官方说的只要写一套代码的意思。
通过抽象层、平台适配、跨平台编译等处理,Taro其实已经为多端组件库的实现铺平了道路。如果你要做一个 Taro-UI 那样适应自己的多端组件库。直接使用Taro提供的基础组件去搭建复杂组件即可。
如果你说,你以前做过一个微信小程序,现在老板要你平行移植到支付宝等小程序中。来不及重构代码的话,反向转换也许能救一救急。反向转换,故名思义就是将小程序转换为Taro项目。
相关的代码在 @tarojs/cli-convertor 包中,核心逻辑在 parseAst 中,生成 AST 树,遍历处理对应的内容:
//taro-cli-convertor/src/index.ts
parseAst?({?ast,?sourceFilePath,?outputFilePath,?importStylePath,?depComponents,?imports?=?[]?}:?IParseAstOptions):?{
????ast:?t.File
????scriptFiles:?Set<string>
??}?{
????...
????//?转换后js页面的所有自定义标签
????const?scriptComponents:?string[]?=?[]
????...
????traverse(ast,?{
??????Program:?{
????????enter?(astPath)?{
??????????astPath.traverse({
????????????//对类的遍历和判断
????????????ClassDeclaration?(astPath){...},
???????????//表达式
????????????ClassExpression?(astPath)?{...},
????????????//导出
????????????ExportDefaultDeclaration?(astPath)?{...},
????????????//导入
????????????ImportDeclaration?(astPath)?{...},
????????????//调用
????????????CallExpression?(astPath)?{...},
????????????//检查节点的 object 属性是否为标识符 wx,如果是,则将 object 修改为标识符 Taro,并设置一个标志变量 needInsertImportTaro 为 true。这段代码可能是将 wx 替换为 Taro,以实现对 Taro 框架的兼容性处理。
????????????MemberExpression?(astPath)?{...},
????????????//检查节点的 property 属性是否为标识符 dataset,如果是,则将 object 修改为一个 getTarget 函数的调用表达式,传递了两个参数 object 和标识符 Taro。它还创建了一个导入语句,将 getTarget 函数引入,并将其赋值给一个对象模式。这段代码可能是对可选链式调用中的 dataset 属性进行处理,引入了 getTarget 函数来实现相应的转换。
????????????OptionalMemberExpression?(astPath)?{...},
????????????//?获取js界面所有用到的自定义标签,不重复
????????????JSXElement?(astPath)?{...},
????????????//?处理this.data.xx?=?XXX?的情况,因为此表达式在taro暂不支持,?转为setData
????????????//?将this.data.xx=XX?替换为?setData()
????????????AssignmentExpression?(astPath)?{...}
??????????})
????????},
????????exit?(astPath)?{...}
??????},
????})
??...
????return?{
??????ast,
??????scriptFiles,
????}
??}
ps:尽管官方提供了反向转换这一种工具,但是目前还是有局限性的。并不是所有的小程序都支持反向转换,目前只有微信小程序。且并不是所有的原生 api 都可以被转换,需要注意。希望后续该功能能够继续扩大,完善。
为什么需要 Prerender?官方给出了解释:
?Taro Next 在一个页面加载时需要经历以下步骤:
?框架(React/Nerv/Vue)把页面渲染到虚拟 DOM 中
?Taro 运行时把页面的虚拟 DOM 序列化为可渲染数据,并使用 setData() 驱动页面渲染
?小程序本身渲染序列化数据
?和原生小程序或编译型小程序框架相比,步骤 1 和 步骤 2 是多余的。如果页面的业务逻辑代码没有性能问题的话,大多数性能瓶颈出在步骤 2 的 setData() 上:由于初始化渲染是页面的整棵虚拟 DOM 树,数据量比较大,因此 setData() 需要传递一个比较大的数据,导致初始化页面时会一段白屏的时间。这样的情况通常发生在页面初始化渲染的 wxml 节点数比较大或用户机器性能较低时发生。
Taro 预渲染的工作原理是,在构建阶段使用服务器端渲染(SSR)的技术,将页面组件渲染成静态 HTML 文件,并将其保存在静态文件目录中。然后,当客户端请求该页面时,直接返回预渲染的静态 HTML,而不是动态生成页面。
通过在构建阶段将页面渲染为静态 HTML 文件,以提升首次加载速度、改善用户体验和优化搜索引擎的索引。
使用方式:
//config/index.js?或?/config/dev.js?或?/config/prod.js
const?config?=?{
??...
??mini:?{
????prerender:?{
??????match:?'pages/shop/**',?//?所有以?`pages/shop/`?开头的页面都参与?prerender
??????include:?['pages/any/way/index'],?//?`pages/any/way/index`?也会参与?prerender
??????exclude:?['pages/shop/index/index']?//?`pages/shop/index/index`?不用参与?prerender
????}
??}
};
module.exports?=?config
更多使用详见官网文档。
经过上面粗浅的分析,我们可以初步了解 Taro 的整套运作机制。以下是对其运作机制的总结:
代码转换和条件编译:Taro 通过将代码转换和条件编译应用于源代码,生成适用于目标平台的代码。这使得我们可以使用一套代码编写多个平台的应用程序。
抽象层和平台适配层:Taro 提供了一个抽象层和平台适配层来处理代码转换过程,确保 api 在不同平台上的兼容性。这使得我们可以在不同的平台上使用相同的 api 进行开发。
Taro 自定义组件和多端适应性:Taro 的内置组件天然适应框架,这意味着我们可以构建适用于多个平台的组件库,如 Taro UI。这样可以提高开发效率并实现跨平台的一致性。
反向转换:反向转换是一种逆向思路,试图通过将已有的应用程序转换为 Taro 代码来实现跨平台。然而,反向转换存在不稳定性和局限性,并且对于维护者来说收益有限。
预渲染(Prerender)作为性能优化选择:Taro 提供了预渲染(Prerender)技术作为一种性能优化选择。预渲染可以在构建过程中生成静态 HTML 页面,以提升首次加载速度和优化搜索引擎的索引。这是一种有效的性能优化手段。