喜茶前端团队的技术栈为 react + mobx + typescript
目前正在对 喜茶go 小程序进行重构,并同时支持微信小程序、支付宝小程序、H5、IOS、Android。
综合以上 我们选了 taro + typescript
以下,是团队在多端工程化开发中的一些探索/解决方案
一、让 taro 支持多环境(dev、test、pre、prod)、同时支持多端开发
- 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
}
- 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",
- 不同环境的配置信息 例如 不同环境对应不同的 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 必须本地引用的图片,我们将其他资源统一上传到七牛云管理,最大化的减少小程序包的大小,以及提升各端的编译速度
- 配置本地 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;
}
- ./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
}
- 长文本(协议、说明)内容的CDN化
创建 ./src/assets/json/{name}.json 文件
{
"code": 0,
"data": [
{
"val": "喜茶隐私保护政策"
}
]
}
然后,在相应的页面发起 api 请求 hosts.cdn+'/json/+'{name}.json' 获取数据
- 静态资源自动同步到七牛云
1). 通过 gitlab 的 CI 来执行脚本上传资源 (推荐 较安全)
2). 本地创建七牛云上传脚本
三、adapter 多端功能的适配
- taro 提供非常多的 Taro.{fnName} 解决了大部分多端功能的适配,但在一些业务场景或者一些比较细的功能,还存在一些没适配到
chooseImage 在 微信小程序跟支付宝小程序表现不一致,RN中不支持
tabBar 的相关操作 目前也只支持到 微信小程序
- 有些功能是需要业务上的妥协来进行适配的
location 的相关 在H5上会因为多浏览器表现不一致 导致更大适配成本,通过 adapter 留个口,方便以后扩展适配
- 为了更好统一的处理错误,也需要将一些统一适配
路由跳转
ajax 请求 通知 adapter 统一处理配置、参数、错误、规范返回格式
- 全局统一使用 await/async 为了尽量不在 page 里做 try/catch 对部分功能进行二次适配
例如 storage.ts
import Taro from '@tarojs/taro'
export function setStorage(key: string, value: any | string): Promise {
return new Promise(resolve => {
Taro.setStorage({ key, data: value, }).then(() => resolve(true), () => resolve(false))
})
}
export function getStorage(key: string): Promise {
return new Promise(resolve => {
Taro.getStorage({ key }).then((res: any) => resolve(res.data), () => resolve(null))
})
}
export function getStorageInfo(): Promise {
return new Promise(resolve => {
Taro.getStorageInfo().then((res: any) => resolve(res), () => resolve(null))
})
}
export function removeStorage(key: string): Promise {
return new Promise(resolve => {
Taro.removeStorage({ key }).then(() => resolve(true), () => resolve(false))
})
}
export function clearStorage(): void {
Taro.clearStorage()
}
四、错误上报、性能监控、业务埋点
- 通过 adapter 接入 GrowingIO SDK 实现基础功能
- 在各个 adapter/components 中 实现自定义错误信息上报,尽可能的不在 page 里处理上报错误
router adapter 中 记录并上报路由跳转错误
ajax adapter 中 记录并上报 接口 相关错误
在 img、link component 中记录加载/跳转错误信息
...
- 由于 喜茶go 小程序每天百万级别pv 我们选择可配置的 百分比 上报性能相关数据
按随机百分比Math.random()<{x} 统计上报页面首屏、白屏、接口、渲染时间
五、利用扫普通链接二维码打开小程序 和 中转页 实现多端统一二维码
实现一个二维码 在多端扫描 打开对应的小程序或者H5或者跳转到APP指定页面
- 微信/支付宝小程序后台分配配置
二维码地址 https://{env}-m.heytea.com/ 小程序路径 pages/transfer/index 并把 前缀占用规则 改为 占用
- 编写 ./src/pages/transfer/index.tsx 文件
@inject('user') @observer
class Index extends Component {
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 (
loading……
)
}
}
六 统一 webview 页的处理
创建 ./src/pages/webview/index.tsx 页,通过适配器模式 统一处理 webview 相关业务,例如H5直接跳转页面,小程序APP,利用 WebView 组件渲染,并对 IOS、安卓 做相关处理优化