本文介绍一款基于 vue 的使 App 支持离线缓存 web 资源的混合开发框架. 本人小白一枚, 请将它视作一份我的学习总结, 欢迎大神们赐教. 本文多阐述思路, 实现细节请阅读源码.
源码 https://github.com/sunmengyuan/ballade
为何选择混合开发?
高效率界面开发: html + CSS + JavaScript 被证实具备极高的界面开发效率.
跨平台: 较统一的浏览器内核标准, 使 H5 页面在 IOS,Android 共享同套代码. 使用 Native 开发一功能需 IOS,Android 研发各一枚, 而使用 H5 一枚前端工程师足矣. 但混合 App 并非 Native 越少越佳, 性能要求较高的仍需劳 Native 大驾... 分工需明确, 不可厚此薄彼.
热更新: 不依赖于发布渠道自主更新应用. Native 修复线上 Bug 需发布新版本, 用户未升级 App 该 Bug 将一直呈现. 而修复 H5 只需将 Fixbug 的代码推至服务器, 任一版本 App 便可同步更新对应功能无需升级.
为何离线缓存 Web 资源?
相比于从远程服务器请求加载 Web 资源, App 优先加载本地预置资源, 可提升页面响应速度, 节省用户流量.
问题来了... 本地预置的 Web 资源也随 App 安装包一起成为泼出去的水, 修复 H5 线上 Bug 也需发版了? 丢西瓜捡芝麻的事定不可做! 请注意 "优先加载本地预置资源", 但检测到更新时加载远程最新资源, 如何检测更新我稍后阐明.
对我司前端团队的意义
技术栈由 Jinja + jQuery + Require + Gulp 迁移至 Vue + Webpack + Gulp + Sass, 拥抱 Vue!
实现前后端分离: 原 Jinja http://docs.jinkan.org/docs/jinja2/templates.html 为 Python 模板引擎, 前端代码的运作依赖于服务端, 服务端异常等待环境维修严重影响前端工作进度. 分离后, 服务器挂了我们愉快的开启 Mock Server 继续搬砖便是.
App 优先加载本地预置 Web 资源, 可提升 H5 页面加载速度.
弊端
技术重构本身具备风险性.
增加团队学习成本.
前端框架通过 JS 渲染 HTML 对 SEO 不友好. 但你可选择使用 Vue 2.2 的服务端渲染 (SSR) https://ssr.vuejs.org/zh/ . 增添 Node 层除实现 SSR, 能做的事还很多...
进入正题~
混合开发框架运作机制
将 Web 资源文件打包至 dist/(含 routes.json 及 N 多 .html) 并压缩为 dist.zip, 图片资源单独打包至 assets/, 一同上传至 CDN.
App 内预置 dist/ 下全部资源 (发版时仅下载 dist.zip, 安装 App 时解压), 在拦截并解析 URL 后, 通过 routes.json 查找并加载本地 .html 页面.
routes.json 如下:
- {
- "items": [
- {
- "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Demo-13700fc663.html",
- "uri": "https://backend.igengmei.com/demo[/]?.*"
- },
- {
- "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Album-a757d93443.html",
- "uri": "https://backend.igengmei.com/album[/]?.*"
- },
- {
- "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/post/ArticleDetail-d5c43ffc46.html",
- "uri": "https://backend.igengmei.com/article/detail[/]?.*"
- }
- ],
- "deploy_time": "Fri Mar 16 2018 15:27:57 GMT+0800 (CST)"
- }
复制代码
欠你一个回答~
请注意 "优先加载本地预置资源", 但检测到更新时加载远程最新资源, 如何检测更新我稍后阐明.
检测 .html 文件更新的桥梁便是 routes.json. 每启动 App 从 CDN 静默更新 routes.json 一次 (CDN 缓存会导致 routes.json 无法及时更新, 下载路由表请添加时间戳参数强制更新), 任一资源更新均同步至 routes.json 并上传 CDN.
标记更新的方式则是为 .html 打 Hash(MD5) 戳, 于 App 而言不同 Hash 后缀的 .html 为不同文件. App 根据路由表 remote_file 查寻本地 .html, 若该 .html 不存在则直接加载远程资源同时静默下载更新.
注: 由于 js,css 脚本均被内联至对应 .html,App 仅需监听 .html 文件的变化. 其实我们可以提取公用脚本并为之打 Hash 戳, 将该资源的变化记录至一张表供 App 监听. 常年不更新的公用脚本, 缓存在 App 内不随 .html 一同加载也可提升页面响应速度.
综上, Web 资源虽被预置于 App, 但其 Fixbug 级别的更新不必走发版这条路.
为何图片资源单独打包至 assets/, 先欠着~
Web 框架设计
Web 框架设计围绕:
减少无用资源及冗余资源
减小依赖模块对 Hash 的影响
开发环境模式尽量简易
减少无用资源及冗余资源
机智的你发现使用 Vue 脚手架 build 后产生单 .html, 单 .js, 单 .css(所有页面资源打包在一坨啦), 而我所举例的却是多 .html. 如何实现 Vue 多页面拆分我会细讲, 先讨论拆分多页面的意义吧:"快" + "节约"!
假定我站含页面 A,B,C, 用户仅访问 A 但单页应用却将 A,B,C 所依赖的全部资源加载. B,C 于用户而言是无用的, 我们偷偷吃用户流量下载无用资源很不厚道.
拆分资源可减小 .html 体积自然提升页面加载速度, 且 App 优先访问本地 .html 免去远程请求更是快上加快.
无用资源需丢弃, 公共资源也需提取. 假定页面 A,B 均引用资源 C, 资源 C 便可单独提取. 可使用 CommonsChunkPlugin https://doc.webpack-china.org/plugins/commons-chunk-plugin 达成对第三方库, 公用组件的抽离. 一提取项目所应用 node_module 脚本示例:
- new webpack.optimize.CommonsChunkPlugin({
- name: 'vendor',
- minChunks: function (module) {
- return (
- module.resource &&
- /\.js$/.test(module.resource) &&
- module.resource.indexOf(
- path.join(__dirname, '../node_modules')
- ) === 0
- )
- }
- })
复制代码
项目中所应用到的 node_module 将统一打包至 vendor.js. 公用脚本也需预置, 也需检测更新, 若认为监听众多资源较麻烦将脚本内联至 .html 也可, 但我不提倡这样做 (失去了去冗余的意义). 预置的公用脚本拷贝到哪里? 拷贝至手机内存空间不够怎么破, 拷贝至存储卡被用户误删怎么破, 客户端同学为此很纠结...emmm
vendor.js 含所有页面依赖到的 node_module. 假定页面 A 使用了 Swiper 而其它页面未引用它, vendor.js 中的 Swiper 相关代码便应仅打包至页面 A, 如何实现?
生成 vendor.js 时过滤 Swiper 并将其单独打包, node_modules 仍含 Swiper.
将 Swiper 从 node_modules 移动至其它路径, 引用时使用迁移后的路径.
引入 Sass 也可一定程度的去除无用代码:
使用 @mixin,% 定义的通用样式未被继承不会被解析产生相应的 css.
想了解更多的同学请研读 Sass: Syntactically Awesome Style Sheets http://sass-lang.com/ .
减小依赖模块对 Hash 的影响
由于 App 需监听众 .html 变化并实时更新资源, 应格外注意 Hash 值的稳定性, 为此应坚守代码模块化原则. 假定全局引入 app.js,app.css, 则不允许添加非全局性质的代码至上述两个文件.
假如模块 A 被注入 app.js, 它的修改将影响所有 .html 的 Hash 值, 未调用模块 A 的页面实际上未做修改却被动更新 Hash.App 根据 Hash 的变化判断资源更新则认为所有 .html 更新了, 进而重新下载所有 Web 资源.
总之 A 未调用 B,B 的修改不要影响 A 的 Hash, 模块如何拆分请自行依照此原则把握.
接下来讨论 manifest 的注入时机. manifest 包含模块处理逻辑, 在 Webpack 编译及映射应用代码时, 模块信息被记录至 manifest,runtime 则根据 manifest 加载模块.
- new webpack.optimize.CommonsChunkPlugin({
- name: 'manifest',
- minChunks: Infinity
- })
复制代码
任一模块更新均会引发它的细微变化 (但可通过 minChunks 控制 manifest 影响范围), 且所有页面加载依赖 manifest. 可怕的现象发生了: manifest 更新所有 .html 的 Hash 更新 -> 所有 .html 被重新下载. 我们可先为 .html 打 Hash 再将 manifest 内联, 因为未更新模块调用旧 manifest 不会受影响.
开发环境模式尽量简易
一个项目参与者众多, 开发环境模式复杂将提高学习成本与风险. 在简化开发模式上我做了哪些:
开发环境单入口, 生产环境多入口
- import Vue from 'vue'
- import Page from '<%=Page%>'
- /* eslint-disable no-new */
- new Vue({
- el: '#app',
- template: '<Page />',
- components: {
- Page
- }
- })
- gulp.task('entries', () => {
- var flag = true
- for (let key in routes) {
- // 检查 entry 是否已存在
- gulp.src(`./entry/entries/${routes[key].view}.js`)
- .on('data', () => {
- // 已存在 entry 不重复构造
- flag = false
- })
- .on('end', () => {
- if (flag) {
- console.log('new entry:', `/entries/${routes[key].view}.js`)
- // 构造新 entry
- gulp.src('./entry/entry.js')
- .pipe(replace({
- patterns: [
- {
- match: /<%=Page%>/g,
- replacement: `../../src/views/${routes[key].path}${routes[key].view}`
- }
- ]
- }))
- .pipe(rename(`entries/${routes[key].view}.js`))
- .pipe(gulp.dest('./entry/'))
- }
- flag = true
- })
- }
- })
- function entries () {
- var entries = {}
- for (let key in routes) {
- entries[routes[key].view] = process.env.NODE_ENV === 'production'
- ? `./entry/entries/${routes[key].view}.js`
- : './entry/dev.js'
- }
- return entries
- }
- {
- test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
- loader: 'url-loader',
- options: {
- limit: 1,
- name: 'assets/imgs/[name]-[hash:10].[ext]'
- }
- }
- const settings = require('../settings')
- module.exports = {
- dev: {
- // code...
- },
- build: {
- assetsRoot: path.resolve(__dirname, '../../dist'),
- assetsSubDirectory: 'static',
- assetsPublicPath: `${settings.cdn}/`,
- // code...
- }
- }
- {
- "/demo": {
- "view": "Demo",
- "path": "demo/",
- "query": [
- "topic_id",
- "service_id"
- ]
- },
- "/album": {
- "view": "Album",
- "path": "demo/"
- }
- }
- {
- "items": [
- {
- "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Demo-2392a800be.html",
- "uri": "https://backend.igengmei.com/demo[/]?.*"
- },
- {
- "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Album-1564b12a1c.html",
- "uri": "https://backend.igengmei.com/album[/]?.*"
- }
- ],
- "deploy_time": "Mon Mar 19 2018 19:41:22 GMT+0800 (CST)"
- }
- const App = {
- $router: {
- query: (key) => {
- var search = window.location.search
- var value = ''
- var tmp = []
- if (search) {
- // 生产环境解析 uri
- tmp = (process.env.NODE_ENV === 'production')
- ? decodeURIComponent(search.split('uri=')[1]).split('?')[1].split('&')
- : search.slice(1).split('&')
- }
- for (let i in tmp) {
- if (key === tmp[i].split('=')[0]) {
- value = tmp[i].split('=')[1]
- break
- }
- }
- return value
- }
- }
- }
- App.install = (Vue, options) => {
- Vue.prototype.$router = App.$router
- }
- export default App
来源: https://juejin.im/post/5b4815bbf265da0f9d19efae