【腾讯课堂】基于Kbone使用React同构小程序开发实践总结

2019 年 11 月 6 日 IMWeb前端社区


导语:Kbone 是微信推出的 Web 与小程序同构解决方案,该方案现已支持 Vue、React 等同构


本文目录一览:

1. 背景

2. 框架选择

3. React-Kbone-Miniprogram 过程

4. 接入现有工程

    4.1 构建配置

        4.1.1 Babel

        4.1.2 Tree Shaking

        4.1.3 与小程序代码复用

    4.2 代码编写 

        4.2.1 小程序、H5 公共库适配

        4.2.2 open-type button 的回调函数

        4.2.3 小程序组件 boolean 类型的属性

5. 小程序同构页面优化

    5.1 主要打包内容分析

    5.2 精简 css

    5.3 精简 js

6. 总结


1
背景

新人课程礼包是腾讯课堂双十一活动需求之一,该需求中有一个礼包课程领取页在多端(H5、小程序、app)都有涉及。


在小程序端我们可以使用 web-view 嵌入 H5,但该方案加载耗时以及无法使用微信特有的能力(例如:获取微信用户绑定的手机号,沉浸式状态栏),适逢 Kbone 已支持 React 同构,因此我们针对该页面尝试基于 Kbone 使用 React 同构小程序实践。


最终实现的效果:

 【h5页面】


【小程序页面】


体验二维码:



2
框架选择


目前使用 React 构建小程序的方案大都使用静态编译的方式实现,例如 taro,nanachi。

这种静态编译方式只是让我们使用 React 和 JSX 的语法来编写小程序代码,然后通过语法分析工具把代码翻译成小程序模板。由于 JSX 并非模板语言,要将其翻译成小程序模板,则必须要牺牲一些 JS 的动态特性,这也就是为什么这种方案在编写上有很多限制,其本质缺陷在于语法分析是静态的,而 JS 是动态的。

此外,这种方案实际运行时并非 “真 React ”,因此对于跟进 React 特性来说,无法做到与官方同步。

至于 Kbone ,它能够支持完整的 React 和 JSX 语法,是因为它把 React 给完整引入进来,而对于 React 底层依赖了的 dom/bom 接口,它提供一套轻量的小程序适配层接口(miniprogram-render 和 miniprogram-element)。

正是因为通过提供适配器的方式来仿造出 Web 环境,所以我们可以在任意位置任意方式书写 React 和 JSX,而无须担心是否不支持某些新特性。


3
React-Kbone-Miniprogram 过程



从 kbone-template-react 官方例子来看,React 代码使用 Kbone 构建出小程序,其流程是基于 Webpack 来实现的,它使用 Babel 转换 React 代码并通过 mp-webpack-plugin 在构建 Web 端代码后追加 Kbone 和小程序相关的文件到小程序工程。


  • package.json、pages/*、app.**、project.config.json 等文件由 mp-webpack-plugin 插件生成的小程序工程文件。

  • miniprogram-render 和 miniprogram-element 是 Kbone 两个核心模块:仿造接口和自定义组件,它们通过小程序 npm 包安装。

  • common 目录包含业务样式、业务代码和第三方库(React 相关),是由 Babel 转换并打包输出的。

  • 从上图看,例子主要是以新项目出发,通过 webpack.mp.config.js 配置生成完整的小程序工程,但对于现存的小程序工程来说,其实我们并不需要 app.*、project.config.json 等文件,同时更多时候我们望是在现有的 H5 项目中书写代码和复用代码,然后生成小程序页面输出到现有小程序工程中。


4
接入现有工程


礼包课程领取页主要涉及到两个现存的工程:

  • m-core:是腾讯课堂 H5 页面,技术栈是 Webpack 4 + Babel 7+ React ^16.8 + Typescript

  • weapp-ke:是腾讯课堂小程序,技术栈是小程序原生框架

为了优先保证 H5 能够正常运行,我们将新页面的代码放到 m-core 项目,接着增加 webpack.mp.config.js 配置,由于同构生成的小程序页面依赖 Kbone 的适配层库,为避免原小程序工程主包过大,我们需要构建生成分包页面,同时上面说到 mp-webpack-plugin 会生成额外的小程序工程文件,所以我们要么在其构建结束后移除这部分文件,要么修改该插件仅生成必要的文件。我们暂时采用后一种方案,相对灵活一些,并已反馈给微信同学以支持生成单一页面代码到指定目录。

4.1
构建配置

我们基于 kbone-template-react 提供的 webpack.mp.config.js 来修改,以支持项目中使用的 React、Typescript、PostCSS、条件编译、Tree Shaking 等特性,还有与小程序代码复用。

Babel

{
  test/\.(ts|js)x?$/,
  use: [
    'thread-loader',
    {
      loader'babel-loader?cacheDirectory',
      options: isMp ? {
        configFilefalse// 避免babel加载babel.config.js
        presets: [
          '@babel/preset-typescript'// 支持typescript
          '@babel/preset-react'// 支持react
        ],
        plugins: [
            '@babel/plugin-proposal-class-properties',
        ]
      } : {
        configFile: path.resolve(rootDir, 'babel.config.js'),
      },
    },
    {
      loader'webpack-strip-block',
      options: { // 依据标记移除代码块
        start: isMp ? 'strip-block--h5-only:begin' : 'strip-block--mp-only:begin',
        end: isMp ? 'strip-block--h5-only:end' : 'strip-block--mp-only:end',
      },
    }
  ],
  include: [
    path.resolve(rootDir, 'src'),
    path.resolve(rootDir, 'node_modules/@tencent'),
  ],
  sideEffects: !isMp, // 小程序开启tree shaking
}


同构小程序使用的 babel-loader 配置与一般 H5 使用的配置有些不同,对于小程序我们可以不加 @babel/preset-env,是因为小程序开发者工具本身提供 ES6 转 ES5 的代码编译能力和增强编译能力。

至于插件请不要使用 @babel/plugin-transform-runtime 和 @babel/plugin-transform-modules-commonjs 插件,这两个插件在 h5 中比较常见,但在这里 @babel/plugin-transform-runtime 会导致小程序开发者工具运行报错,@babel/plugin-transform-modules-commonjs 会影响 Webpack 的 Tree Shaking。

此外,我们使用到 webpack-strip-block,目的就是根据环境移除不必要的代码块(效果与 DefinePlugin 相同,但 DefinePlugin 无法处理 import 声明),配合 DefinePlugin 和 Tree Shaking 一起使用。


Tree Shaking

由于小程序对包大小有严格限制,因此我们需要尽可能地减少包大小。Tree Shaking 是一种代码优化技术,它可以消除那些无用的代码。

Webpack 中要使用 Tree Shaking,我们必须保证:

  • 使用 ES2015 模块语法(即 import 和 export)。

  • 确保没有编译器将 ES2015 模块语法转换为 CommonJS 模块。

  • 在项目 package.json 文件中添加一个 "sideEffects" 属性或者在 module.rules 配置选项中设置 "sideEffects"。

  • 使用 production mode 配置选项启用各种优化插件,包括 Minification 和 Tree Shaking。

如上述条件所示,在同构项目中我们需要注意以下几点:

  • tsconfig.json 中的 compilerOptions.module 切勿设置为 "CommonJS"。

  • @babel/preset-envmodules 切勿设置为 "commonjs",同时避免使用 @babel/plugin-transform-modules-commonjs

  • 如果项目中 H5 部分使用了某些自执行的模块而无法使用 Tree Shaking,那么我们可以仅在构建小程序的配置中使用 Module.Rule.sideEffects 开启 Tree Shaking 而不影响 H5 的构建。


与小程序代码复用

现有 weapp-ke 小程序工程中使用了 @tencent/imwxutils 等 npm 库以及实现了各种 utils 代码,如果在同构代码中再实现一遍显然是再造轮子,同时会增加小程序包的大小,因此我们需要复用 weapp-ke 已有的代码。


  1. 对于 npm 包,由于 weapp-ke 小程序主包已经引入,所以同构代码在构建小程序代码时只需要通过 Webpack 的 externals 将 npm 包从输出的代码中排除,这样小程序在运行时会去主包获取这些依赖。

  2. 对于 weapp-ke 小程序已实现的 utils 代码,首先我们需要定位到 utils 目录并给它取一个别名,例如 weappKeUtils。

resolve.alias.weappKeUtils = path.resolve(weappKeDir, 'utils');


接着我们在同构的代码中引用小程序的代码模块。

import * as keRouter from 'weappKeUtils/router';

export function refresh() {
  if (process.env.isMiniprogram) {
    keRouter.refresh();
  } else {
    window.location.reload();
  }
}


最后通过 Webpack 的 externals 将 weappKeUtils 代码模块以相对路劲的方式外部依赖到小程序中。

  externals: [
    (context, request, callback) => {
      if (/^weappUtils/.test(request)) { // 通过commonjs引入小程序项目的utils,小程序模块定位只能使用相对路径
        return callback(null, request.replace('weappUtils''../../../utils'), 'commonjs');
      }
      return callback();
    }
  ],


4.2
代码编写

小程序、H5 公共库适配

由于原本的 H5 和小程序项目是分开的,暂时没有统一的模块管理。尤其是涉及两端特有 api 的库无法共用。

需要注意的是有副作用的库,有些库中会除了 export 方法外会有一些自执行逻辑,若是其中逻辑涉及到 wx API,或 Web API,会导致另一端上抛出异常,需要注意分别在两端中剔除无关的库。

因此可以考虑抽出一个适配层,抹平两端公共库的差异,如 request、上报组件、统一路由跳转等。

举个例子,由于同构的代码在 H5 项目中,我们将小程序的 request 方法向 H5 的 request 方法对齐入参和返回值,进行适配。

/* strip-block--h5-only:end */
request = function mpRequest(url: string, options: any = {}{
  // ...
}
/* strip-block--h5-only:end */

/* strip-block--h5-only:begin */
request = require('assets/request').default;
/* strip-block--h5-only:end */

export default request;


open-type button 的回调函数

React 有一套自己的事件系统,用事件委托的方法,在 document 对事件进行监听。因此我们在 JSX 中所传入的若不是 React 支持的 DOM 事件(如 click、mouseenter),DOM 上是获取不到我们传入的回调方法的。

而在小程序中,对于部分设置了 open-type 的 button,小程序支持设置回调来获取一些用户授权的信息,如 <button open-type="getPhoneNumber"> 对应了 bindgetphonenumber 回调,我们从在回调中获得解密用户手机号码的参数。这些都不是 React 中支持的回调函数。

因此这些方法需要被手动绑定到 DOM 上,才能被 Kbone 获取并触发到。我们可以另外封装一个 WxButton 组件,对这种特殊的回调做处理:

processWxEvents = (fn: any) => {
    if (process.env.isMiniprogram) {
      Object.keys(this.props).forEach((k) => {
        if (k.indexOf('wxOn') === 0) {
          const eventName = `on${k.slice(4)}`;
          fn(eventName, this.props[k]);
        }
      });
    }
  }

componentDidMount() {
  this.processWxEvents(this.addEventToDom);
}

componentWillUnmount() {
  this.processWxEvents(this.removeEventFromDom);
}

render() {
    const { children, className } = this.props;
    return (
      <wx-button
        {...this.props}
        {...(className ? { classclassName } : null)}
        ref={this.buttonRef}
      >
        {children}
      </wx-button>
);


小程序组件 boolean 类型的属性

<img lazy-load={1} />
5
小程序同构页面优化



腾讯课堂小程序原本大小大约是1350k,同构页面基本开发完后,构建出的页面分包大小约800k,页面甚至比主包还要多出200k


主要打包内容分析

  • React:由于使用 Kbone 能够真正引入 Vue、React 的运行时,最终的代码包也会完整包含这些库的代码。本次同构页面引入压缩后的 react + react-dom 大约是120k

  • Kbone适配层组件:Kbone 通过两个 npm 包 miniprogram-elementminiprogram-render 来提供基础的 dom/bom api,这两个包在压缩后上传约180k

  • 业务wxss代码:压缩前约350k

  • 业务js代码:压缩前约340k

  • 其中1、2部分属于第三方库,不容易进一步优化,于是我们把目光聚焦在构建出的业务代码。

精简 css

业务 wxss 代码达到 350k 显然是不正常的。经过排查,PostCSS 转换出的 wxss 文件中包含了两块“庞然大物”:base64的背景图和iconfont


  1. 其中背景图是当前需求引入的,我们将背景图上传到cdn上,并设置图片加载失败时的背景色,避免将图片资源打包入 css 中。

  2. 而 iconfont 在小程序中本身就有一份设置在了全局样式中,没有必要重复打包一份到页面级的 wxss。于是这里选择复用小程序公共样式,对 H5 和小程序分开处理。首先抽出 iconfont 相关的 css 文件 catefont.css,在构建 H5 时引入,而在构建小程序时将其去除。

import './index.css';

/* strip-block--h5-only:begin */
import './catefont.css';
/* strip-block--h5-only:end */

在去除了背景图和重复样式(iconfont)后,wxss仅剩约90k(压缩前)。


精简js

  1. npm 包和小程序代码复用,通过前面 Webpack 构建实现。

  2. Tree Shaking,通过前面 Webpack 构建实现。

精简后,js 仅剩约100k(压缩前)。

最终代码约420k,其中包含约300k的第三方库。活动页面是一个分包,对主包大小不造成影响。




6
总结


就目前 Kbone 实现的同构小程序效果来看还是不错的:

  • 开发体验:低成本接入现有 H5 项目,并只需要针对 process.env.isMiniprogram 做小程序端特有的逻辑,其他完全与开发 H5 无异。

  • 性能质量:由于实践的页面结构相对简单,所以流畅性基本可以与原生开发一样。后续实践将会针对结构复杂的页面研究其性能。

从上面开发实践来看,虽然已实现 H5 和小程序同构,但仍有一些可以改进优化的地方,例如 webpack-strip-block 这个 loader,它通过注释包裹的方式来区分 H5 端和小程序端的依赖引入(import),在 vscode 下可能会自动修正依赖引入的顺序,导致注释包裹内的依赖乱掉并影响到程序正常的运行,所以后续需要通过另外一种方式来优化这一部分。




IMWeb 团队隶属腾讯公司,是国内最专业的前端团队之一。

我们专注前端领域多年,负责过 QQ 资料、QQ 注册、QQ 群等亿级业务。目前聚焦于在线教育领域,精心打磨 腾讯课堂、企鹅辅导 及 ABCMouse 三大产品。

扫码关注 腾讯IMWeb前端团队 

登录查看更多
3

相关内容

React.js(React)是 Facebook 推出的一个用来构建用户界面的 JavaScript 库。

Facebook开源了React,这是该公司用于构建反应式图形界面的JavaScript库,已经应用于构建Instagram网站及 Facebook部分网站。最近出现了AngularJS、MeteorJS 和Polymer中实现的Model-Driven Views等框架,React也顺应了这种趋势。React基于在数据模型之上声明式指定用户界面的理念,用户界面会自动与底层数据保持同步。与前面提及 的框架不同,出于灵活性考虑,React使用JavaScript来构建用户界面,没有选择HTML。Not Rest

FPGA加速系统开发工具设计:综述与实践
专知会员服务
65+阅读 · 2020年6月24日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
235+阅读 · 2020年5月21日
Python计算导论,560页pdf,Introduction to Computing Using Python
专知会员服务
73+阅读 · 2020年5月5日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
87+阅读 · 2019年11月25日
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
硬核实践经验 - 企鹅辅导 RN 迁移及优化总结
IMWeb前端社区
5+阅读 · 2019年5月6日
职人沙龙 | 走进小打卡,小程序技术实战交流
使用 C# 和 Blazor 进行全栈开发
DotNet
6+阅读 · 2019年4月15日
支持多标签页的Windows终端:Fluent 终端
Python程序员
7+阅读 · 2019年4月15日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
WebAssembly在QQ邮箱中的一次实践
IMWeb前端社区
13+阅读 · 2018年12月19日
刚开始学编程?这几款小工具能让你事半功倍
Arxiv
92+阅读 · 2020年2月28日
Arxiv
3+阅读 · 2018年4月18日
Arxiv
15+阅读 · 2018年4月3日
Arxiv
17+阅读 · 2018年4月2日
Arxiv
3+阅读 · 2018年3月2日
VIP会员
相关VIP内容
FPGA加速系统开发工具设计:综述与实践
专知会员服务
65+阅读 · 2020年6月24日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
235+阅读 · 2020年5月21日
Python计算导论,560页pdf,Introduction to Computing Using Python
专知会员服务
73+阅读 · 2020年5月5日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
87+阅读 · 2019年11月25日
相关资讯
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
硬核实践经验 - 企鹅辅导 RN 迁移及优化总结
IMWeb前端社区
5+阅读 · 2019年5月6日
职人沙龙 | 走进小打卡,小程序技术实战交流
使用 C# 和 Blazor 进行全栈开发
DotNet
6+阅读 · 2019年4月15日
支持多标签页的Windows终端:Fluent 终端
Python程序员
7+阅读 · 2019年4月15日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
WebAssembly在QQ邮箱中的一次实践
IMWeb前端社区
13+阅读 · 2018年12月19日
刚开始学编程?这几款小工具能让你事半功倍
Top
微信扫码咨询专知VIP会员