随着小程序用户增多,越来越多的平台开始扩展自己的小程序能力。如传统微信小程序,我厂的京东小程序,字节和百度等众多平台都在加入。但是当前的开发大环境,却是"小步快跑,快速迭代"。那么,"一套代码,多端运行"成为很多团队实践的梦想。我们在最近的京采云移动端商城项目中使用 Taro + Vue3 + NutUI3 , 实践了一下多端开发之路。
开发背景(项目背景)
京采云将需求管理、寻源招标、供应商管理、自助式商城、履约协同、财务结算“六大管理能力”集于一体,能够高效完成多供应商的引入和全生命周期管理,并通过智能化采购分析帮助企业将采购需求和采购方案自动匹配,实现便捷、高效的供应链管理。同时,产品希望项目不只能在 PC 端运行,同时还要求开发 H5 页面方便嵌入客户 APP 中,满足客服自建电商商城的需求,且在下一阶段还希望项目能够在小程序中快速应用。
技术选型和介绍
技术对比
刚接到需求,我们着手调查了市面上目前存在的多端框架,例如 Taro , uni-app , chameleon 等。 为了寻找更适合我们的框架,从以下几个方面做了对比:
1 开发工具
uni-app 应该是一骑绝尘,它的文档内容最为翔实丰富,还自带了 IDE 图形化开发工具(HBuilderX),鼠标点点点就能编译测试发布。其它的框架都是使用 CLI 命令行工具,但值得注意的是 chameleon 有独立的语法检查工具,Taro 则单独写了 ESLint 规则和规则集。
2 技术栈
mpvue、uni-app、Taro 均支持 TypeScript,也都能通过 typing 实现编辑器自动补全。除了 API 补全之外,得益于 TypeScript 对于 JSX 的良好支持,Taro 也能对组件进行自动补全。但是 Taro 对 React 和 Vue 都能完整的提供支持,其他都只能支持一种前端框架。
而 CSS 语法方面,所有框架均支持 SASS、LESS、Stylus,Taro 则多一个 CSS Modules 的支持。
3 多端支持
目前uni-app、Taro 和 chameleon 都能支持市面上常见的小程序。
4 性能对比
不管是 Taro 还是 uni-app,setData
的优化都是小程序性能优化中最为重要之事,且优化主要有两个方向:
- 尽可能减少
setData
调用的频次 - 尽可能减少单次
setData
传输的数据
我们自己动手写了一个长列表测试,分别写了 Taro 版、uni-app 版、原生小程序版,前几页数据滚动时效率都差不多,但是7、8页过去后,发现 uni-app 加载新页面时有变慢的感觉。
推测 uni-app 的长列表没有 recycle 机制,花了点时间把 demo 改进了下,滚动下面时把前面几页的数据干掉,然后再滚动就感受不到流畅度的差别了。
所以得出结论:Taro 在性能优化上做的更细致,使用 uni-app 需要自己注意代码优化。
综上所述,Taro 和 uni-app 作为市面上最为优秀的多端框架。在各项指标上都不相上下。但是 Taro 对开发者更加开放且更加多元化,同时配合更符合京东风格的 NutUI 组件库。所以 Taro + NutUI3 成为我们首选。
我们先介绍一下我们将要使用的工具
Taro 介绍
Taro 是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发 微信 / 京东 / 百度 / 支付宝 / 字节跳动 / QQ 小程序 / H5 / RN 等应用。
当前 Taro 已进入 3.x 时代,一套更加高效,精简的 DOM/BOM API 包(taro-runtime)可以实现同时对齐 React 和 Vue 2/3 的开发。
@tarojs/runtime 是 Taro 的运行时适配器核心,它实现了精简的 DOM、BOM API、事件系统、Web 框架和小程序框架的桥接层等
这时 Web 框架就可以使用 Taro 模拟的 API 渲染出一颗 Taro DOM 树,但是这一切都运行在小程序的逻辑层。而小程序的 xml 模板需要提前写死,Taro 如何使用一个静态的模板文件去渲染这颗动态的 Taro DOM 树呢?
Taro 选择了利用小程序 可以引用其它 的特性,把 Taro DOM 树的每个 DOM 节点对应地一个个渲染。这时只需要把 Taro DOM 树的序列化数据进行 setData,就能触发数据的相互引用,从而渲染出最终的 UI。 更多原理我们在下面详细介绍。
NutUI 介绍
作为一个京东风格的轻量级移动端 Vue 组件库,在 NutUI 3 的版本中,采用了 Vite + Vue3 + ts 的架构,同时全面支持 Taro 对小程序的适配。Taro 官方也将 NutUI 作为 Vue 技术栈的推荐组件库。
NutUI 是如何实现支持 Taro 小程序的?
针对每个组件,我们在原有组件的目录结构中新增 .taro.vue
文件来专门处理 Taro 兼容。而在使用的时候,可以根据你的需求,安装常规 Vue3 版本还是 Taro 版本。
#Vue3 项目
npm i @nutui/nutui@next -S
# NutUI 小程序多端项目
npm i @nutui/nutui@taro -S
而使用的时候,根据不同的源引用对应的组件即可。
import { createApp } from 'vue';
// vue
import { Button } from '@nutui/nutui';
// taro
import { Button } from '@nutui/nutui-taro';
这样,我们就可以在 Taro 中使用这套组件库了。
业务开发
京采云商城为了满足企业客户自建电商商城,搭建了一套完整的商城流程,包括首页(banner 图,导航,推荐等多个楼层),分类,搜索,商品详情,店铺列表,店铺详情,购物车,订单,个人页面,地址 选择等。同时针对大批量采购,增加了订单提报和审批的流程。同时后期将会更加丰富商城功能,为客户提供更高效,快捷的采购服务。
先从视觉上感受一下~
支持多平台嵌入
为了方便企业客户内 app 引入我们商城,更好的引流商城商品。我们只要客户的 app 内添加我们的入口链接,使用 Cookie 打通登陆态,即可完美嵌入。
我们设置 Cookie 的 domain 参数,来实现不同域名之间访问同一个 Cookie 。
document.cookie = "Cookie=testcookie; domain=test.com;path=/";
主题定制
为了更好的服务各种接入平台,保持与接入平台的风格一致,我们必须实现主题可配置化,根据接入平台的主题色,传入不同的参数实现主题一致的效果。
首先,我们在开发的时候,提取出公共的 sass 文件,其中包含我们常见的主题样式变量。
//主题色
$primary-color: #478ef2;
//按钮颜色
$bg-color-selected: #fef4f3;
$bg-color-disabled: #fcd4cf;
//边框颜色
$border-color: #ececec;
// 更多
......
我们在开发过程中,我们直接使用对应的样式变量,既方便我们后期维护,也简单实现了主题一处修改,全局应用的要求。
但是这样我们每次修改主题色都需要修改这个 Scss 文件,我们希望通过一个接口来返回主题色,然后实现主题的渲染。如何实现呢?
得益于 Vue3 sfc 的功能,我们可以直接在 Vue 文件中的 style 书写 css 样式的时候,可以使用 v-bind 来绑定变量,从而实现 css 样式中使用 js 中的变量。
const colorState = reactive({
primaryColor: '#f0250f', //默认颜色
borderColor:#fef4f3
});
//在 App.vue 中获取全局主题
router.isReady().then(async () => {
...
const result = await getTheme(); //获取主题的接口
if (result?.state === 0) {
colorState.primaryColor = result.primaryColor;
colorState.borderColor = result.borderColor;
}
});
<style lang="scss">
// 定义全局类
.primary-color {
color: v-bind(primaryColor);
}
.border-color {
color: v-bind(borderColor);
}
</style>
通过以上的定义,我们可以在代码中直接使用 css 类 primary-color 等实现动态样式。
Taro + NutUI 的应用
随着 Vue 3 的正式发布,更好的性能,更小的体积,更好的 TypeScript 集成,更好的处理大规模用例的新 API ,所有的开发者开始拥抱 Vue3 。
Taro 紧随潮流,迅速在 Taro3 中开始支持 Vue3 代码的转换,而 NutUI 也紧随其后,首先完成了所有组件 Vue2 到 Vue3 的升级,紧接着完成了对 Taro 的适配,更好的服务开发人员。
目前 Taro 仅提供一种开发方式:安装 Taro 命令行工具(Taro CLI)进行开发。通过在终端输入命令 npm i -g @tarojs/cli
安装 Taro CLI 之后就可以使用了。具体的 Taro 如何使用和开发,在官网上和都有完整的教程,这里我们就不做重点介绍。
下面我们重点研究一下 Taro 和 NutUI 是如何实现一套代码应用多端的。
我们先来看一下小程序的架构。
微信小程序主要分为逻辑层和视图层,以及在他们之下的原生部分。逻辑层主要负责 JS 运行,视图层主要负责页面的渲染,它们之间主要通过 Event 和 Data 进行通信,同时通过 JSBridge 调用原生的 API。这也是以微信小程序为首的大多数小程序的架构。
小程序原生部分,各个平台是不一样的,而且这部分对于前端开发这就是一个黑盒。所以前端层面只需要关注逻辑层和视图层。
因此,只需要在逻辑层调用对应的 App()/Page()
方法,且在方法里面处理 data、提供生命周期/事件函数等,同时在视图层提供对应的模版及样式供渲染就能运行小程序了。这也是大多数小程序开发框架重点考虑和处理的部分。
那么对前端来讲,无论什么框架 Vue or React,最终运行结果都是调用了浏览器的几个 BOM/DOM 的 API ,如:createElement
、appendChild
、removeChild
等。因此,上文也提到过,Taro 使用 taro-runtime 完成了这套 API。
接下来,在 React 中实现了一个 taro-react 包,用来连接 taro-runtime 和 React 维护 dom 树的核心 react-reconciler,从而实现 render 函数,生成 Taro DOM Tree。Vue 同样处理,在 Vue 的 CreateVuePage 方法中对齐 一些运行时方法的处理,如生命周期或者 wacth 等。
通过内部的 Taro 插件,Taro就会生成一个Taro DOM Tree,那么Taro DOM Tree 如何对齐小程序,从而渲染到小程序页面上呢?
首先,我们将小程序的所有组件挨个进行模版化处理,从而得到小程序组件对应的模版,如下图就是小程序的 view 组件经过模版化处理后的样子:
<template>
<view
id="{{uid}}"
class="{{className}}"
style="{{style}}"
>
<block wx:for="{{children}}">
<template is="{{'tpl_'+item.nodeName}}" data="{{item}}">
</block>
</template>
接下来,我们就去遍历我们的 Taro DOM Tree 树,再根据每个子元素的类型选择对应的小程序模板来渲染子元素,然后在每个模板中我们又会去遍历当前元素的子元素,以此把整个节点树递归遍历出来。最后就可以在小程序上渲染成功了。
// Taro 转换得到的 dom 树
{
1:[{
type:REPLACE,node:ELEMENT
}],
2:[{
type:text,content:'文本',children:[ ... ]
}],
3:[{
type:PROPS,props:{class:"content",number:1},...
}],
...
10:[{
type:ELEMENT,tage:"ul",childred:[{tag:"li",children:["item"] },...]
}]
}
当然,大家就可以看出来,这就是一个普通的虚拟 dom 树。类似
const newVnode = h(
"div#container.two.classes",
{ on: { click: anotherEventHandler } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
);
那么,我们动态递归处理这个 dom 树,同时转换成符合小程序格式,这样就完美的在小程序上展示了。
<template>
<text>
文本 ...
</text>
...
<ul>
<li>...</li>
....
</template>
那么 NutUI 组件库又是如何实现对 Taro 的适配呢?
上面我们提过,NutUI 针对每个组件单独设置了一个 Taro 文件,如 button.taro.vue。专门针对 Taro 的适配。其中直接使用 view 标签来作为常容器,更贴合小程序的转换。
我们来看看这个文件内容:
<template>
<view :class="classes" :style="getStyle" @click="handleClick">
<view class="nut-button__warp">
<nut-icon class="nut-icon-loading" v-if="loading"></nut-icon>
<nut-icon :class="icon" v-if="icon && !loading" :name="icon"></nut-icon>
<view :class="{ text: icon || loading }" v-if="$slots.default">
<slot></slot>
</view>
</view>
</view>
</template>
同时借助 tarojs****/plugin-html实现对一些 dom 和事件的处理。
//对dom转换
if(inlineElements.has(nodeName){
return "text"
}else if(specialElements.has(nodeName)){
return "view"
} else if( ...)
//对事件的处理
onAddEvent(type,_hader_,_options,node){
if(!isHtmlTags(node.nodeName)) return;
//click事件转换成tap
if(type == "click"){
defineMappedProp(node._handlers,type,"tap")
}else if(type == "input"){
defineMappedProp ...
}
}
项目优化和反思
开发过程中当然会遇到很多问题,同时也值得我们记录下来,更好的服务下次开发。
路由
首先 Taro 在 Vue 中并没有实现真正的路由,而是通过控制组件现实/隐藏来模拟路由切换。所以在历史记录中,做了一个10层的限制。当使用 Taro.navigateTo 跳转的时候,很快就会突破这个10层。这个需要我们对 navigateTo 函数做了一层封装,防止溢出。
此外,因为没有真正的实现路由,所以当 A 页面跳转到 B 页面,其实 A 页面并没有消失,只是通过 display:none 进行了隐藏,并给了 A 页面一个独一无二的随机 id 。即使这样,我们在代码中操作 DOM 元素或者一些 setInterval 方法,都要特别注意。taro 导致不会每次进入页面就会刷新,也不会离开就销毁,刷新,清理数据的动作都需要自己在生命周期函数里主动触发。
页面内容缓存
接上面内容,使用 navigateTo 完成路由跳转,当 A 页面进入 B 页面 再回到 A 页面的时候,其实 A 页面并没有重新渲染。还会保留上次的数据。实现类似 Vue 中 keep-alive 组件的效果。但因为没有提供额外的 生命周期函数,所以会导致数据不更新。即使强制驱动数据更新,也因为上一次渲染的缓存,导致页面有一个闪烁的变化,影响用户体验。所以建议在每次的 beforeMount 时清理一下当前页面数据,避免闪烁。同时慎用 navigateTo 跳转,多用 redirectTo 完成路由。
监听页面滚动事件
为了适配小程序,所以页面的滚动事件只能通过 onPageScroll 来监听,所以当我想在组件里进监听操作时,要将该部分的逻辑提前到onPageScroll函数,提高了抽象成本。例如我需要开发一个滚动到某个位置就吸顶的tab,本来可以在tab内部处理的逻辑被提前了,减少了其可复用性。
优化
性能优化是每个项目的重点内容,我们使用了在总结了一下几点常见的优化点。
总结
作为第一个使用 Taro + Vue3 + NutUI3 完成的项目,虽然开发中遇到很多问题,但也收获很多。"一套代码,多端应用" 的需求场景将会越来越多,我们将会在更多的业务中深入实践,完成自我的提升。