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

3,245 阅读6分钟

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,通过在不同端实现这些标准,从而实现差异化。

image.png

  • 基础框架(生命周期、组件 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/,即可解决跨域问题。

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

虽然 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 文件。

image.png 脚本需要读取 prodDist 文件夹,命令行工具会自动添加输出路径参数 outputPath。

image.png

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

image.png

最终的处理方法,在 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 端、其他小程序,赶紧着手实践吧。

参考资料