只要这么做,你也能用 taro 搞掂多端开发!

lxf2023-03-12 16:34:01

1. 前言

使用 Taro 开发有一段时间了,但之前一直只是基于 Taro 使用 React 快速开发微信小程序,并没有考虑到其他端的运行情况。

最近有一个需求,为增大访问流量,需要把微信小程序端的某个子模块输出到 H5 端,并以 Hybrid 的形式嵌入到其他小程序和 APP 上。

本文主要总结如何使用 Taro 兼容运行H5与微信小程序,以便日后兼容其他端有法可依。

2. 方案分析

最开始跟小伙伴讨论过一个方案是,打算另起一个 H5 项目,并改造原来的微信小程序,使用这个 H5 项目替代原来的子模块。

这个方案的缺点:

  • 微信小程序需要改造。
  • 以后该模块要在字节 APP、百度 APP 中运行,必须先创建一个壳子小程序,而不能直接编译输出字节小程序、百度小程序。
  • 后续要输出 RN 模块,又得改造一番。

考虑后续其他模块也可能需要处理为 h5,所以,基于 Taro 编译输出 h5 是目前最好的解决方案,在用户体验不差的情况下,「Write Once, Run Everywhere」 给予开发者极大的开发体验,极大的节省公司的开发成本。

当然使用 Taro 进行多端适配,要求也高,需要考虑一处编写,多端填坑的情况。

3. 多端开发策略

多端开发要解决的核心问题有两个:

  • 代码转换:使代码可以在不同平台上运行。
  • 运行时适配:使代码在不同平台上有相同表现。

Taro 编译让代码在各个平台上能够运行起来,就是对输入的源代码进行语法分析,语法树构建,随后对语法树进行转换操作再解析生成目标代码的过程。

但是纯靠编译是不行的,这是因为小程序和 Web 端上组件标准与 API 标准有很大差异,这些差异仅仅通过代码编译手段是无法抹平的。

例如你不能直接在编译时将小程序的 <view/> 直接编译成 <div/>,因为他们虽然看上去有些类似,但是他们的组件属性有很大不同的。比如 hover-start-timehover-stay-time 等属性在常规 Web 开发中并不存在。

针对这样的情况,Taro 采用了定制一套运行时标准来抹平不同平台之间的差异。这一套标准主要以三个部分组成,包括标准运行时框架标准基础组件库标准端能力 API

其中运行时框架和端能力 API 对应 @taro/taro,组件库对应 @tarojs/components,通过在不同端实现这些标准,从而实现差异化。

只要这么做,你也能用 taro 搞掂多端开发!

  • 基础框架(生命周期、组件 API):以 React 的生命周期、组件 api 为基础,小程序的特性 作为补充。
  • 标准组件库(View、Button):以微信小程序组件为标准,各端模拟实现
  • 标准API(request、setState):扩展的小程序标准 Api,隔断模拟实现。

即使 Taro 做了编译时转换、运行时适配,还是无法完全满足开发者的需求,比如一些特殊的端能力、端插件、端组件,这些都需要开发者自行抹平差异,还有不同平台显示的页面可能不同,也需要差异化的配置

比如一些 API 是 Web 端无论如何无法实现的,wx.login,又或者wx.scanCode ,虽然可以通过 Taro.xxx 获取 api 方式,但是 Taro 本身没有做 h5 的兼容处理。

Taro 维护了一个 API 实现情况的列表,在开发中应该尽可能避免使用它们。

3.1 多端同步调试

多端开发,需要编译多个版本,这里输出 H5 和微信小程序版本。

Taro 提供了内置环境变量 process.env.Taro_ENV 来区分不同编译环境:weapp / swan / alipay / tt / qq / jd / h5 / rn

首先在 config/index.js 文件中改写输出配置:

// before 只输出微信小程序
outputRoot: `dist`
// after 输出多份目标代码
outputRoot: `dist/${process.env.TARO_ENV}`

然后打开两个命令行终端,分别运行 npm run dev:h5npm run dev:weapp 打包命令

"scripts": {
    "dev:h5": "taro build --type h5 --watch",
    "dev:weapp": "taro build --type weapp --watch",
},

因为基于现有项目改造,最初都无法成功编译运行 H5 页面,大量的报错,只能先把一些适配小程序端的能力注释掉,比如:

  • 插件:异常上报插件、语音识别
  • 组件:地图组件、画布 Canvas、movableView 等
  • 埋点上报:神策sdk
  • api:获取图片信息、图片识别编辑相关的 canvas 元素获取

最终在 dist 目录输出以下文件,并成功编译运行 h5 页面:

dist
- h5
- weapp # 这里打开微信小程序开发者工具,需要定位到该文件夹

3.1.1 h5 跨域问题

如何解决本地开发 h5 跨域问题呢?在 Taro 项目中可以设置代理服务器:

// config/index.js
const config = {
    h5: {
        devServer: {
            port: 10086,
            proxy: [
                {
                    context: ['/freight-retail', '/restApi'],
                    target: 'https://jecyu.sit.com', // 服务端地址
                    changeOrigin: true
                }
            ]
        }
    }
}

然后在请求接口路径或静态资源路径,更改请求前缀为:本地电脑 ip 地址,比如 http://xxx:10086/,即可解决跨域问题。

业务接口有测试环境域名和生产环境域名两种,比如:

  • jecyu.sit.com
  • jecyu.com

虽然 Taro 针对 npm run build 命令npm run build --watch时分别赋予 NODE_ENV productiondevelopment,但无法区分三种构建环境:

  • 本地开发环境
  • 线上测试环境
  • 线上生产环境

这里我新增一个 DEPLOY_ENV 环境变量,取值为:local/sit/prod,分别创建 localsitprod 配置文件

//  config/local.js
module.exports = {
    env: {
        DEPLOY_ENV: '"local"', // sit、prod 配置类似
    },
    defineConstants: {},
    mini: {},
    h5: {}
}

然后在 config/index.js 入口文件这样处理:

const config = { /**/};
module.exports = function (merge) {
    switch(process.env.DEPLOY_ENV) {
        case 'local':
            return merge({}, config, require('./local'));
        case 'sit':
            return merge({}, config, require('./sit'));
        case 'prod':
            return merge({}, config, require('./prod'));
        default:
            return merge({}, config, require('./local'));
    }
}

最后更改打包命令:

{
    "build:weapp": "cross-env DEPLOY_ENV=\"prod\" taro build --type weapp",
    "buildSit:weapp": "cross-env DEPLOY_ENV=\"sit\" taro build --type weapp",
    "dev:h5": "cross-env DEPLOY_ENV=\"local\" taro build --type h5 --watch",
}

经过上面的配置后,在请求路径就可以更改为这样:

export function isH5() {
    return process.env.TARO_ENV === 'h5';
}
export const isDev = () => ['local', 'sit'].includes(process.env.DEPLOY_ENV || "");
export const isLocal = () => ['local'].includes(process.env.DEPLOY_ENV || "");
let host = 'https://jecyu.com';
if (isDev()) {
    host = 'https://jecyu.sit.com';
}
if (isDev() && isH5()) {
    // 本地 ip 路径
    host = 'http://xxx:10086'; 
}

除了手工更改外,还可以把个人电脑 ip 注入为常量,这样就不用每次都手动改了。

// config/index.js
const path = require('path')
const os = require('os');
function getNetworkIp() {
  let needHost = ''; // 打开的host
  try {
    // 获得网络接口列表
    let network = os.networkInterfaces();
    for (let dev in network) {
      let iface = network[dev];
      for (let i = 0; i < iface.length; i++) {
        let alias = iface[i];
        if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal && !needHost) {
          needHost = alias.address;
        }
      }
    }
  } catch (e) {
    needHost = 'localhost';
  }
  return needHost;
}
const config = {
    // ...
  	defineConstants: { // 注入常量
    	LOCAL_IP: JSON.stringify(getNetworkIp())
	},
}

3.1.2 h5 添加 vconsole

在 h5 环境下,添加 vconsole 查看网络日志和控制台很有必要。

// app.ts
if (isDev() &&isH5()) {
    const vConsole = new VConsole();
    vConsole.show();
}

3.3 多端脚本编写

taro api 有些能力在 h5 端不兼容,或者 h5 会嵌入并调用其他 app 的能力,比如 Taro.getLocation 获取用户定位。

为了更快改写涉及到这个 api 的调用的页面,减少对原页面的改动,解决方案是对 taro api 劫持,也就是重写,并让输入参数与输出结果一致,后续官方支持后直接把之前的逻辑去掉即可。

import Taro from '@tarojs/taro';
if (isH5()) {
    Taro.getLocation = async () => ({
        data: '重写该函数',
        errMsg: 'getLocation:ok'
    });
}

但实践后发现在 h5 端无法重写 Taro.getLocation方法,其他 taro 的 api 也不行,只能另寻方法。

官方推荐这样编写多端脚本:

方式一,通过 process.env.TARO_ENV 判断环境

if (process.env.TARO_ENV === 'weapp') {
    // 加载不同资源
    require('path/to/weapp/name')
    // weapp 脚本
} else if (process.env.TARO_ENV === 'h5') {
    require('path/to/h5/name')
    // h5 脚本
}

方式二,通过文件后缀名编译时加载对应的文件

// set_title.weapp.ts
import Taro from '@tarojs/taro'
export default function setTitle (title) {
    Taro.setNavigationBarTitle({title})
}

// set_title.h5.ts
export default function setTitle (title) {
    document.title = title
}

考虑到每个目标环境一个文件太过冗余,而且 h5 会嵌入到一些原生 APP 上,一些特殊的能力需要调用一些原生 APP 的方法,最终我采用这样的方式:

// 使用 Taro Api 的类型声明
interface MyTaro {
    getLocation(option: Taro.getLocation.Option): Promise<Taro.getLocation.SuccessCallbackResult>;
}
// 默认为 Taro api
export const myTaro: MyTaro = {
    getLocation: Taro.getLocation,
}
function initMyTaroAPI() {
    if (isH5()) {
        myTaro.setNavigationBarTitle = setNavigationBarTitle;
    } else if (isSyApp()) {
        //
    }
}
initMyTaroAPI();

外部使用

import { myTaro } from '@/utils/natives'
myTaro.setNavigationBarTitle('jecyu');

3.4 多端样式编写

在一个样式文件编写:

/*  #ifdef  h5  */
* {
  box-sizing: border-box;
}
taro-view-core,
taro-text-core,
div {
  font-size: 32px;
}
/*  #endif  */

/*  #ifdef  weapp  */
view,
text {
  box-sizing: border-box;
}
/*  #endif  */

多个样式文件:

- app.scss
- app.h5.scss
- app.tsx

在 app.tsx 中引入:

import './app.scss';

3.5 多端 UI 组件适配

跟多端脚本一样,多端 UI 组件适配也可以这样做:

方式一,根据环境变量加载

<View>
{process.env.TARO_ENV === 'weapp' && <ScrollViewWeapp />}
{process.env.TARO_ENV === 'h5' && <ScrollViewH5 />}
</View>

方式二,声明多个平台文件

├── test.tsx Test 组件默认的形式,编译到微信小程序、百度小程序和 H5 之外的端使用的版本
├── test.weapp.tsx Test 组件的微信小程序版本
├── test.swan.tsx Test 组件的百度小程序版本
└── test.h5.tsx Test 组件的 H5 版本

外部使用:

import Test from '@/components/test'
<Test />

3.6 多端鉴权

多端鉴权也是一个需要注意的地方,这次要兼容 H5 与微信小程序鉴权:

  • 以 h5 嵌入 jecyuAPP 鉴权
  • 以 h5 嵌入 jecyu 微信小程序鉴权
  • 微信小程序一键登录鉴权

1.首先定义一个鉴权高阶组件,用来包裹需要鉴权的页面

//**
 * 防止用户越过正常流程,例如说直接通过连接访问,或者尝试越权访问
 * taro 目前不支持路由中间件,需开发者在需要鉴权的页面包装多一层
 * <Auth>
 *  <Order/>
 * </Auth>
 */
import { View } from "@tarojs/components";
import Taro from '@tarojs/taro';
import React, { useEffect, useState } from 'react';
import { appAuth } from "@/utils";

/**
 * 状态:
 * 刚进来页面:加载中(后台获取授权)
 * 加载后:授权通过、未授权不能访问,会由 appAuth 跳转到登录页面
 */
export default props => {
    const [hasAuth, setHasAuth] = useState(false);
    const [loading, setLoading] = useState(true);
    const { children, ...params } = props;
    useEffect(() => {
        Taro.showLoading({ title: '页面加载中' });
        appAuth(params)
            .then(({ success }) => {
                setHasAuth(success);
            })
            .catch(() => { setHasAuth(false) })
            .finally(() => {
                Taro.hideLoading();
                setLoading(false);
            });
    }, []);
    if (loading) {
        return null;
    }
    return <View>
        {hasAuth ? children : '当前页面未授权,不能访问'}
    </View>;
};

2.获取当前应用运行环境,然后返回鉴权函数。

// utils/auth.ts
interface AuthFn {
    (params?: any): Promise<{ success: boolean }>
}
export const appAuth = (function () {
  const appEnv = getAppEnv(); // 获取 h5、weapp、其他 APP 环境
  let authFn: AuthFn = async function () {
      alert(`没有找到对应环境${appEnv}的环境`);
      return { success: false };
  };
  if (checkLogin()) {
    authFn = async function () {
       return { success: true };
    }
    return authFn;
  }
  switch (appEnv) {
      case APP_ENV.WEAPP:
          authFn = async () => {
            	const { success } = await wxLogin();
            	return { success };
          };
          return authFn;
      case APP_ENV.H5:
          authFn = async () => {
          		return h5Login();
          };
          return authFn;
      case APP_ENV.JECYU_APP:
          // auth
          // return authFn;
      default:
          return authFn;
  }
})();

3.7 多端线上化部署

3.7.1 微信小程序构建部署

之前为了多端同步调试,构建出来的文件是这样的:

dist
- weapp
- h5

这样的话,为了保持跟之前的部署一致,需要这样处理:

"build:weapp": "cross-env DEPLOY_ENV=\"prod\" taro build --type weapp && cp -r dist/weapp ./ && rm -rf dist && mv weapp dist"

3.7.2 h5 部署构建部署

之所以 h5 要单独说明,是因为之前的 h5 构建流水线都使用了公司统一的 oss 打包脚本,同时打包测试环境 sit 和生产环境 prod 文件。

只要这么做,你也能用 taro 搞掂多端开发! 脚本需要读取 prodDist 文件夹,命令行工具会自动添加输出路径参数 outputPath。

只要这么做,你也能用 taro 搞掂多端开发!

但 Taro 构建不支持这样动态更改 webpack 配置

只要这么做,你也能用 taro 搞掂多端开发!

最终的处理方法,在 config/index.js 里进行特殊的处理:

if (process.env.DEPLOY_ENV === 'prod' && process.env.DEPLOY_TYPE === 'h5') {
    outputRoot = 'prodDist';
}

4. 多端开发规范

4.1 样式管理

4.1.1 选择器

虽然在业务页面上不推荐 BEM 写法,BEM 写法更适合写 UI 组件库。

但是因为 Taro 在 React Native 端仅支持类选择器,且不支持组合器,推荐使用 BEM 写法,通过连接前缀避免样式冲突,兼容 H5、小程序和 React Native。

以下选择器的写法都是不支持的,在样式转换时会自动忽略。

.button.button_theme_islands {
    font-style: bold;
}
img + p {
    font-style: bold;
}
p ~ span {
    color: red;
}
div > span {
    background-color: DodgerBlue;
}
div span {
    background-color: DodgerBlue;
}

若我们基于 scss 等预编译语言开发,则可基于 BEM 写样式,如:

<View className="block">
    <Text className="block__elem">文本</Text>
</View>
.block: {
    background-color: DodgerBlue;
    &__elem {
        color: yellow;
    }
}

不要使用 ImageView 等 taro 标签选择器,因为转换 h5 时生成的标签名不是这个,导致样式失效。

4.1.2 css 单位

在 Taro 中尺寸单位建议使用 px、 百分比 %,Taro 默认会对所有单位进行转换。在 h5 中会转为 rem,在小程序中转为 rpx。

在编译时,Taro 会帮你对样式做尺寸转换操作,但是如果是在 JS 中书写了行内样式,那么编译时就无法做替换了,针对这种情况,Taro 提供了 API Taro.pxTransform 来做运行时的尺寸转换。

Taro.pxTransform(10) // 小程序:rpx,H5:rem

注意:默认配置会对所有的 px 单位进行转换,有大写字母的 Px 或 PX 则会被忽略。更多看taro-docs.jd.com/docs/size

4.2 其他

  1. 慎用第三方组件,注意跨端兼容性。比如我这里使用到 taro-uitabs 组件,它们依赖了 taro 的 ScrollView 组件,在 h5 有兼容问题。
  2. 尽量页面都放到分包里,静态资源都托管在 CDN上,避免打包体积超出微信小程序最小限制。

5. 小结

实现 Taro 多端开发,主要包括以下几步。

多端同步调试,通过打包配置输出多个平台的目标文件,使用本地代理服务器解决 h5 的跨域问题。

通过注入目标编译环境变量按需编译多端脚本、多端 UI 组件。

通过策略模式,判断多端环境从而实现对应的鉴权。

本次只是实现 h5 与微信小程序,Taro 还支持RN 端、其他小程序,赶紧着手实践吧。

参考资料

  • Taro 官方文档
  • AdminJS小册 Taro 多端开发实现原理与项目实战
  • Awesome Taro 多端统一开发框架 Taro 优秀学习资源汇总