携程Web组件在跨端场景的开发实践

2023-07-0408:37:33WEB前端开发Comments726 views字数 6361阅读模式
携程Web组件在跨端场景的开发实践

一、背景

我们在开发 H5 营销活动后,通常会将营销活动的入口投放到多端,包括 App、小程序。常见的投放形式有:Native 原生页面、React Native 页面和小程序页面的内嵌弹窗。那么此时,就需要 Native、RN、小程序端的人力投入。由此,整个流程从仅需 H5 开发演变成需要多端开发、沟通,从 H5 营销活动灵活上线演变成受制于 App 和小程序的版本发布。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

为了优化此流程,我们引入了一种全新的方案——跨端共享 Web 组件。这一方案秉承“一套 Web 代码,多端共享”的理念,旨在缩短上线周期、降低人力成本、并快速响应迭代。采用跨端共享 Web 组件,我们能够高效地实现多端共享,同时也能够更加丰富地展示 Web 组件,从而为我们的业务带来更多的价值。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

二、方案介绍

那么如何做到“一套 Web 代码,多端共享”——文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

我们的小程序使用 Taro 框架和 React 框架进行开发,Taro 支持渲染 HTML 标签,鉴于此,我们选择了 React 作为 Web 组件的开发技术栈,这样,一方面,我们能直接运行在小程序端,另一方面可以用 React 的强大功能来创建可复用的自定义 HTML 元素。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

在小程序端,Web 组件以 NPM 包的形式存在。在 Native 和 RN 端,使用 WebView,加载一个包含 Web Components 的 H5 链接。不管是 NPM 包的形式,还是 Web Components 的形式,都是同一套 Web 代码的产物。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

在介绍实践过程之前,先简单介绍一下 Web Components。Web Components 是 Web 标准的一部分,是 W3C 提出的一套组件模型。由三个主要技术组成:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

a. Custom Elements:允许开发者创建自定义 HTML 元素,这些元素可以拥有自己的属性和方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

b. Shadow DOM:允许开发者创建封装的 DOM 树,将其附加到自定义元素上,从而实现样式和行为的隔离。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

c. HTML Templates:允许开发者定义可重用的 HTML 模板,这些模板可以在不同的 Web 应用程序中使用。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

浏览器基于此标准实现了一套 API,Web Components 作者可以用这些 API 去封装组件功能,然后把它应用到任何地方,不必担心有任何冲突。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

React 或 Vue 都提供了相应 API,让开发者能以 React 组件或 Vue 组件的形式书写 Web Components。而这里,我们正是用的 React 组件的形式书写 Web 组件,然后将其打包为 Web Components。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

假设弹窗组件名为 zt-dialog,我们提供给 Native 和 RN 端的 H5 链接内容形似:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

<html>
  <head>
    <script src="https://static.tripcdn.com/zt-dialog.umd.js"></script>
  </head>
  <body>
    <zt-dialog></zt-dialog>
  </body>
</html>

这段代码表明,zt-dialog 组件的自定义 HTML 元素是 `zt-dialog` ,其功能逻辑被打包到一个 UMD 格式的 JavaScript 文件中。这意味着,Web 组件可以被应用到任何其他 H5 中。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

我们给小程序端提供的内容则是一个 NPM 包 @ctrip/zt-dialog,主要内容则是:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

import Dialog from '@ctrip/zt-dialog'
import '@ctrip/zt-dialog/dist/styles/mini.css'

三、Web组件与宿主环境

我们的 Web 组件相较于普通的 React 组件,需要考虑哪些问题呢?可以从 Web 组件寄宿于不同环境这个角度进行思考,在这个场景下,Native 端、RN 端、小程序端都是宿主环境。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

因此我们要思考三个核心问题是:如何识别不同宿主环境,如何使用宿主环境的能力以及如何与宿主环境通信。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

3.1 识别宿主环境

其实方法有很多种,比如各端可以传一个特殊参数,或者利用 WebView 区别于小程序的全局变量等等,来做宿主环境的识别判断。但最终我们选择了一种更优解,利用环境变量,在构建时仅打包所需代码。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

环境变量是在应用程序运行时根据不同环境提供不同值的一种机制。我们的 Web 组件使用 Vite 进行构建,它支持在项目中使用环境变量。在应用程序中,通过 `import.meta.env` 对象来访问这些环境变量,根据值不同,来执行不同的逻辑。在构建时,这些环境变量会被静态替换。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

比如下面这段源代码,根据`VITE_COMP_TYPE` 变量的值来处理不同的宿主环境下的 onClose 事件和 onJump 事件:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

const onClose = () => {
  if (import.meta.env.VITE_COMP_TYPE === 'mini') {
    console.log("mini")
  } else {
    console.log("webview")
  }
});


const onJump = () => {
  if (import.meta.env.VITE_COMP_TYPE === 'mini') {
    console.log("mini jump")
  } else {
    console.log("webview jump")
  }
}

通过这段构建命令:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

cross-env VITE_COMP_TYPE=mini vite build

最终小程序端使用的 NPM 包结果输出如下图:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

const u = () => {
      console.log("mini")
    },
    p = () => {
      console.log("mini jump")
    };

可以看出我们这里只会有`mini` 的代码。从另一个角度讲,小程序端引入 Web 组件,其 Size 是很敏感的,所以我们用这种方式也可以尽可能打包更小 Size 的代码。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

3.2 使用宿主环境的能力

Web 组件需要使用的能力一般来说,有发送请求、导航、分享、埋点。在 Native 和RN 端,我们使用 WebView 加载 Web 组件,那么发送请求,可以利用浏览器发送请求的能力;至于埋点,我们也可以使用浏览器加载埋点脚本,从而自行处理埋点逻辑;而导航和分享则使用桥方法即可。在小程序端,我们考虑得则要多一些,下面展开讲讲。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

一般来说原生小程序都会对请求进行封装,带一些特定的请求参数,并且对请求返回值做预先的处理,因此发送请求只能由小程序端以组件参数的形式传给 Web 组件。导航、埋点同理。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

分享则有一些特殊,微信小程序规定,唤起分享有两个条件:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

条件一:通过给 button 组件设置属性`open-type=share`;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

条件二:在用户点击按钮后触发`Page.onShareAppMessage`事件获取到分享相关信息。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

条件一经测试,Web 组件用这样的写法即可满足:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

<button openType="share">
    <p>分享</p>
</button>

条件二则不行,如果你是小程序开发人员,那么你一定知道`Page.onShareAppMessage`是一个页面处理函数,它是用于监听用户点击页面分享按钮的事件,并不能被主动调用。解决这个问题的思路如下文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

a. Web 组件从小程序端提供的注册中心拿到一个唯一分享源 ID文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

b. Web 组件将分享源 ID 给到 button 标签文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

c. Web 组件向分享源信息中心注册这个 ID 对应的分享信息文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

最终,用户在点击分享的时候,小程序端可从分享源信息中心拿到当前分享源 ID 对应的分享信息。图示:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

携程Web组件在跨端场景的开发实践

3.3 与宿主环境通信

思考一个问题,Web 组件是否需要与宿主环境通信?如果是,那通信场景有哪些?在实践过程中,我们发现有这两种场景:用户点击关闭组件、在合适的时机显示组件。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

通信方式如图:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

携程Web组件在跨端场景的开发实践

就实际场景来看下对应代码,以“用户点击关闭按钮”场景为例:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

const closePopUp = () => {
    if (import.meta.env.VITE_COMP_TYPE === 'mini') {
        props.close(); // 小程序端传递的关闭事件参数
    } else if (isRNWebView() {
        window.postMessage(JSON.stringify({
            closeModal: true  // RN端使用postMessage发送closeModal事件
        }));
    } else if (isNativeWebView()) {
        window.Bridge.insideClose(() => {}); // APP端使用桥方法关闭当前WebView
    }
};

由此,不管什么场景下,我们都可以用类似的方式实现与宿主环境的通信。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

再看下“在合适的时机显示组件”这种场景,首先我们理解下什么是“合适的时机”,也许你会想,在符合特定业务逻辑的前提下,让 Web 组件正常显示不就是“合适的时机”吗?实际实践后,我们发现,在小程序端,我们采用了 NPM 包形式嵌入、打包分离、公共样式抽离、webp 等方式尽可能优化其性能,Web 组件确实能正常显示,准确说做到了让用户对组件加载无感知。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

但是在 Native 和 RN 端,我们使用了 WebView 加载 H5 链接的方式,一旦使用了大图+显示动画,那么 Web 组件的呈现方式就有一些不尽如人意,主要体现在用户能明显感知到大图的加载过程、大图未显示完成动画就已经开始。因此,需要把这种场景处理得更细致些。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

处理思路如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

a. Native 加载一个 WebView 容器,此时 WebView 不显示文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

b. WebView 加载完成后,加载一个 H5,这个 H5 会加载耗时较多的资源文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

c. 待资源加载完成后,H5 通知到 Native 显示 WebView文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

d. H5 显示 Web 组件,此时开始 Web 组件的动画文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

图示:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

携程Web组件在跨端场景的开发实践

等资源加载完成后,“通知Native显示WebView”这个过程则使用桥方法通信机制。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

由此,在 Native 和 RN 端,能够更加细致化地控制 Web 组件的显示,从而更加优雅地显示 Web 组件。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

至此,Web 组件和宿主环境之间的核心问题就解决了。在这时,我们还在小程序端遇到一个样式的小问题。Taro 在进行 px 尺寸单位的换算时,默认以 750px 作为换算标准,而我们编写 Web 组件时,通常以 375px 为标准。这导致在小程序端显示时,整体样式会比小程序的样式小一倍,最后的解决方案是编译小程序样式时利用插件对尺寸*2。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

另外为了优化图片加载性能,Web 组件的图片会使用 webp 格式。在小程序端,支持 webp,因此可以直接使用,而 Native 和 RN 端则需要根据浏览器支持情况做一下判断。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

四、对Web组件的支持

在了解了“一套 Web 代码,多端共享”的正确打开方式之后,再来看下各端对 Web 组件需要做怎样的支持。毕竟在换位思考之后,我们才能从“旁观者清”的角度去完善 Web 组件。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

首先,Native 端为 Web 组件开启了一个透明的 WebView。这个 WebView 要区分于非透明的 WebView。因此约定 H5 链接里添加特定 query 参数。如果 Web 组件想要指定 WebView 的宽高,也是同样地添加特定 query 参数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

假设约定的 query 参数是 `insidepop=1`,zt-dialog 组件的 H5 链接形式如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

https://m.ctrip.com/demo/zt-dialog.html?insidepop=1

以 Android 为例,在 Native 端被使用:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

Intent intent = new Intent(); // 初始化一个通用Intent
Activity activity = new Activity();
intent.setClass(activity, H5Container.class)
intent.putExtra(H5Container.URL_LOAD, 'https://m.ctrip.com/demo/zt-dialog.html?insidepop=1'); // 加载包含Web组件的H5链接
AppUtil.startActivity(activity, intent);

再者,在 RN 端,我们使用 WebView 控件开启一个透明的 WebView。由于需要处理关闭弹窗、导航、分享等功能,RN 端基于 WebView 控件再次做了封装。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

同样是 zt-dialog 组件的 H5 链接形式,在 RN 端被使用:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

import React from 'react';
import { ViewPort, Text, TouchableHighlight } from 'react-native';
import { WebViewModal } from 'react-native-webview';


export default class Demo {
  render() {
    return (
      <ViewPort>
        <TouchableHighlight onPress={() => {this.webviewRef.showModal()}}>
          <Text>show modal</Text>
        </TouchableHighlight>          
        <WebViewModal
          position='bottom'
          webViewUrl={'https://m.ctrip.com/demo/zt-dialog.html'}
        />      
      </ViewPort>
    )
  }
}

最后,小程序端使用的是 NPM 包的形式,基于上述的一些思考,在小程序端,其很多能力都依赖于参数传递的方式,因此小程序端封装了一个 React Hoc 组件,将我们约定好的请求、导航、分享等等能力都封装到这个 Hoc 组件中。这个 Hoc 组件类似:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

import React from "react"
import Taro from "@tarojs/taro"


const webBridgeHoc = (WebComp)=>{
  return (props)=>{
    return <WebComp
      _ubtTrace={_ubtTrace} // 埋点
      _request={requestFunc} // 小程序原生request
      _navigateTo={Taro.navigateTo} // 跳转
      _redirectTo={Taro.redirectTo} // 重定向跳转
      _reLaunch={Taro.reLaunch} // 关闭所有页面,打开到应用内的某个页面
      _switchTab={Taro.switchTab} // 切换tab页
      ...
    />
  }
}
export default webBridgeHoc

zt-dialog 组件在小程序端被使用时:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

import Dialog from '@ctrip/zt-dialog'
import '@ctrip/zt-dialog/dist/styles/mini.css'
import webBridgeHoc from '@/components/webBridgeHoc'
 
export default webBridgeHoc(Dialog)

总的来说,各端对 Web 组件的支持是相对简单的。在做了一定的封装之后,实际应用过程中,我们还在 Native 端的首页弹窗进一步做了服务端收口下发 Web 组件的 H5 链接。因此 Native 端的首页弹窗甚至无需再有 Native 端的人力介入,也可以完成一个完整闭环的需求交付周期。而这样的过程是可以完全复制到小程序端和 RN 端的。至此,完全释放 Native、RN、小程序的人力。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

五、总结与展望

其实,从各端对 Web 组件的支持就可以看出,跨端共享 Web 组件一方面是整合了各端现有的能力,另一方面是发挥自己的优势如丰富的动画吸引用户。换句话说,在实践前期,投入的成本并不大,但初期的效益却是直观的——释放了多端人力,而是否能够最大化地发挥优势产生收益则是我们 Web 组件开发需要继续关注的课题。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

后续我们将持续关注,丰富的 Web 组件表现形式是否有效提高了用户的点击率以及 Web 组件在各端的性能表现。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

最后,让我们看下 Web 组件的效果:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

Native 端:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

携程Web组件在跨端场景的开发实践文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

小程序端:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

携程Web组件在跨端场景的开发实践文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

欢迎感兴趣的 Web 开发者提出宝贵意见。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

【作者简介】

Iris,携程前端开发经理,专注于前端组件库和工程化领域。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

Abert,携程高级研发经理,关注跨端解决方案。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/49675.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/gcs/49675.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定