关于
此项目由 Qunar.com 提供支持。
大约从11月份开始,我们切换到branch3开发,正式启动自定义组件机制实现nanachi的组件机制
原来master上使用 template标签来编写组件,它其实规避了许多问题,因为4大小程序的自定义组件机制都各有不同,template则是兼容成本最低的方案。但是用template标签编写组件,其实那不是组件,对于小程序来说就是视图片段。换言之,一个页面只有一个组件,而这个组件的数据则是非常庞大。果不其然,它在支付宝小程序的IOS8/9中因为性能问题挂掉,只好匆匆启动后备方案
简单回顾一下四大小程序的模板
<!--wxml-->
<view class="container">
<view class="userinfo" bindtap="eventCanBubble">
<button wx:if="{{!hasUserInfo && canIUse}}" catchtap="eventNoBubble"> 获取头像昵称 </button>
<block wx:else>
<view wx:for="array" wx:for-item="el" wx:for-index="index" wx:key="*this">
{{el.title}}
</view>
</block>
</view>
</view>
<!--axml-->
<view class="container">
<view class="userinfo" onTap="eventCanBubble">
<button a:if="{{!hasUserInfo && canIUse}}" catchTap="eventCanBubble"> 获取头像昵称 </button>
<block a:else>
<view a:for="array" a:for-item="el" a:for-index="index" key="title" >
{{el.title}}
</view>
</block>
</view>
</view>
<!--swan-->
<view class="container">
<view class="userinfo" bind:tap="eventCanBubble">
<button s-if="{{!hasUserInfo && canIUse}}" catch:tap="eventCanBubble"> 获取头像昵称 </button>
<block s-else>
<view s-for="(index, el) in array" s-key="title" >
{{el.title}}
</view>
</block>
</view>
</view>
<!--快应用ux-->
<template>
<view class="container">
<view class="userinfo" onClick="eventCanBubble">
<button if="{{!hasUserInfo && canIUse}}" onClick="eventCanBubble"><text> 获取头像昵称</text> </button>
<block else>
<view for="(index, el) in array" tid="title" >
<text>{{el.title}}</text>
</view>
</block>
</view>
</view>
</template>
<!--头条小程序-->
<view class="container">
<view class="userinfo" bindtap="eventCanBubble">
<button tt:if="{{!hasUserInfo && canIUse}}" catchtap="eventCanBubble"> 获取头像昵称 </button>
<block tt:else>
<view tt:for="array" tt:for-item="el" tt:for-index="index" tt:key="*this">
{{el.title}}
</view>
</block>
</view>
</view>
从模板来看,其实差别不大,改一下属性名,每个公司都想通过它们来标识自己的存在。但内部实现完全不一样,因为源码并没有公开或者混淆了。使用自定义组件机制的风险就比<template>
标签大很多。 BAT三公司都暴露了一个Component入口函数,让你传入一个配置对象实现组件机制,而以小米为首的快应用则是内部走vue,没有Component这个方法,只需你export一个配置对象。
//微信
Component({
data: {},
lifetimes: {//钩子必须放在lifetimes
created(){},//拿不到实例的UUID
attached(){},//钩子触发顺序与元素在文档位置一致
dettached(){}
},
methods: {//事件句柄必须放在methods
onClick(){}
}
})
//支付宝
Component({
data: {},
//没有与created对应的didCreate/willMount钩子
didMount(){},//能拿到实例的UUID
didUpdate(){},//钩子触发顺序是随机的
didUnmount(){},
methods: {
onClick(){}
}
})
//支付宝 生命周期V2
Component({
data: {},
onInit(){},//对应 react constructor, 只可以读取 this.props 设置 this.data 或调用 this.setData/$spliceData 修改 已有data
deriveDataFromProps(props){},//对应 react getDerivedStateFromProps,只可以调用 this.setData/$spliceData 修改 data
didMount(){},//对应 react componentDidMount
didUpdate(){},//对应 react componentDidUpdate
didUnmount(){},//对应 react componentWillUnmount
methods: {
onClick(){}
}
})
//百度
Component({
data: {},
created(){},//应该是微信自定义组件的早期格式,没有lifetimes,methods
attached(){},//拿不到实例的UUID
dettached(){},//钩子触发顺序与元素在文档位置一致
onClick(){}
})
//小米(快应用都是由小米提供技术方案)
export {
props: {},//基本与百度差不多
onInit(){},
onReady(){},
onDestroy(){},
onClick(){}
}
//头条小程序
Component({
data: {},
created(){},//拿不到实例的UUID
attached(){},//钩子触发顺序与元素在文档位置一致
dettached(){}
methods: {//事件句柄必须放在methods
onClick(){}
}
})
从内部实现来看,BAT 都是走迷你React虚拟DOM, 快应用走迷你 vue虚拟DOM, 但支付宝的实现不好,钩子的触发顺序是随机的。因此在非随机的三种中,我们内部有一个迷你React, anu,产生的组件实例放进一个队列中,而BTM (百度,微信,小米)的created/onInit钩子再逐个再出来,执行setData实现视图的更新。而支付宝需要在编译层,为每个自定义组件标签添加一个UUID ,然后在didMount匹配取出。
//anu
onBeforeRender(fiber){
var type = fiber.type;
var reactInstances = type.reactInstances;
var instance = fiber.stateNode;
if(!instance.wx && reactInstances){
reactInstances.push(instance)
}
}
//BTM的created/onReady <anu-dog></anu-dog>
created(){
var reactInstances = type.reactInstances;
var reactInstance = reactInstances.shift();
reactInstance.wx = this;
this.reactInstance = reactInstance;
updateMiniApp(reactInstance)
}
//支付宝 <anu-dog instanceUid="{{'i32432' }}"></anu-dog>
didMount(){
var reactInstances = type.reactInstances;
var uid = this.props.instanceUid;
for (var i = reactInstances.length - 1; i >= 0; i--) {
var reactInstance = reactInstances[i];
if (reactInstance.instanceUid === uid) {
reactInstance.wx = this;
this.reactInstance = reactInstance;
updateMiniApp(reactInstance);
reactInstances.splice(i, 1);
break;
}
}
}
其实如果一个页面的数据量不大,template标签实现的组件机制比自定义组件的性能要好,自定义组件标签会对用户的属性根据props配置项进行过滤,还要传入slot,启动构造函数等等。但数据量大,自定义组件机制由于能实现局部更新,性能就反超了。
但支付宝是个例,由于它延迟到在didMount钩子才更新数据,即视图出来了又要刷新视图,比其他小程序多了一次rerender与伴随而来的reflow。
快应用就更麻烦些,主要有以下问题
快应用要求像vue那样三种格式都放在同一个文件中,但script标签是无法export出任何东西,于是我只好将组件定义单独拆到另一个文件, 才搞定引用父类的问题。
快应用在标签的使用上更为严格,文本节点必须放在a, span, text, option这4种标签中,实际上span的使用限制还严厉些,于是我们在编译时,只用到a, text, option。而a是对标BAT的navigator,因此一般也用不到。
最大的问题是对CSS支持太差,比如说不支持
display: block, display: line
, 不支持浮动,不支持相对绝对定位,不支持.class1.class2
的写法……API也比BAT的API少这么多东西,兼容起来非常吃力。
娜娜奇提供的核心组件及他们对应的关系,核心的技术内幕
娜娜奇主要分为两大部分, 编译期的转译框架, 统一将以React为技术栈的工程转换为各种小程序认识的文件与结构
转译框架又细分为4部分, react组件转译器,es6转译器, 样式转译器及各种辅助用的helpers.
运行时的底层框架与补丁组件, 底层框架为ReactWx, ReactBu, ReactAli, ReactQuick,分别对标微信,百度,支付宝小程序及快应用,因为官方React的size太大,并没有适用的钩子机制,让我们在渲染前将数据传给原生组件进行setData() (setData是小程序实例更新视图的核心方法),因此我们基于我们早已成熟的迷你React框架anu进行一下扩展去掉DOM渲染层,加上各种对应的渲染层,从而形成 对应的React.
补丁组件是指, 小程序都自带一套UI组件,它们存在一些无法抹平的差异或在个别平台直接没有这个组件,我们需要用原生的view ,text等基础组件元素封装成缺省组件,比如Icon, Button, Navigator.
娜娜奇的目录结构以及对应的工程规范,cli以及发布打包,如何控制size
娜娜奇的目录结构以微信的标准为蓝图,大概分为app.js, pages目录, components目录,针对我们的业务,还添加了commons目录与assets目录。
- app.js是全工程的配置,以react组件形式呈现, 全局共享对象,全局的分享函数都在这实例上
- pages目录 放所有页面组件, 组件在index.js中, 这里目录存在层次
- components目录 放所有有视图的业务组件, 组件在index.js中, 这里的目录只有两层, components/ComponentName/index.jsindex.js 要exports与目录名同名的类名
- commons目录 放所有没有视图的业务组件,没有文件名与目录名的限制,但希望每个业务线的组件都放在与业务线同名的目录下
- assets目录, 放静态资源
app.js pages目录,components目录会应用react转译器与样式转译器, commons目录应用es6转译器,assets目录应用样式转译器
直观的效果见 这里的两个图
cli 命令见 这里
build后的大小 见开发工具的预览
娜娜奇提供的重要功能组件和模块,如何帮助开发者做到快速开发
提供了 @react, @components,@assets这几个别名,用法如import React from '@react' 这样在很深的目录下,大家就不需要import React from '../../../../ReactAli'这样写@components指向components目录@assets 则用在css, sass, less文件中指向assets目录
React.getApp(), React.getCurrentPage()方便大家得到当前APP配置对象与页面组件的实例
React.api 将对所有平台的上百个API进行抹平,API是wx, swan, my这几个对象,它们里面提供了访问底层的能力如通信录,电池,音量,地理信息, 上传下载,手机型号信息等一大堆东西
React.api 里的所有异步方法,都Promise化,方便大家直接用es7 的async ,await语法
样式转译器,帮用户处理样式表中的rpx/px之间的转换。
为了保证跨平台,设计娜娜奇技术方案的重要原则和开发规范,哪些不支持
所有接口访问必须 使用React.api的方法,不要直接在wx, swan, my对象中取
React组件的只有render方法才能使用JSX,它们需要遵守一下规范,详见这里
样式方面,为了兼容快应用,布局统一使用flexbox, 不能使用display:block/inline, float,position:absolute/relative
娜娜奇如何和原生小程序兼容,以及其他有用的辅助功能或者工具
娜娜奇不与某一种原生小程序兼容,因为它要照顾4种小程序
如果你的目录名,样式不符合规范,我们在转译阶段会给出友好提示
快应用的文本节点要求放在text, a, option, label下,娜娜奇会在编译阶段自动对没有放在里面的文本包一个text标签
页面配置对象的许多配置项(如tabBar, titBar的配置参数,页面背景参数), 我们也进行了抹平,用户只需要以微信方式写,我们自动转换为各个平台对应的名字,在快应用中,是没有tabBar, 我们直接让每个页面组件继承了一个父类,父类里面有tabBar, 令它长得与其他小程序一模一样