RT,忙乎双 11 潮流盛典的预热活动差不多近一个月了。
由于公司是 Ali 在虚拟试衣技术上的技术提供商,所以有幸参加了这么一场盛宴,特地来总结一下这次活动开发过程中的一些经验和感受。
下文所涉及 Tida 版本均为 3.2.115。

准备

这次双 11 潮流盛典活动分为三个部分:
试衣有礼(我负责)、边看边试、身材创建;

为了准备这次活动,我们以原主项目的为 origin,重新建了一个新项目,删除不必要的代码和静态资源。
主要是为了避免原主项目中过多的冗余代码,造成 app.js 过大。

配置好 charles 代理,开发过程中遇见过许多不明所以的坑,这个过程中 charles 代理后查看页面请求以及资源请求起到了非常大的作用,帮助定位了许多问题。

开发过程

其实活动开发还是相对简单的,逻辑并不复杂。主要都是在 Ali 环境下和 Tida 进行交互,诸如权益接口调用、页面激活状态监听、分享接口调用。

页面激活状态监听

Tida 页面激活状态监听文档

这里有多种方式监听页面激活状态:

1
2
3
4
5
6
7
8
9
var watchId = Tida.pageVisibility.watch(function(result){
///~ visible 1为激活 0为隐藏
///~ 移动端按Home键回到桌面js会挂起不执行,所有再次回到页面该方法会先后一起调用,注意区别该值
if(result.visible ==1 ){
// 页面显示了
}else{
// 页面隐藏了
}
});

这种方式是我们最早采用的, 但是出现过不稳定的情况,所以后续被抛弃(具体是哪种环境我忘了 …🙃 )。

1
2
3
document.addEventListener('WV.Event.APP.Active', function(e) {
isActive = true;
}, false);

这是随后我们采用的监听方式,表现比较稳定,但依然有个坑:
在边看边试活动中,直播态下,如果页面触发的是唤起 App 另一个 Webview,这个监听效果也是不存在,不会触发的(同事选择的方式是用 setInterval 轮询来 hack)。

权益接口调用

权益接口调用是需要 Ali 内部的支持的,内部提供奖池建立,以及安全码、抽奖码、店铺ID 三个参数供我们调用。
调用权益接口是非常严肃的,在判断接口返回数据的过程中需要严格判断是否中奖、以及奖品类型,否则会出现自损。

1
2
3
4
5
if (Object.hasOwnProperty.call(result, 'succ') && (result.succ === 'true' || result.succ === true)) {
this.rightsResult = result.data;
} else {
this.rightsResult = { isWin: false };
}

这里是判断接口是否调用成功,在 IOS 端返回 succ 字段是 boolean 型,但是 android 端返回时 string 型。需要特别注意。

1
2
3
4
5
6
7
8
9
10
isWin() {
const isWin = this.rightsResult.isWin;
let result = false;
if (typeof isWin === 'boolean') {
result = isWin;
} else if (typeof isWin === 'string') {
result = isWin === 'true';
}
return result;
}

这里是判断接口调用成功的情况下,是否中奖。
同理,也需要对 string 型和 boolean 型都做处理和判断。

分享接口调用

正常分享接口可以按照文档调用使用。

但是由于需求的特殊性,这里我们和 Ali 内部沟通后,得知可以使用一个隐藏的字段 targets。
这个字段在手淘上表现为可以按字段指定分享渠道。ex:

1
2
3
4
5
6
7
8
const options = {
title: 'title',
content: 'content',
url: 'url',
image: 'image',
targets: [code],
};
api.aliPlatformShare(options);

这里 targets 字段是数组,如果我期望我点击某个按钮只分享到微博环境下,可以传递 code 为 weibo/sinaweibo,这样可以直接唤起微博 App,分享指定文案 & title & 分享缩略图。

但是这个字段在猫客 App 里面表现为唤起系统的分享组件,需要用户二次点击分享渠道。

拍照、相册接口调用

这一部分本身并不由我负责,不过确是这一整个活动开发过程中最让人头疼的地方。在新版 tida3 中拍照和相册会引起 App 闪退的问题,回退到旧版 tida2 又没有权益接口。

和 Ali 开发沟通后,我们全部采用 Tida3 版本。

1
2
3
4
5
window.Tida.photo({
param: { compress: 0, type: 0, compatible: true },
}, data => {
// TODO
});

这里传递了三个特别的参数:

  • compress:Tida 文件不压缩,使得 IOS 端下唤起拍照 & 相册不会闪退;
  • type: 关闭 compress 后虽然不会引起闪退,但是上传文件的过程依然会出现问题,所以这里我们关闭了 Tida 的自动上传功能,选择自己获取拍照后返回数据,自己手动上传。
  • compatible:解决 App 端图层问题,部分时候App内部认为已经完成了唤起动作,但是页面上没有显示组件,是因为唤起的组件因为图层问题,被盖在了 webview 之后。

长按图片保存

这里主要是因为需求的特殊性。在微信链路下我们采用长按保存海报图片的形式完成分享,但是 Ali 产品希望不要暴露部分商业信息,所以在展示海报时要遮掉部分海报信息,这里我们采用了一种 hack 的方式完成这个需求。

我们在全页面上生成两张图片,第一张图片为展示给用户看到的图片,第二张图片为长按保存的图片,第二张图片的层级高于第一张,但是 opacity 我们设置为 0。

这样用户就会看到下一个层级的图片,但是长按保存的确是最顶上图层的图片。

优化

解决了大部分的坑以及需求上的 hack 之后,按照 ali 的验收标准需要对当前项目做一定的优化。

懒加载

这里商品列表中图片,我们是直接使用的 mint-ui 的 vue-lazy 实现的,loading 图片通过编译为 base64 直接写入到 app.js 中。

图片优化

上传到 CDN 的图片也需要处理,由 Ali 设计给出的静态图片大多数都非常大,有的甚至达到了 1M 多,这个情况下我们将非透明的背景图全部转成 jpg,可以实现图片资源的减小。(非透明底的 png 图片转成 jpg 会将透明底置为白色底)

然后开发了一个工具函数,imgHelper, 完成对加载的 CDN 图片的格式转化,因为手淘和猫客都是支持 webp 格式的图片,但是微信环境又不支持。所以这里会根据环境判断加载图片的格式。

不过这里也有一个 bug,就是手淘和猫客如果从缓存中读取 webp 格式的图片,会莫名其妙出现白底。

JS 文件优化

前后端分离中间层的 node 服务肯定是要开启 gzip 的,这个功能十分容易,看一下 express 的文档即可。
实现代码分割,完成所取即所需的也很重要。
npm run build 文件之后将打包好的 JS 文件上传到 CDN。

这里我们采用的 CDN 是阿里的顽兔,但是他们有一个 bug,就是 text/JavaScript 格式的文件是不支持 gzip 的,所以我们实现了一个脚本,将上传后的 JS 文件格式全部转成了 application/JavaScript 格式。随后成功开启了 JS 文件的 Gzip。

同时这里 chrome 也有一个问题,就是加载后的文件在 network 里面查看大小是没有开启 gzip 的,但是通过 charles 代理我们发现资源文件其实已经开启了,压缩率能够稳定在 60% 。

webview 加载优化

这个优化十分坑,因为我们发现在直播态下,直播的推流会跑满大部分的带宽,导致 JS 文件加载很慢。

而我们都知道 JS 文件在 script 标签中是同步加载的。webview 在确定 DOMContentloaded 之前是不会唤起的。(这里我们测试发现 script 标签上的 defer 和 async 均无效)

所以我们也是通过一个脚本 hack,将 app.js 的加载放到了 index.html 的最后面。通过 JS 注入是异步的,而 vendor.js、manifest.js、app.js 三个文件是有依赖关系的, 所以这里需要我们修改一下 webpack,把 vendor.js、manifest.js,统一打包到 app.js 之中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
build/webpack.prod.conf.js
// split vendor js into its own file
// new webpack.optimize.CommonsChunkPlugin({
// name: 'vendor',
// minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
// return (
// module.resource &&
// /\.js$/.test(module.resource) &&
// module.resource.indexOf(
// path.join(__dirname, '../node_modules')
// ) === 0
// )
// }
// }),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
// new webpack.optimize.CommonsChunkPlugin({
// name: 'manifest',
// chunks: ['vendor']
// }),
// copy custom static assets

将以上的代码注释掉即可。

其他优化

为了减小 app.js,减少部分 base64 是有必要,这个可以通过修改 webpack.base.conf.js 中的 imgloader 实现。

1
2
3
4
5
6
7
8
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 500,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},

总结

以上就是这个开发过程我特别注意到了的一些东西,除此之外还有两个问题我这里单独说,是因为十分重要。

1、 charles 代理是十分重要的的,日常开发可以帮助我们定位许多问题,优化过程中也需要 charles 查看文件资源大小帮助我们。

2、前端的 qps 十分重要,现在 web 开发几乎都是前后端分离的模式,前端为了解决跨域的问题,大多数都会构建一个简易的 node 服务部署在自己的前端服务器上。这里我们做压测的时候也需要对前端做,虽然会认为只是转发请求并没有什么业务量和代码逻辑,但是当前端服务器阻塞时一样会造成页面的崩溃。

以上。