基于Vue和TS的Web移动端项⽬实战⼼得作者:mcuking(杭州个推)
笔者在公司⽤ web 技术开发移动端应⽤已经有⼀年多的时间了,开始主要以 vue 技术栈配合 native 为主,⽬前演进成 vue + react native 技术架构,vue 主要负责开发 OA 业务,⽐如报销、出差、crm 等等,react native 主要负责即时通信部分,是在 mattermost-mobile[1] 的基础上修改的(mattermost 是⼀个开源的即时通讯⽅案)。
因为公司在这⽅⾯没有太多技术沉淀,所以在开发期间遇到了很多坑,经过⼀年多的技术攻克积累,最终形成了这套⽐较完善的解决⽅案,总结出来希望能够帮助到⼤家,尤其是对⼀些中⼩公司这⽅⾯经验不⾜的(PS: ⼤公司估计有他们⾃⼰的⼀套⽅案了)。
好了废话不多说,先亮下这个库的 GitHub 地址,后⾯还会不断完善,欢迎 star:
mobile-web-best-practice[2]
移动端 web 最佳实践,基于 vue-cli3[3] 搭建的 typescript[4] 项⽬,可以⽤于 hybrid 应⽤或者纯 webapp 开发。以下⼤部分内容同样适⽤于 react[5] 等前端框架。
其中有三个点尚在完善中:领域驱动设计(DDD)应⽤、微前端、性能监控,后续完成后会以单独的⽂章发出来。其中性能监控还没有太好的选择,类似错误监控 sentry 那种开源免费⽽且功能强⼤的⼯具,
如果有⼈知道的⿇烦告知下。⽂中难免有些错误或者更好的⽅案,也欢迎不吝赐教。
⽬录
组件库[6]
JSBridge[7]
路由堆栈管理(模拟原⽣ APP 导航)[8]
请求数据缓存[9]
构建时预渲染[10]
Webpack 策略[11]
基础库抽离[12]
⼿势库[13]
样式适配[14]
表单校验[15]
阻⽌原⽣返回事件[16]
通过 UA 获取设备信息[17]
mock 数据[18]
调试控制台[19]
抓包⼯具[20]
异常监控平台[21]
常见问题[22]
组件库
vant[23]
vux[24]
mint-ui[25]
cube-ui[26]
vue 移动端组件库⽬前主要就是上⾯罗列的这⼏个库,本项⽬使⽤的是有赞前端团队开源的 vant。
vant 官⽅⽬前已经⽀持⾃定义样式主题,基本原理就是在 less-loader[27] 编译 less[28] ⽂件到 css ⽂件过程中,利⽤ less 提供
的 modifyVars[29] 对 less 变量进⾏修改,本项⽬也采⽤了该⽅式,具体配置请查看相关⽂档:
定制主题[30]
推荐⼀篇介绍各个组件库特点的⽂章:
Vue 常⽤组件库的⽐较分析(移动端)[31]
JSBridge
DSBridge-IOS[32]
DSBridge-Android[33]
WebViewJavascriptBridge[34]
混合应⽤中⼀般都是通过 webview 加载⽹页,⽽当⽹页要获取设备能⼒(例如调⽤摄像头、本地⽇历等)或者 native 需要调⽤⽹页⾥的⽅法,就需要通过 JSBridge 进⾏通信。
开源社区中有很多功能强⼤的 JSBridge,例如上⾯列举的库。本项⽬基于保持 iOS android 平台接⼝统⼀原因,采⽤了 DSBridge,各位可以选择适合⾃⼰项⽬的⼯具。
本项⽬以 h5 调⽤ native 提供的同步⽇历接⼝为例,演⽰如何在 dsbridge 基础上进⾏两端通信的。下⾯是两端的关键代码摘要:
安卓端同步⽇历核⼼代码,具体代码请查看与本项⽬配套的安卓项⽬ mobile-web-best-practice-container[35]:
public class JsApi {
/**
* 同步⽇历接⼝
* msg 格式如下:
* ...
*/
@JavascriptInterface
public void syncCalendar(Object msg, CompletionHandler<Integer> handler) {
try {
JSONObject obj = new String());
String id = String("id");
String title = String("title");
String location = String("location");
long startTime = Long("startTime");
long endTime = Long("endTime");
JSONArray earlyRemindTime = JSONArray("alarm");
String res = CalendarReminderUtils.addCalendarEvent(id, title, location, startTime, endTime, earlyRemindTime);
handlerplete(Integer.valueOf(res));
} catch (Exception e) {
e.printStackTrace();
handlerplete(6005);
}
}
}
h5 端同步⽇历核⼼代码(通过装饰器来限制调⽤接⼝的平台)
class NativeMethods {
// 同步到⽇历
webserver接口开发@p()
public syncCalendar(params: SyncCalendarParams) {
const cb = (errCode: number) => {
const msg = NATIVE_ERROR_CODE_MAP[errCode];
Vue.prototype.$toast(msg);
if (errCode !== 6000) {
}
};
dsbridge.call('syncCalendar', params, cb);
}
// 调⽤ native 接⼝出错向 sentry 发送错误信息
private errorReport(errorMsg: string, methodName: string, params: any) {
if (window.$sentry) {
const errorInfo: NativeApiErrorInfo = {
error: new Error(errorMsg),
type: 'callNative',
methodName,
params: JSON.stringify(params)
};
window.$sentry.log(errorInfo);
}
}
}
/**
* @param {platforms} - 接⼝限制的平台
* @return {Function} - 装饰器
*/
function p(platforms = ['android', 'ios']) {
return (target: AnyObject, name: string, descriptor: PropertyDescriptor) => {
if (!platforms.includes(window.$platform)) {
descriptor.value = () => {
return Vue.prototype.$toast(
`当前处在 ${window.$platform} 环境,⽆法调⽤接⼝哦`
);
};
}
return descriptor;
};
}
另外推荐⼀个笔者之前写的⼀个基于安卓平台实现的教学版 JSBridge[36],⾥⾯详细阐述了如何基于底层接⼝⼀步步封装⼀个可⽤的JSBridge:
JSBridge 实现原理[37]
路由堆栈管理(模拟原⽣ APP 导航)
vue-page-stack[38]
vue-navigation[39]
vue-stack-router[40]
在使⽤ h5 开发 app,会经常遇到下⾯的需求:从列表进⼊详情页,返回后能够记住当前位置,或者从表单点击某项进⼊到其他页⾯选择,然后回到表单页,需要记住之前表单填写的数据。可是⽬前 vue 或 react 框架的路由,均不⽀持同时存在两个页⾯实例,所以需要路由堆栈进⾏管理。
其中 vue-page-stack 和 vue-navigation 均受 vue 的 keepalive 启发,基于 vue-router[41],当进⼊某个页⾯时,会查看当前页⾯是否有缓存,有缓存的话就取出缓存,并且清除排在他后⾯的所有 vnode,没有缓存就是新的页⾯,需要存储或者是 replace 当前页⾯,向栈⾥⾯ push 对应的 vnode,从⽽实现记住页⾯状态的功能。
⽽逻辑思维前端团队的 vue-stack-router 则另辟蹊径,抛开了 vue-router,⾃⼰独⽴实现了路由管理,相较于 vue-router,主要是⽀持同时可以存活 A 和 B 两个页⾯的实例,或者 A 页⾯不同状态的两个实例,并⽀持原⽣左滑功能。但由于项⽬还在初期完善,功能还没有vue-router 强⼤,建议持续关注后续动态再做决定是否引⼊。
本项⽬使⽤的是 vue-page-stack,各位可以选择适合⾃⼰项⽬的⼯具。同时推荐⼏篇相关⽂章:
【vue-page-stack】Vue 单页应⽤导航管理器 正式发布[42]
Vue 社区的路由解决⽅案:vue-stack-router[43]
请求数据缓存
mem[44]
在我们的应⽤中,会存在⼀些很少改动的数据,⽽这些数据有需要从后端获取,⽐如公司⼈员、公司职位分类等,此类数据在很长⼀段时间时不会改变的,⽽每次打开页⾯或切换页⾯时,就重新向后端请求。为了能够减少不必要请求,加快页⾯渲染速度,可以引⽤ mem 缓存库。
mem 基本原理是通过以接收的函数为 key 创建⼀个 WeakMap,然后再以函数参数为 key 创建⼀个 Map,value 就是函数的执⾏结果,同时将这个 Map 作为刚刚的 WeakMap 的 value 形成嵌套关系,从⽽实现对同⼀个函数不同参数进⾏缓存。⽽且⽀持传⼊ maxAge,即数据的有效期,当某个数据到达有效期后,会⾃动销毁,避免内存泄漏。
选择 WeakMap 是因为其相对 Map 保持对键名所引⽤的对象是弱引⽤,即垃圾回收机制不将该引⽤考
虑在内。只要所引⽤的对象的其他引⽤都被清除,垃圾回收机制就会释放该对象所占⽤的内存。也就是说,⼀旦不再需要,WeakMap ⾥⾯的键名对象和所对应的键值对会⾃动消失,不⽤⼿动删除引⽤。
mem 作为⾼阶函数,可以直接接受封装好的接⼝请求。但是为了更加直观简便,我们可以按照类的形式集成我们的接⼝函数,然后就可以⽤装饰器的⽅式使⽤ mem 了(装饰器只能修饰类和类的类的⽅法,因为普通函数会存在变量提升)。下⾯是相关代码:
import http from '../http';
import mem from 'mem';
/**
* @param {MemOption} - mem 配置项
* @return {Function} - 装饰器
*/
export default function m(options: AnyObject) {
return (target: AnyObject, name: string, descriptor: PropertyDescriptor) => {
const oldValue = descriptor.value;
descriptor.value = mem(oldValue, options);
return descriptor;
};
}
class Home {
@m({ maxAge: 60 * 1000 })
public async getUnderlingDailyList(
query: ListQuery
): Promise<{ total: number; list: DailyItem[] }> {
const {
data: { total, list }
} = await http({
method: 'post',
url: '/daily/getList',
data: query
});
return { total, list };
}
}
export default new Home();
构建时预渲染
针对⽬前单页⾯⾸屏渲染时间长(需要下载解析 js ⽂件然后渲染元素并挂载到 id 为 app 的 div 上),SEO 不友好(index.html 的 body 上实际元素只有 id 为 app 的 div 元素,真正的页⾯元素都是动态挂载的,搜索引擎的爬⾍⽆法捕捉到),⽬前主流解决⽅案就是服务端渲染(SSR),即从服务端⽣成组装好的完整静态 html 发送到浏览器进⾏展⽰,但配置较为复杂,⼀般都会借助框架,⽐如 vue
的 nuxt.js[45],react 的 next[46]。
其实有⼀种更简便的⽅式--构建时预渲染。顾名思义,就是项⽬打包构建完成后,启动⼀个 Web Server 来运⾏整个⽹站,再开启多个⽆头浏览器(例如 Puppeteer[47]、Phantomjs[48] 等⽆头浏览器技术)去请求项⽬中所有的路由,当请求的⽹页渲染到第⼀个需要预渲染的页⾯时(需提前配置需要预渲染页⾯的路由),会主动抛出⼀个事件,该事件由⽆头浏览器截获,然后将此时的页⾯内容⽣成⼀个
HTML(包含了 JS ⽣成的 DOM 结构和 CSS 样式),保存到打包⽂件夹中。
根据上⾯的描述,我们可以其实它本质上就只是快照页⾯,不适合过度依赖后端接⼝的动态页⾯,⽐较适合变化不频繁的静态页⾯。
实际项⽬相关⼯具⽅⾯⽐较推荐 prerender-spa-plugin[49] 这个 webpack 插件,下⾯是这个插件的原理图。不过有两点需要注意:
⼀个是这个插件需要依赖 Puppeteer,⽽因为国内⽹络原因以及本⾝体积较⼤,经常下载失败,不过可以通过 .npmrc ⽂件指定Puppeteer 的下载路径为国内镜像;
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论