shinken008 / blog

不常维护,请移步 https://shinken008.github.io/
0 stars 0 forks source link

Taro@3.x 微信小程序不完全升级指南 #5

Open shinken008 opened 4 years ago

shinken008 commented 4 years ago

前言

Taro@3.0 发版以来,基本上保持在一周的时间发版,主要是修复 bug。通过社区可以了解到,Taro@3.x 发版带来了重大变革,其中对 H5 和小程序场景进行重构,提供了新的特性和新的架构。

Taro@3.0 发版之后,社区一直很活跃,不少用户开始使用一键式(命令)升级。

$ taro update project [version]

敲完之后是这样:

还有这样的:

看到这些我差点笑出声,这是我在 issue 在讨论 3.x 时看到的,这位同学版本应该是敲错了。不得不说逛社区是件很有意思的事情,有的时候看 issue 能发现一些同道中人,你踩的坑别人也在前赴后继[手动尴尬]。

Taro 目前还有很多的 ,现在有 600+ 的 issue 数,平摊到各个平台其实不会很多,咋看一下 V-3 issue 也就几十个。也有可能是用户基数的关系,毕竟刚发版不久,幸存者偏差。这里我不是要劝退想要升级的同学,就像官方说的,”没有枪,没有炮,没有轮子自己造“,不要怂,就是*!首先我们先看看 Taro@3.x 特性。

以下是在小程序场景下升级 Taro@3.x ,其他端升级仅供参考和学习。

Taro@3.x 特性

看到它那么多特性,似乎很诱人,详细参考 官方文档,文档有的东西在这里就不过多的赘述了。

下面是如何用上它呢?如果是新项目那很好办,只需要实现新功能,对于旧项目呢?”买不了吃亏,买不了上当“,以其忍受旧的技术栈,不如升级玩出新花样。

Taro@3.x 是个啥

首先先简单了解下框架原理和解决的问题。

框架

下面是引用官方的一张架构图:

解决了什么问题

从架构层面上讲,Taro 从一个编译型框架变成了一个运行时框架,基本上曾经的 Taro 无法运行的代码在 Taro Next 中完全没有压力。Taro@3.x 在运行时维护了一个 DOM 模型,使得编译的时候不去做 data xml 转化,从而规避掉了编译带来的 bug,同时降低学习成本,PC 端开发的同学也能轻松接入。

看起来挺香的。了解了一些知识后,下面我们先从依赖开始升级。

Taro@3.x 依赖

以下是基于 Taro@2.2.5 升级到基于 React@16.10.0 框架 Taro@lastest(@3.0.7) 的历程。

框架选型

对用户来说,开放式框架给了用户更多地选择,原来的版本只支持的类 React 语法,现在完全可以使用社区的其他框架,比如 Vue 2Vue 3jQuery。在这里因为我是一个 React 的重度用户,比起官方的 Nerv,倾向于 React。而且 React 是一个非常优秀的开源项目,有庞大的团队在维护。这里安利一下 ReactReact@17.0 发布了,只是一个过渡版本,消除了设计上的隐患,帮助用户更安全的升级过渡,新的功能特性放在 React@18 发布,有没有一种服务到家的感觉(这里有点要黑 Taro@3.x break change 式升级的嫌疑)。

跑题了,下面继续介绍 Taro升级,下面是选择 React 框架需要改造的地方:

Babel@7

Taro@2.x 使用的 babel@6,了解 babel 同学应该知道 6.x 版本和 7.x 版本的差异,比如 Presets ,新的 Proposals 等命名,babel@7 使用 @babel/x scope 代替 babel/x,防止被占用。

相关依赖包升级

这里我使用的是手动升级,先删除对应老版本的 npm 包,再安装新版本

npm i @babel/runtime @tarojs/components @tarojs/runtime @tarojs/taro @tarojs/react react-dom@16.10.0 react@16.10.0
npm i -D @tarojs/cli @types/webpack-env @types/react @tarojs/mini-runner @babel/core @tarojs/webpack-runner babel-preset-taro eslint-config-taro eslint eslint-plugin-react eslint-plugin-import eslint-plugin-react-hooks stylelint

各个端的相关包。比如原生小程序转过来会用 with-weapp 包一层

npm i @tarojs/with-weapp // withWeapp 接受一个小程序规范的 Page/App 构造器参数,转换为对应框架规范的组件实例
npm i @tarojs/taro-weapp // 微信小程序
npm i @tarojs/taro-alipay //解决方案支付宝小程序

新增的包。这里使用安装 babel presets 或者插件,因为我偷懒,安装 babel-preset-taro,然后在 babel-config.js 配置上老项目使用的插件和预设,参考文档配置或者 代码

npm i @babel/runtime @tarojs/react
npm i @tarojs/runtime // Taro 运行时。在小程序端连接框架(DSL)渲染机制到小程序渲染机制,连接小程序路由和生命周期到框架对应的生命周期。在 H5/RN 端连接小程序生命周期规范到框架生命周期
npm i -D @babel/core babel-preset-taro // babel 相关

删除 babel@7 以下的包。安装 babel-preset-taro 可配置相关的 babel,如果有些 babel presets pluginsbabel-preset-taro 没有集成的话,请手动安装

npm remove babel-plugin-transform-class-properties babel-plugin-transform-decorators-legacy babel-plugin-transform-jsx-stylesheet babel-plugin-transform-object-rest-spread babel-plugin-transform-runtime babel-preset-env babel-runtime @tarojs/plugin-babel

删除老的 nerv 框架相关的包

npm remove nervjs nerv-devtools

配置更改

Taro@3.x 业务代码改造

页面 config 独立文件

Taro 1.x,2.x 时,配置是在页面实例里面去配置 config,编译后输出 x.json 配置文件。在 Taro@3.x 里,需要在同级目录里新增一个独立的 JS 文件去配置:x.config.js

在这里需要注意的一点是,跟页面代码一起的 config 会通过 webpack 编译,而独立成 x.config.js 只会被 babel-register 进行编译,在项目里刚好 config 配置的 pages 常量是根据不同的场景定义的 webpack 常量,所以 config.js 取不到 webpack 常量配置报错。解决方案是,使用 babel-plugin-transform-define,主要不能与 webpack 常量冲突,需要额外定义 config.js 里面的常量。这种解决方式毕竟不优雅,官方考虑在后续添加这个特性,相关issue

config/index 配置文件

页面实例的改动

Taro@3.x 多场景下框架的兼容

Taro@2.x ,小程序分包我们可以使用宿主小程序的框架,但在升级 Taro@3.x过程中,我们需要分析 2.x 和 3.x 框架版本能不能做到共用。这个章节我们针对主包、分包和插件场景下对小程序的运行、打包和包大小进行分析,演示多场景框架升级的可行性。

独立运行

在58小程序体系里面,经常会有同一份源码编译到不同端的场景,以及借助内部工程化管理工具 MPS 更为便捷的管理分包,组件。在分包和插件的场景下,是一份这样的目录结构:

分包

在分包场景中,为了减少包大小,分包尽量减少了依赖第三方 vendors 的注入,比如 Taro,共用主包的 vendor,删掉了无效的 app.xproject.config.json。在升级 Taro@3.x 的过程中,由于框架机制的改变,而无法做到与低于 3.0 的版本做到框架的共用。因为在3.x,各个 Page 依赖入口文件 app.js createReactApp(App: React.ComponentClass, react: typeof React, reactdom, config: AppConfig) 创建的 Current(包含 app , router , page )和 Reconciler 实例,就是 page 依赖执行 app.js 文件(参考代码实现)。但是分包的 app.js 不是作为入口文件执行的,咋办呢?

我想到的是在各个文件 require('./**/app.js'),在加载完整个分包后执行 app.js,实现 App 的实例化。

// app.js
export default class App extends React.Component {
  componentDidMount() {
    console.log('app.onLaunch Taro.Current:', Taro.Current);
  }
  render() {
    return this.props.children;
  }
}
// home/list.js
export default class Home extends Component {
  componentDidMount() {
    console.log('page.onLoad Taro.Current:', Taro.Current);
  }
  componentDidShow() {
    console.log('page.onShow Taro.Current:', Taro.Current);
  }
  render() {
    return (
      <View>hello world</View>
    )
  }
}

可以看到,在入口文件我们取到实例,且在 Page 页面也能取到实例。但是有个问题发现了没,我们是跟主包共用一个 App 实例,这会导致我们改掉 App 实例的内容影响到主包。

注入 app.js 方案在分包场景是没有问题的,但是怎么解决 App 实例影响问题呢?这个问题等下再回答,我们再来以插件场景进行尝试这个方案。

插件

还是跟分包一样的入口文件和 page 页面。我们先看下编译后的 app.js 代码长啥样:

这就是 Taro@3.x 编译后的入口文件,下面我们引入这个插件:

发现在插件模式下,微信小程序没有提供 App 实例。

分析分包和插件页面加载方案

从上面可以看出,require('./**/app.js')方案 从运行机制上来看可行,但是存在一些问题:

怎么解决上面的问题呢?所以我们想到,我们能不能在分包和插件自己实现一个 App 函数,并且提供一个 onLaunch 空方法,让taro-runtime 能够执行createReactApp 呢?下面是我们使用插件 @mps/mps-taro-plugin/dist/MpsRuntimeTaroPlugin 实现了一个 App 函数:

export function App(config) {
  config.onLaunch({})
}

并且通过 webpack.ProvidePlugin 注入到 app.js 中,打包后的文件是这样的:

我们再来手动引入 app.js 试试,没有报错,程序能正常运行。

我们不能每个 Page 都手动注入 app.js 吧?接下来如何给每个 Page 引入 app.js 呢?这时候我们可以通过 webpack 插件做这件事情,刚好我们现有 @mps/mps-taro-plugin/dist/MpsBusinessTaroPlugin 已经提供了这个能力,安装 @mps/mps-taro-plugin@3.0.2 版本。下面是提供 App 函数和给每个 Page 注入 app.js 的插件配置:

plugins: [
    '@mps/mps-taro-plugin/dist/MpsRuntimeTaroPlugin',
   ['@mps/mps-taro-plugin/dist/MpsBusinessTaroPlugin', {
      commonChunks: ['app'], 
   }],
]

撒花!通过以上的方式解决了多场景下框架版本的兼容性问题。

打包文件

入口文件处理

多场景如何定义入口文件,这是一个问题,我们经常在入口文件主包需要引用一一些 sdk 之外,然而在分包和插件场景并不需要使用到。比如在 Taro2.x 的是按 WEBPACK_CONST 条件将 import 进来模块赋值给已经赋值过的变量,引入 target 模块定义的上下文在没有使用的时候会被 tree-shaking 掉,其实这应该是一个 bug。或者说是现在的 webpack + uglyfy(terser) 目前没有实现的一个特性(类消除也是后面实现的),理论上编译器只有在静态分析100%确定没问题的情况下才会删,不会去分析程序流,意味着你的分包和插件在不使用的一些代码都会被打包进来。听起来可能不好理解,我们直接看代码,感兴趣的同学可以尝试下,分别放在 Taro@3.xTaro@2.x 编译:

// App.js 入口文件
import { cube } from './util.js';
let value = null;
if (WEBPACK_CONST) { // 场景常量,WEBPACK_CONST = false
    value = cube(2);
}

// utils.js
console.log('before utils');
export function square(x) {
  console.log('square'); 
  return x * x;
}

export function cube(x) {
  console.log('cube');
  return x * x * x;
}

如果入口文件沿用 Taro@2.x 的写法会带来几个问题:

解决上面问题很简单,我们用 webpak + require + defineConstants 的方式对各个场景按需引入,比如:if (WEBPACK_CONST) { require('..') },当 WEBPACK_CONST (编译常量)为 false 的时候,Webpack 编译分析不会把条件外的 require 内容引入进来。有了这个结论我们就对入口文件进行了处理:

// app.js
let App = () => null;
if (WEBPACK_CONST === 'a') {
  App = require('./app.a').default;
}
if (WEBPACK_CONST === 'b') {
  App = require('./app.b').default;
}
if (WEBPACK_CONST === 'c') {
  App = require('./app.c').default;
}

export default App;

// app.a.js
import React from 'react'
import Taro from "@tarojs/taro"

export default class App extends React.Component {
  componentDidMount() {
    console.log('app.onLaunch Taro.Current:', Taro.Current);
  }
  render() {
    return this.props.children;
  }
}

咋一看,上面的文件是解决了,但是不太优雅,如果支持配置指定入口文件是不是更好。可惜的是, Taro@3.x 在小程序场景中的将入口文件写死了 app.x,所以不得不使用这种方式去做到分入口加载。感兴趣的同学可以看看这个 PRentryFileName 其实已经支持在 H5RN 里面配置。

包大小

玩游戏最怕的是”一顿操作猛如虎,一看战绩零杠五“。要是包大小不通过,所有的解决方案都是白忙活。下面是我通过开发者工具统计的包大小信息:

Taro@2.2.5 Taro@3.0.7
主包 1469.0kb 1369.0kb
分包 1246.1kb 1235.0kb
插件 1493.0kb 1456.0kb

根据上面统计的信息,在主包,分包和插件场景下,现有的包大小没有超过原来的包大小。

总结

在这里,我们可以认为升级 Taro@3x 在现有的58体系里面是可行的,改动的范围也是可以接受的。

我是从发布不久开始接入,可以说从 Taro@3.0.2 一路踩坑过来的,开始的时候因为 api 改动大有点打击信心,但随着了解了一些运行机制之后和简单的开发体验后,越发觉得 Taro@3.x 颠覆式的重构反而能让 Taro 走的更远。有兴趣的同学可以按照上面的步骤进行升级,基本上没啥问题,有啥问题可以给提 issuePR,官方人手不多,一起帮忙加特性(改 bug )。还是那句话,”没有枪,没有炮,没有轮子自己造“。

参考资料

[1] Taro 3 正式版发布:开放式跨端跨框架解决方案: https://aotu.io/notes/2020/06/30/taro-3-0-0/index.html

[2] 从旧版本迁移到 Taro Next: https://nervjs.github.io/taro/docs/migration

[3] Taro Next 发布预览版: [https://juejin.im/post/6844904063675400199](

rottenpen commented 4 years ago

学习了

wuchangming commented 4 years ago

👍