写点什么

uni-app 黑魔法:小程序自定义组件运行到 H5 平台

用户头像
崔红保
关注
发布于: 2020 年 04 月 22 日

引言


移动互联网的初期,囿于设备硬件性能限制,流量以原生 App 为主,iOS、Android 是当时两大平台。


随着硬件及 OS 的更新换代,H5 可承载的体验逐步完善,为提高开发效率、节约资源(复用代码)以及热更新等目的,Hybrid 模式成为主流;以及轻应用、服务号等平台的助推,H5 网页流量暴涨,成为第三大平台。


2017 年 1 月 9 日,微信发布小程序,历经 3 年发展,在今年主题为”未完成 Always Beta“的微信公开课 PRO 上,微信团队披露,2019 年小程序日活跃用户超过 3 亿,全年累计成交额达 8000 亿,同比增长超 160%。


看到小程序如此惊人的增长力,我们有理由相信,有中国特色的小程序互联网时代已经到来,微信小程序也已成为继 iOS、Android、H5 之后的第四大流量平台。


平台分裂,为不同平台编写相同的业务代码,是件无趣的事情。


有追求的程序员,一直在探索代码复用的方案,Hybrid App 即是代表。


而在如今的小程序时代,对于同样基于 WEB 技术的 H5 和小程序,如何实现代码复用,是很多前端工程师探索的方向。业内也已有不少成熟方案,从场景上来说,大致分为三类:


  1. 基于跨端框架,从头开发,一套代码,发行多个平台,比如 DCloud 出品的 uni-app、京东凹凸实验室的 taro

  2. 复用 H5 代码,转换 H5 代码在小程序环境中执行;适用于有 H5 平台沉淀,未开发小程序或小程序完善度较低的开发者;

  • 美团的 mpvue 框架是早期探索解决这个问题的代表,但因小程序不支持 dom 操作,故 mpvue 适用于 vue 的无 dom 操作的 H5 代码转换;

  • 最近微信官方推出的 kbone,也是为了解决“把 Web 端的代码挪到小程序环境内执行”;不过,kbone 相比 mpvue 前进了一步(当然也有了新的性能缺陷),因为:

kbone 实现了一个适配器,在适配层里模拟出了浏览器环境,让 Web 端的代码可以不做什么改动便可运行在小程序里。

  1. 复用小程序代码,转换小程序代码在 web 环境中运行;适用于有小程序代码沉淀,未开发 H5 或 H5 平台完善度较低的开发者;这个方向业内成熟的方案还比较少。


uni-app 近期支持了小程序自定义组件运行到 H5 平台,是对如上第三种场景的一种探索。

需求场景


鉴于小程序的低成本获客特征,很多厂商选择先开发小程序,验证业务模式后,再扩展至 H5、App 等其它平台。


开发者虽可借助转换器将小程序代码转换为uni-app项目(或其它跨端框架项目),快速实现多平台发行;但不少开发者是不敢轻易决策将跨端版本替换之前线上的小程序版本的,毕竟线上版本已稳定运行了一段时间。


常选的方案是:让原生小程序版本和uni-app跨端版本并行一段时间,微信平台继续使用原生版本,其它平台使用 uni-app 跨端版本;经过一段时间验证 uni-app 版本稳定后,再使用 uni-app 版替换掉原生小程序版本。


在这段并行的时间内,开发者需要同时维护微信原生、uni-app 两个版本,新增业务需编写两份逻辑相同的代码,重复劳动,成本叠加,如何改善?


借助 uni-app 支持将微信小程序组件运行到 H5 平台的特性,我们给出一种思路:

  • 开发者在原生小程序项目中,将新增业务以自定义组件的方式开发,优先上线小程序;

  • 拷贝小程序组件的 wxml/wxss/js/json 文件到 uni-app 项目下,通过 uni-app 的编译器及运行时,保证小程序自定义组件在 H5 平台的正确运行。


这个方案的好处是:


  • 优先小程序开发,毕竟小程序早已上线,有存量用户

  • 复用小程序组件,新增业务仅需开发一套代码即可,降低开发成本


不止自己开发的小程序组件,业内开源的三方小程序组件,均可复制到 uni-app 项目项目中,运行到 H5 平台。


另外,部分公司的产品经理,会要求不同平台有不同的交互,但核心业务逻辑是相同的,开发者常会通过维护不同项目的方式来满足产品经理需求。此时,采取如上方案,同样可满足多个项目复用相同业务逻辑的诉求。


实际上,uni-app 之前已支持将小程序自定义组件运行到 App 平台,对于有小程序组件沉淀或优先小程序的开发者来说,这是个好消息,一套业务组件,快速运行到 iOS、Android、H5、微信小程序这四大流量平台(实际上也可运行到 QQ 小程序平台)。

uni-app 引用小程序组件演示


uni-app 项目中使用自定义组件的方法很简单,分为三步:


1、拷贝小程序自定义组件到 uni-app 项目根目录下的 wxcomponents 文件夹下


2、在 pages.json 对应页面的 style -> usingComponents 引入组件,如:

{    "pages": [        {            "path": "index/index",            "style": {                "usingComponents": {                     "custom": "/wxcomponents/custom/index"                }            }        }    ]}
复制代码


3、在页面中使用自定义组件,如:

<!-- 页面模板 (index.vue) --><view>    <!-- 在页面中对自定义组件进行引用 -->    <custom name="uni-app"></custom></view>
复制代码

方案实现思路


简单介绍下 uni-app 的多端发行原理。


uni-app 基于 Vue.js runtime,页面文件遵循 Vue.js 单文件组件 (SFC) 规范,天然对 H5 的支持比较好,发行到 H5 平台时,先通过 vue-loader 解析.vue 文件,导出 Vue.js 组件选项对象,然后在运行时补充规范实现:


  • 组件:框架提供内置组件(view/swiper/picker 等)的实现,保证平台 UI 及交互的一致性

  • 接口:在 H5 平台封装框架接口,比如路由跳转,showToast 等界面交互

  • 生命周期:Vue.js 的理念是一切皆为组件,没有应用和页面的概念;框架需创造出应用及页面的概念,模拟 onLaunch、onShow 等钩子


uni-app 发行到小程序平台时,逻辑又有不同,主要工作有 2 块:


  • 编译器:将.vue 文件拆分成 wxml/wxss/js/json4 个原生页面文件

  • 运行时:Vue.js 和小程序都是逻辑视图层框架,都有数据绑定功能;运行时会实现 Vue.js 到小程序的数据同步,及小程序到 Vue.js 的事件代理


小程序自定义组件类似小程序原生的页面开发,一个自定义组件同样由 wxml/wxss/js/json 4 个文件组成,另有单独的组件规范(如 Component 构造器、Behaviors 特性等)。


所以,小程序自定义组件运行到 H5 平台,可借助 uni-app 已有平台功能快速实现:


  • 编译阶段:将 wxml/wxss/js/json4 个文件合并为.vue 文件(类似 uni-app 发行到小程序的逆过程),然后调用 uni-app 发行 H5 平台的编译过程,通过 vue-loader 解析.vue 文件,导出 Vue.js 组件选项对象

  • 运行阶段:实现 Component 构造器、Behaviors 特性,模拟自定义组件特有的生命周期

编译:转换文件(mp2vue)


小程序自定义组件发行到 H5 平台,在编译环节主要有 2 项工作:


  1. 将自定义组件的 wxml/wxss/js/json 4 个文件组成,编译转换成.vue 文件,即小程序转 vue,可简写为 mp2vue

  2. 通过 vue-loader 解析.vue 文件,导出 Vue.js 组件选项对象


其中,步骤 2 是 Vue.js 项目的标准编译过程,略过不提;我们重点阐述步骤 1。


mp2vue 将 4 个独立 wxml/wxss/js/json 的文件合并成一个.vue 文件,并组装成 template、script、style 这种三段式的结构,流程包括:


  1. wxml 文件生成 template 节点,同时完成指令、事件等模板语法转换

  2. js/json 文件生成 script 节点,同时完成组件注册过

  3. wxss 文件生成 style 节点,自动转换部分 css 兼容语法

  4. 合并为.vue 文件


具体实现上,uni-app 编译前先扫描 wxcomponents 目录,若存在则认为有小程序自定义组件,启动文件转换工作(uni-migration 插件来完成):

//加载转换器const migrate = require('@dcloudio/uni-migration') //扫描wxcomponents目录const wxcomponents = path.resolve(process.env.UNI_INPUT_DIR, 'wxcomponents') if (fs.existsSync(wxcomponents)) {   migrate(wxcomponents, false, {    silent: true   }) // 转换 mp-weixin 小程序组件}
复制代码


接着开始对 wxml/wxss/js/json 文件逐个解析,并合并为一个.vue 文件:

module.exports = function transformFile(input, options) {	//首先转换json文件,判断是否为组件  const [jsCode, isComponent] = transformJsonFile(filepath + '.json', deps)  options.isComponent = isComponent	//转换 wxml 文件  const [templateCode, wxsCode = '', wxsFiles = []] = transformTemplateFile(filepath + templateExtname, options)	//转换wxss文件  const styleCode = transformStyleFile(filepath + styleExtname, options, deps) || ''  //转换js文件  const scriptCode = transformScriptFile(filepath + '.js', jsCode, options, deps)	// 生成合并后的.vue文件源码  return [    `${commentsCode}<template>${templateCode}</template>${wxsCode}<script>${scriptCode}</script><style platform="mp-weixin">${styleCode}</style>`,    deps,    wxsFiles  ]}
复制代码

进一步细节说明,wxml 文件转为 template 节点时,需完成各项指令、事件等模板语法的转换,例如:


将一个最简自定义组件,按照如上流程转换,结果示意如下:

运行时:模拟小程序组件环境

uni-app 的编译器并不转换小程序组件的 JS 代码,依然保留 Component 构造器的写法,甚至其中的 API 依然是 wx.开头的方式,这些都依赖 uni-app 在 H5 平台的运行时来解决,主要有如下几部分内容:


  • Component 构造器:解析小程序组件的各种选项配置,转换为 Vue 组件定义,包括变通实现其中的差异部分,如小程序组件特有的”组件所在页面的生命周期“

  • Behaviors 特性:转换为 Vue 的混入(mixin)

  • 数据响应:在 H5 平台实现 setData 接口及 this.data.xx = yy 的数据通讯机制

  • API 前缀:可在运行时通过代理机制,自动将 wx.xx 替换为 uni.xx,这个比较简单,不详述


Component 构造器


uni-app 在 H5 平台定义了一个 Component 函数,执行到小程序的 Component 构造器函数后,开始循环解析其属性,并转换成 Vue 组件属性,流程示意代码如下:

export function Component (options) {  const componentOptions = parseComponent(options)  componentOptions.mixins.unshift(polyfill)  componentOptions.mpOptions.path = global['__wxRoute']  initRelationsHandler(componentOptions)  global['__wxComponents'][global['__wxRoute']] = componentOptions}
export function parseComponent (mpComponentOptions) { const { data, options, methods, behaviors, lifetimes, observers, relations, properties, pageLifetimes, externalClasses } = mpComponentOptions
const vueComponentOptions = { mixins: [], props: {}, watch: {}, mpOptions: { mpObservers: [] } }
// 开始逐个解析所有属性 parseData(data, vueComponentOptions) parseOptions(options, vueComponentOptions) parseMethods(methods, vueComponentOptions) parseBehaviors(behaviors, vueComponentOptions) parseLifetimes(lifetimes, vueComponentOptions) parseObservers(observers, vueComponentOptions) parseRelations(relations, vueComponentOptions) parseProperties(properties, vueComponentOptions) parsePageLifetimes(pageLifetimes, vueComponentOptions) parseExternalClasses(externalClasses, vueComponentOptions) parseLifecycle(mpComponentOptions, vueComponentOptions) parseDefinitionFilter(mpComponentOptions, vueComponentOptions) // 返回 Vue 组件 return vueComponentOptions
复制代码


在这个过程中,需处理小程序自定义组件和 Vue 组件的属性对应关系及细节差异,如小程序组件的 lifetimes:


小程序的pageLifetimes(组件所在页面的生命周期)在 Vue 中是没有的,需要映射为 uni-app 封装的页面生命周期:


Behaviors 特性的实现过程,类似 Component 构造器,不再赘述。

数据响应


Vue 和小程序都有一套数据绑定系统,但机制不同,比如在 Vue 体系下,数据赋值是这样的:

this.a = 1
复制代码

但在小程序中,数据赋值方式则是这样的:

this.setData({	a:1}) //响应式this.data.a = 2 //非响应式
复制代码

另外,小程序和 Vue 在数据的 properties、observer 等方面都存在不少差异,经过我们评估,若将小程序的数据响应用法直接映射到 Vue 体系下,复杂度较高且有性能压力,故 uni-app 在 H5 平台按照微信的语法规范,单独实现了一套数据响应系统。

// 小程序的setData在H5平台的实现function setData (data, callback) {  if (!isPlainObject(data)) {    return  }  Object.keys(data).forEach(key => {    if (setDataByExprPath(key, data[key], this.data)) {      !hasOwn(this, key) && proxy(this, SOURCE_KEY, key);    }  });  this.$forceUpdate();//数据变化,强制视图更新(响应式)  isFn(callback) && this.$nextTick(callback);}
复制代码


将 setData 挂载到 vm 对象上,可通过 this.setData 这种小程序的方式调用;同时将数据绑定到 data 属性上,支持 this.data.xx 的访问方式。

export function initState (vm) {  const instanceData = JSON.parse(JSON.stringify(vm.$options.mpOptions.data || {}))  vm[SOURCE_KEY] = instanceData    //vm对象上挂载 setData 方法,实现小程序方法	vm.setData = setData 	  const propertyDefinition = {    get () {      return vm[SOURCE_KEY]    },    set (value) {      vm[SOURCE_KEY] = value    }  }	//小程序用法,可通过this.data.xx访问  Object.defineProperties(vm, {    data: propertyDefinition,    properties: propertyDefinition  })
Object.keys(instanceData).forEach(key => { proxy(vm, SOURCE_KEY, key) })}
复制代码

虽然数据响应是 uni-app 自己实现的,但渲染依然使用了 Vue 框架的 render 函数,此时需小程序规范中的 this.data.xx 和 Vue 规范中的 this.xx 保持一致,通过代理的方式实现:

// mp/polyfill/state/proxy.jsconst sharedPropertyDefinition = {  enumerable: true,  configurable: true};
function proxy (target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition);}
复制代码

这里仅列出了主要的几步,中间涉及细节很多;部分无法通过 Vue 扩展机制实现的功能,只好修改 Vue.js 的内核源码,比如 updateProperties 支持、小程序 wxs、externalClasses 等功能在 H5 平台的支持,都需要定制部分 Vue.js runtime 源码。


结语


本文分享了uni-app将微信小程序自定义组件发行到 H5 平台的实现思路,希望对大家有所启发。


但这种方案,归根到底是为了解决多套项目并存时的业务重复开发的问题。


如果你是从头开发,我们建议直接选择业内成熟的跨端框架,既可以保持一套代码,更省力的维护,还可以借助框架的成熟生态(如跨端 UI 库及插件市场),基于成熟轮子,快速完成业务的上线开发;


uni-app框架代码,包括小程序组件发行到 H5 平台的代码,全部开源在 github,如果大家对本文逻辑有疑问,欢迎提交 issue 交流。


发布于: 2020 年 04 月 22 日阅读数: 95
用户头像

崔红保

关注

中年人,很少写代码的CTO 2018.11.08 加入

还未添加个人简介

评论

发布
暂无评论
uni-app黑魔法:小程序自定义组件运行到H5平台