taro + typescript 多端工程化开发的探索

喜茶前端团队的技术栈为 react + mobx + typescript
目前正在对 喜茶go 小程序进行重构,并同时支持微信小程序、支付宝小程序、H5、IOS、Android。
综合以上 我们选了 taro + typescript
以下,是团队在多端工程化开发中的一些探索/解决方案

一、让 taro 支持多环境(dev、test、pre、prod)、同时支持多端开发

  1. package.json 的 scripts 增加变量 TARO_APP_TYPE={type} TARO_APP_API={env}

修改 ./config/index.js 的 outputRoot 让其根据不同的 type + env 生成不同目录 支持多端开发
并将 TARO_APP_TYPE、TARO_APP_API 加入到 env 中

const { TARO_APP_TYPE, TARO_APP_API, NODE_ENV } = process.env
const outputRoot = `dist/${NODE_ENV === 'development' ? 'tmp' : 'build'}-${TARO_APP_TYPE}-${TARO_APP_API}`
const config = {
  env: {
    TARO_APP_TYPE: '"' + TARO_APP_TYPE + '"',
    TARO_APP_API: '"' + TARO_APP_API + '"'
  },
  outputRoot
}
  1. setProjectConfig.js 根据不同的 TARO_APP_API 动态设置 微信小程序 project.config.json 中的 appId

根目录创建文件 setProjectConfig.js

var fs = require('fs')
const config = {}
const testAppId = 'test'
const prodAppId = 'prod'
switch (process.env.TARO_APP_API) {
  case 'dev':
    config.appid = testAppId
    break
  case 'test':
    config.appid = testAppId
    break
  case 'pre':
    config.appid = prodAppId
    break
  case 'prod':
    config.appid = prodAppId
    break
  default:
    config.appid = testAppId
}

function writeJson() {

  fs.readFile('./project.config.json', function (err, data) {
    if (err) {
      return console.error(err)
    }
    var person = { ...JSON.parse(data.toString()), ...config }
    var str = JSON.stringify(person)
    fs.writeFile('./project.config.json', str, (writeFileErr) => {
      if (writeFileErr) {
        console.error(writeFileErr);
      } else {
        console.log('----------修改成功-------------');
      }
    })
  })
}

writeJson()

package.json 添加 script

"set:dev": "cross-env TARO_APP_API=dev node ./setProjectConfig.js",
"set:test": "cross-env TARO_APP_API=test node ./setProjectConfig.js",
"set:pre": "cross-env TARO_APP_API=pre node ./setProjectConfig.js",
"set:prod": "cross-env TARO_APP_API=prod node ./setProjectConfig.js",

相关 weapp script 增加 "npm run set:${env} 例如:

"build:weapp-api-dev": "npm run set:dev && cross-env a taro build --type weapp",
  1. 不同环境的配置信息 例如 不同环境对应不同的 api 地址

创建 ./src/config.ts

if (process.env.TARO_APP_API === 'dev') {
  hosts.api = ''
} else if (process.env.TARO_APP_API === 'test') {
  hosts.api = ''
} else if (process.env.TARO_APP_API === 'pre') {
  hosts.api = ''
} else if (process.env.TARO_APP_API === 'prod') {
  hosts.api = ''
} else {
  hosts.api = ''
}

二、静态资源的处理

除了 tabbar 必须本地引用的图片,我们将其他资源统一上传到七牛云管理,最大化的减少小程序包的大小,以及提升各端的编译速度

  1. 配置本地 nginx

创建 ./nginx/local.conf 文件

server {
    listen 80;
    server_name local-cdn-taro.heytea.com;
    root /www/heytea/taro/src/assets;
    error_log  off;
    access_log  off;
    error_page 405 =200 $uri;
}
  1. ./src/config.ts 增加 hosts.cdn 域名配置, 并通过 二级目录做版本控制
const cdnDir = '/taro/v1'
if (process.env.TARO_APP_API === 'dev') {
  hosts.cdn = 'http://local-cdn-taro.heytea.com'
} else if (process.env.TARO_APP_API === 'test') {
  hosts.api = 'https://static.heytea.com/' + cdnDir
} else if (process.env.TARO_APP_API === 'pre') {
  hosts.api = 'https://static.heytea.com/' + cdnDir
} else if (process.env.TARO_APP_API === 'prod') {
  hosts.api = 'https://static.heytea.com/' + cdnDir
} else {
  hosts.api = 'https://static.heytea.com/' + cdnDir
}
  1. 长文本(协议、说明)内容的CDN化

创建 ./src/assets/json/{name}.json 文件

{
  "code": 0,
  "data": [
    {
      "val": "喜茶隐私保护政策"
    }
  ]
}

然后,在相应的页面发起 api 请求 hosts.cdn+'/json/+'{name}.json' 获取数据

  1. 静态资源自动同步到七牛云
    1). 通过 gitlab 的 CI 来执行脚本上传资源 (推荐 较安全)
    2). 本地创建七牛云上传脚本

三、adapter 多端功能的适配

  1. taro 提供非常多的 Taro.{fnName} 解决了大部分多端功能的适配,但在一些业务场景或者一些比较细的功能,还存在一些没适配到

chooseImage 在 微信小程序跟支付宝小程序表现不一致,RN中不支持
tabBar 的相关操作 目前也只支持到 微信小程序

  1. 有些功能是需要业务上的妥协来进行适配的

location 的相关 在H5上会因为多浏览器表现不一致 导致更大适配成本,通过 adapter 留个口,方便以后扩展适配

  1. 为了更好统一的处理错误,也需要将一些统一适配

路由跳转
ajax 请求 通知 adapter 统一处理配置、参数、错误、规范返回格式

  1. 全局统一使用 await/async 为了尽量不在 page 里做 try/catch 对部分功能进行二次适配

例如 storage.ts

import Taro from '@tarojs/taro'

export function setStorage(key: string, value: any | string): Promise<boolean> {
  return new Promise(resolve => {
    Taro.setStorage({ key, data: value, }).then(() => resolve(true), () => resolve(false))
  })
}

export function getStorage(key: string): Promise<any> {
  return new Promise(resolve => {
    Taro.getStorage({ key }).then((res: any) => resolve(res.data), () => resolve(null))
  })
}

export function getStorageInfo(): Promise<any> {
  return new Promise(resolve => {
    Taro.getStorageInfo().then((res: any) => resolve(res), () => resolve(null))
  })
}

export function removeStorage(key: string): Promise<boolean> {
  return new Promise(resolve => {
    Taro.removeStorage({ key }).then(() => resolve(true), () => resolve(false))
  })
}

export function clearStorage(): void {
  Taro.clearStorage()
}

四、错误上报、性能监控、业务埋点

  1. 通过 adapter 接入 GrowingIO SDK 实现基础功能
  2. 在各个 adapter/components 中 实现自定义错误信息上报,尽可能的不在 page 里处理上报错误

router adapter 中 记录并上报路由跳转错误
ajax adapter 中 记录并上报 接口 相关错误
在 img、link component 中记录加载/跳转错误信息
...

  1. 由于 喜茶go 小程序每天百万级别pv 我们选择可配置的 百分比 上报性能相关数据

按随机百分比Math.random()<{x} 统计上报页面首屏、白屏、接口、渲染时间

五、利用扫普通链接二维码打开小程序 和 中转页 实现多端统一二维码

实现一个二维码 在多端扫描 打开对应的小程序或者H5或者跳转到APP指定页面

  1. 微信/支付宝小程序后台分配配置

二维码地址 https://{env}-m.heytea.com/ 小程序路径 pages/transfer/index 并把 前缀占用规则 改为 占用

  1. 编写 ./src/pages/transfer/index.tsx 文件
@inject('user') @observer
class Index extends Component<IProps> {
  static defaultProps = {
    user: store.user
  }
  config: Config = {
    navigationBarTitleText: 'loading…'
  }

  fail = (e) => {
    Taro.showToast(e.errMsg)
    Taro.switchTab({ url: routes.menu })
  }

  componentDidMount() {
    const { setUrlQuery } = this.props.user
    const { q = '' } = this.$router.params
    const path = decodeURIComponent(q).replace(/http[s]?:\/\/[^/?]+/, '')
    if (path) {
      let [page, query = ''] = path.split('?')
      if (page === '' || page === '/') {
        page = routes.menu
      }
      query ? query += '?' : ''
      const url = page + query
      if (routerBar.indexOf(page) >= 0) {
        setUrlQuery(query) // 为了解决 wx.switchTab: url 不支持 queryString
        Taro.switchTab({ url, fail: this.fail })
      } else {
        Taro.redirectTo({ url, fail: this.fail })
      }
    } else {
      Taro.switchTab({ url: routes.menu })
    }
  }
  render() {
    return (
      <View>
        <Text>loading……</Text>
      </View>
    )
  }
}

六 统一 webview 页的处理

创建 ./src/pages/webview/index.tsx 页,通过适配器模式 统一处理 webview 相关业务,例如H5直接跳转页面,小程序APP,利用 WebView 组件渲染,并对 IOS、安卓 做相关处理优化

推荐阅读更多精彩内容