京东质量社区 京东到家小程序 - 在性能及多端能力的探索实践 | 京东云技术团队

京东云开发者 · 2023年06月29日 · 4631 次阅读

一、前言

京东到家小程序最初只有微信小程序,随着业务的发展,同样的功能需要支持容器越来越多,包括支付宝小程序、京东小程序、到家 APP、京东 APP 等,然而每个端分开实现要面临研发成本高、不一致等问题。

为了提高研发效率,经过技术选型采用了 taro3+ 原生混合开发模式,本文主要讲解我们是如何基于 taro 框架,进行多端能力的探索和性能优化。

二、多端能力的探索

1.到家小程序基于 taro3 的架构流程图

框架分层解释

1.配置层:主要包含编译配置、路由配置、分包加载、拓展口子。

2.视图层:主要完成 App 生命周期初始化、页面初始化、注入宿主事件、解析配置为页面和组件绑定事件和属性。

3.组件库:是一个单独维护的项目,多端组件库包括业务组件和原子组件,可由视图层根据配置动态加载组件。

//渲染主入口
 render() {
    let { configData, isDefault, isLoading } = this.state;
    const pageInfo = { ...this.pageInfoValue, ...this._pageInfo }
    return (
        <MyContext.Provider value={pageInfo}>
            <View
                className={style.bg}
            > 
                {//动态渲染模板组件
                    configData &&
                    configData.map((item, key) => {
                        return this.renderComponent(item, key);
                    })
                }
            </View>
            {isLoading && <Loading></Loading>}
        </MyContext.Provider>
    );
}

 //渲染组件 注入下发配置事件和属性
 renderComponent(item, key) {
    const AsyncComponent = BussinesComponent[item.templateName];
    if (AsyncComponent) {
        return (
            <AsyncComponent
                key={key}
                dataSource={item.data}
                {...item.config}
                pageEvent={pageEvent}
            ></AsyncComponent>
        );
    } else {
        return null;
    }
}

4.逻辑层:包括业务处理逻辑,请求、异常、状态、性能、公共工具类,以及与基础库对接的适配能力。

5.基础库: 提供基本能力,定位、登录、请求、埋点等基础功能,主要是抹平各端基础功能的差异。

2、基础库

1.统一接口,分端实现,差异内部抹平

关于基础库我们采用分端实现的方式,即统一接口的多端文件。

基础库如何对接在项目里,修改 config/index.js,结合 taro 提供的 MultiPlatformPlugin 插件编译。


const baseLib = '@dj-lib/login' 
//增加别名,便于后续基础库调整切换
alias: {
  '@djmp': path.resolve(__dirname, "..", `./node_modules/${baseLib}/build`),
},
//修改webpack配置,h5和mini都要修改
webpackChain(chain, webpack) {
    chain.resolve.plugin('MultiPlatformPlugin')
      .tap(args => {
        args[2]["include"] = [`${baseLib}`]
        return args
      })
  }

业务里使用方式

import { goToLogin } from '@djmp/login/index';

goToLogin()

2.高复用

基础库不应该耦合框架,那么基础库应该如何设计,使其既能满足 taro 项目又能满足原生项目使用呢?

npm 基础库 在 taro 经过编译后生成为 vendors 文件

npm 基础库 在小程序原生项目 npm 构建后 生成 miniprogram_npm

一样的基础库经过编译后会存在 2 种形态,多占了一份空间呢。

我们对小程序包体积大小是比较敏感的,为了节约空间,那么如何让 taro 使用小程序的 miniprogram_npm 呢?

先简单说一下思路,更改 webpack 的配置项,通过 externals 配置处理公共方法和公共模块的引入,保留这些引入的语句,并将引入方式设置成 commonjs 相对路径的方式,详细代码如下所示。

const config = {
  // ...
  mini: {
    // ...
    webpackChain (chain) {
      chain.merge({
        externals: [
          (context, request, callback) => {
            const externalDirs = ['@djmp/login']
            const externalDir = externalDirs.find(dir => request.startsWith(dir))

            if (process.env.NODE_ENV === 'production' && externalDir) {
              const res = request.replace(externalDir, `../../../../${externalDir.substr(1)}`)
              return callback(null, `commonjs ${res}`)
            }
            callback()
          },
        ],
      })
    }
    // ...
  }
  // ...
}

3、组件库

想要实现跨端组件,难点有三个

第一:如何在多个技术栈中找到最恰当的磨平方案,不同的方案会导致 开发适配的成本不同,而人效提升才是我们最终想要实现的目的;

第二:如何在一码多端实现组件之后,确保没有对各个组件的性能产生影响

第三:如何在各项目中进行跨端组件的使用

基于以上,在我们已经确定的以 Taro 为基础开发框架的前提下,我们进行了整体跨端组件方案实现的规划设计:

在组件层面,划分为三层:UI 基础组件和业务组件 为最底层;容器组件是中间层,最上层是业务模板组件;我们首先从 UI 基础组件与业务组件入手,进行方案的最终确认;

调研过程中,UI 组件和业务组件主要从 API、样式、逻辑三个方面去调研跨端的复用率:

经过以上调研得出结论:API 层面仍需要使用各自技术栈进行实践,通过属性一致的方式进行 API 层面的磨平;样式上,基础都使用 Sass 语法,通过 babel 工具在转化过程中生成各端可识别的样式形式;逻辑上基本是平移,不需要做改动;所以当我们想做跨端组件时,我们最大工作量在于:API 的磨平和样式的跨端写法的探索;

例:图片组件的磨平:

基于以上,跨端组件的复用方案经过调研是可行的,但是接下来,我们该如何保证转化后的组件能够和原生组件的性能媲美呢?我们的跨端组件又该如何在各个项目中使用呢?

在这个过程中,我们主要调研对比两种方案:

第一:直接利用 Taro 提供的跨端编辑功能进行转换,转换编译成 RN . 微信小程序 以及 H5;

第二:通过 babel 进行编译,直接转换成 RN 原生代码,微信小程序原生代码,以及 H5 原生代码

对比方向 原码大小 编译成本 生成的组件性能
Taro 直接编译 大(携带了 Taro 环境) 中(Taro 直接提供,但需要各端调试) 与原生相同
通过 babel 转义 小(只有当前组件的源码代码) 中(需要开发 Babel 转义组件) 与原生相同

经过以上几组对比,我们最终选用了 babel 转义的方式。在项目中使用时,发布到 Npm 服务器上,供各个项目进行使用。

方案落地与未来规划:

在确认整体的方案方向之后,我们进行了项目的落地,首先搭建了跨端组件库的运行项目:能够支持预览京东小程序、微信小程序以及 H5 的组件生成的页面;以下是整个组件从生成到发布到对应项目的全部流程。

目前已经完成了个 5 种 UI 组件的实现,4 种业务组件;其中优惠券模块已经落地在到家小程序项目中,并已经沉淀了跨端组件的设计规则和方案。未来一年中,会继续跨端组件的实现与落地,从 UI、业务层到复杂容器以及复杂页面中。

4、工程化构建

1.构建微信小程序

因为存在多个 taro 项目由不同业务负责,需要将 taro 聚合编译后的产物,和微信原生聚合在一起,才能构成完整的小程序项目。

下面是设计的构建流程。

为了使其自动化,减少人工操作,在迪迦发布后台( 到家自研的小程序发布后台 创建依赖任务即可,完成整体构建并上传。

其中执行【依赖任务】这个环节会进行,taro 项目聚合编译,并将产物合并到原生项目。

迪迦发布后台

2.构建京东小程序

yarn deploy:jd 版本号 描述

//集成CI上传工具 jd-miniprogram-ci
const { upload, preview } = require('jd-miniprogram-ci')
const path = require('path')
const privateKey = 'xxxxx'
//要上传的目录-正式
const projectPath = path.resolve(__dirname, '../../', `dist/jddist`)
//要上传的目录-本地调试
const projectPathDev = path.resolve(__dirname, '../../', `dist/jddevdist`)
const version = process.argv[2] 
const desc = process.argv[3]
//预览版
preview({
    privateKey: privateKey,
    projectPath: projectPathDev,
    base64: false,
})
//体验版
upload({
    privateKey: privateKey,
    projectPath: projectPath,
    uv: version,
    desc: desc,
    base64: false,
})

3.构建发布 h5

yarn deploy:h5

h5 的应用通常采用 cdn 资源 +html 入口 这种模式。先发布 cdn 资源进行预热,在发布 html 入口进行上线。

主要进行 3 个操作

1.编译出 h5dist 产物,即 html+ 静态资源

2.静态资源,利用集成 @jd/upload-oss-tools 工具上传到 cdn。

3.触发【行云部署编排】发布 html 文件入口

关于 cdn: 我们集成了 cdn 上传工具,辅助快速上线。


//集成 @jd/upload-oss-tools上传工具
const UploadOssPlugin = require("@jd/upload-oss-tools");
const accessKey = new Buffer.from('xxx', 'base64').toString()
const secretKey = new Buffer.from('xxx', 'base64').toString()

module.exports = function (localFullPath, folder) {
  return new Promise((resolve) => {
    console.log('localFullPath', localFullPath)
    console.log('folder', folder)
    // 初始化上传应用
    let _ploadOssPlugin = new UploadOssPlugin({
      localFullPath: localFullPath, // 被上传的本地绝对路径,自行配置
      access: accessKey, // http://oss.jd.com/user/glist 生成的 access key
      secret: secretKey, // http://oss.jd.com/user/glist 生成的 secret key
      site: "storage.jd.local", 
      cover: true, // 是否覆盖远程空间文件 默认true
      printCdnFile: true, // 是否手动刷新cdn文件 默认false
      bucket: "wxconfig", // 空间名字 仅能由小写字母、数字、点号(.)、中划线(-)组成
      folder: folder, // 空间文件夹名称 非必填(1、默认创建当前文件所在的文件夹,2、屏蔽字段或传undefined则按照localFullPath的路径一层层创建文件夹)
      ignoreRegexp: "", // 排除的文件规则,直接写正则不加双引号,无规则时空字符串。正则字符串,匹配到的文件和文件夹都会忽略
      timeout: "", // 上传请求超时的毫秒数 单位毫秒,默认30秒
      uploadStart: function (files) { }, // 文件开始上传回调函数,返回文件列表参数
      uploadProgress: function (progress) { }, // 文件上传过程回调函数,返回文件上传进度
      uploadEnd:  (res) =>{
        console.log('上传完成')
        resolve()
      },
      // 文件上传完毕回调函数,返回 {上传文件数组、上传文件的总数,成功数量,失败数量,未上传数量
    });
    _ploadOssPlugin.upload();
  })
}

三、性能优化

性能优化是一个亘古不变的话题,总结来说优化方向:包下载阶段、js 注入阶段、请求阶段、渲染阶段。

以下主要介绍在下载阶段如何优化包体积,请求阶段如何提高请求效率。

(一)体积优化

相信使用过 taro3 的同学,都有个同样的体会,就是编译出来的产物过大,主包可能超 2M!

1.主包是否开启

优化主包的体积大小 :optimizeMainPackage。

像下面这样简单配置之后,可以避免主包没有引入的 module 不被提取到commonChunks中,该功能会在打包时分析 module 和 chunk 的依赖关系,筛选出主包没有引用到的 module 把它提取到分包内。

  module.exports = {
  // ...
  mini: {
    // ...
    optimizeMainPackage: {
      enable: true,
    },
  },
}


2.使用压缩插件 terser-webpack-plugin

//使用压缩插件
   webpackChain(chain, webpack) {
     chain.merge({
       plugin: {
         install: {
           plugin: require('terser-webpack-plugin'),
           args: [{
             terserOptions: {
               compress: true, // 默认使用terser压缩
               keep_classnames: true, // 不改变class名称
               keep_fnames: true // 不改变函数名称
             }
           }]
         }
       }
     })
   }

3.把公共文件提取到分包。

mini.addChunkPages为某些页面单独指定需要引用的公共文件。

例如在使用小程序分包的时候,为了减少主包大小,分包的页面希望引入自己的公共文件,而不希望直接放在主包内。那么我们首先可以通过 webpackChain 配置 来单独抽离分包的公共文件,然后通过 mini.addChunkPages 为分包页面配置引入分包的公共文件,其使用方式如下:

mini.addChunkPages 配置为一个函数,接受两个参数

pages 参数为 Map 类型,用于为页面添加公共文件

pagesNames 参数为当前应用的所有页面标识列表,可以通过打印的方式进行查看页面的标识

例如,为 pages/index/index 页面添加 eatingmorning 两个抽离的公共文件:


mini: {
    // ...
    addChunkPages(pages: Map<string, string[]>, pagesNames: string[]) {
      pages.set('pages/index/index', ['eating', 'morning'])
    },
  },

4.代码分析

如果以上方式,还达不到我们想要的效果,那么我们只能静下心来分析下 taro 的打包逻辑。

可以执行 npm run dev 模式查看产物里的xxx .LICENSE.txt 文件, 里面罗列打包了哪些文件,需要自行分析去除冗余。

以下以 vendors.LICENSE.txt 为例

runtime.js: webpack 运行时入口 ,只有 2k,没有优化空间。

taro.js: node_modules 中 Taro 相关依赖,112k,可以魔改源码,否则没有优化空间。

vendors.js: node_modules 除 Taro 外的公共依赖,查看vendors.js.LICENSE.txt文件分析包括哪些文件

common.js: 项目中业务代码公共逻辑,查看common .js.LICENSE.txt文件分析包括哪些文件

•app.js app 生命周期中依赖的文件。查看 app .js.LICENSE.txt文件分析包括哪些文件

•app.wxss 公共样式文件 ,看业务需求优化,去除非必要的全局样式。

•base.wxml 取决于使用组件的方式,可优化空间较小。

(二)网络请求优化:

相信大家的业务里有多种类型的请求,业务类、埋点类、行为分析、监控、其他 sdk 封装的请求。然而在不同的宿主环境有不同的并发限制,比如,微信小程序请求并发限制 10 个,京东等小程序限制为 5 个。

如下图,以微信小程序为例,在请求过多时,业务与埋点类的请求争抢请求资源,造成业务请求排队,导致页面展示滞后,弱网情况甚至造成卡顿。

那么基于以上问题,如何平衡业务请求和非业务请求呢?

这里我们有 2 个方案:

1.动态调度方案 https://www.cnblogs.com/rsapaper/p/15047813.html

思路就行将请求分为高优和低优请求,当发生阻塞时,将高优请求放入请求队列,低优进入等待队列。

请求分发器 QueueRequest:对新的请求进行分发。

◦加入等待队列:正在进行的请求数超过设置的 threshold,且请求为低优先级时;

◦加入请求池:请求为高优先级,或并发数未达到 threshold。

等待队列 WaitingQueue:维护需要延时发送的请求等待队列。在请求池空闲或请求超过最长等待时间时,补发等待请求。

请求池 RequestPool:发送请求并维护所有正在进行的请求的状态。对外暴露正在进行的请求数量,并在有请求完成时通知等待队列尝试补发。

2.虚拟请求池方案

该思路是将微信的 10 个请求资源,分成 3 个请求池,业务请求:埋点类:其他请求的比例为 6:2:2。比例可以自行调整。

这样各类型请求都在自己的请求池,不存在争抢其他请求池资源,保障了业务不被其他请求阻塞。

实现方式

方案对比

优缺点 动态调度(方案一) 虚拟请求池(方案二)
拓展性
成本 (开发、测试、维护)
请求效率

2 个方案都可以完成请求资源的分配,但结合业务实际采用的是虚拟请求方案,经测试在弱网情况下,请求效率可以提升15%.

四、总结和展望

未来一定是一码多端的方向,所以我们未来在基础建设上会投入更多的精力,包括框架层升级优化、基础库建设、组件库建设、工程化建设快速部署多端。

在性能优化上我们还可以探索的方向有京东小程序分包预加载、分包异步化、京东容器 flutter 渲染、腾讯 skyLine 渲染引擎等。

在团队沟通协作上会与 Taro 团队、京东小程序容器团队、nut-ui、拼拼等团队进行学习沟通, 也希望能与大家合作共建。

五、结束语

京东小程序开放平台是京东自研平台,提供丰富的开放能力和底层的引擎支持,目前有开发者工具、转化工具、可视化拖拽等多种开发工具可供内部研发同事使用,提升开发质量同时快速实现业务功能的上线。内部已有京东支付、京东读书、京东居家等业务使用京东小程序作为技术框架开展其业务。

如您想深入了解和体验京东小程序,可前往京东小程序官网(https://mp.jd.com/?entrance=shendeng)查看更多信息。

参考:

https://www.cnblogs.com/rsapaper/p/15047813.html

https://taro-docs.jd.com/docs/next/config-detail#minioptimizemainpackage

https://taro-docs.jd.com/docs/next/dynamic-import

https://zhuanlan.zhihu.com/p/396763942

作者:京东零售 邓树海、姜微

来源:京东云开发者社区

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册