taro + typescript 多端工程化开发的探索---by喜茶前端团队



  • 喜茶前端团队的技术栈为 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
    }
    

    2.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",
    

    3.不同环境的配置信息 例如 不同环境对应不同的 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、安卓 做相关处理优化

    本文转载已经取得作者授权
    作者:HeyteaTech
    链接:https://www.jianshu.com/p/d785b9b9f141
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系原作者获得授权并注明出处。


登录后回复