本文介绍一款基于 Vue 的使 App 支持离线缓存 Web 资源的混合开发框架。本人小白一枚,请将它视做一份个人学习总结,欢迎大神们赐教。本文多阐述思路,实现细节请阅读源码。css
源码html
高效率界面开发:HTML + CSS + JavaScript 被证明具有极高的界面开发效率。前端
跨平台:较统一的浏览器内核标准,使 H5 页面在 IOS、Android 共享同套代码。使用 Native 开发一功能需 IOS、Android 研发各一枚,而使用 H5 一枚前端工程师足矣。但混合 App 并不是 Native 越少越佳,性能要求较高的仍需劳 Native 大驾...分工需明确,不可厚此薄彼。vue
热更新:不依赖于发布渠道自主更新应用。Native 修复线上 Bug 需发布新版本,用户未升级 App 该 Bug 将一直呈现。而修复 H5 只需将 Fixbug 的代码推至服务器,任一版本 App 即可同步更新对应功能无需升级。node
相比于从远程服务器请求加载 Web 资源,App 优先加载本地预置资源,可提高页面响应速度,节省用户流量。webpack
问题来了...本地预置的 Web 资源也随 App 安装包一块儿成为泼出去的水,修复 H5 线上 Bug 也需发版了?丢西瓜捡芝麻的事定不可作!请注意“优先加载本地预置资源”,但检测到更新时加载远程最新资源,如何检测更新我稍后阐明。git
实现先后端分离:原 Jinja 为 Python 模板引擎,前端代码的运做依赖于服务端,服务端异常等待环境维修严重影响前端工做进度。分离后,服务器挂了咱们愉快的开启 Mock Server 继续搬砖即是。github
App 优先加载本地预置 Web 资源,可提高 H5 页面加载速度。web
技术重构自己具有风险性。vue-router
增长团队学习成本。
前端框架经过 JS 渲染 HTML 对 SEO 不友好。但你可选择使用 Vue 2.2 的服务端渲染(SSR)。增添 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 框架设计围绕:
减小无用资源及冗余资源
减少依赖模块对 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 达成对第三方库,公用组件的抽离。一提取项目所应用 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。
因为 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 不会受影响。
一个项目参与者众多,开发环境模式复杂将提升学习成本与风险。在简化开发模式上我作了哪些:
先讲下 Vue 多页面拆分如何作。相关文章不少在此推荐一篇,点我~
核心思想:
单页:多 View 对应 单 index.html + 单 entry.js。
多页:多 View 对应 多 index.html + 多 entry.js。
假定含 100 个 View 则需对应建立 100 个 index.html、100 个 entry.js!但它们几乎如出一辙,重复建立十分浪费,开发成本也被增长。
index.html 可被多个 View 复用,entry.js 不可。共享 entry 需在其中 import 所有 View,则 build 生成的每一页面含每一 View 的所有资源,即 100 个内容如出一辙的 .html。
咱们可形式上单入口,实际上多入口,如何作?定义一含占位符的 entry 模板,build 时将占位符替换为对应 View 的引入,如此 import 资源将按需拆分。
含 <%=Page%> 占位符的 entry.js:
import Vue from 'vue'
import Page from '<%=Page%>'
/* eslint-disable no-new */
new Vue({
el: '#app',
template: '<Page />',
components: {
Page
}
})
复制代码
生成多 entry 的 gulp task:
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
})
}
})
复制代码
仅生产环境执行 gulp entries 构造多入口,开发环境单入口便可,免去研发同窗构造 entry 的成本。
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
}
复制代码
因为 App 仅监听 .html 变化,图片资源需从远程引用。研发自行上传图片至 CDN 彷佛并不复杂,但我司 CDN 上传权限泛滥是不被容许的。
图片上传交专人负责,方法原始沟通成本高,等待他人上传也影响自身开发效率。
开发阶段将图片上传测试 CDN,生产阶段再统一拷贝至线上环境?转化成本不小,遗漏上传还会引起线上事故。
开发阶段书写相对路径引用本地资源,免去研发自行上传图片的烦恼且模式与传统 Web 开发保持一致。生产环境直接转化图片连接为 CDN 路径。并将全部 image 单独打包至 assets/ 一同上传 CDN,此时 .html 对 CDN 图片的引用生效了。
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 1,
name: 'assets/imgs/[name]-[hash:10].[ext]'
}
}
复制代码
为防止 CDN 缓存致使图片没法及时更新,build 后图片名称添加 Hash 后缀。在此我设置 Base64 转化 limit 为 1,防止 HTML 穿插过多 Base64 格式图片阻塞加载。
生产环境图片连接转化 CDN 路径代码以下:
const settings = require('../settings')
module.exports = {
dev: {
// code...
},
build: {
assetsRoot: path.resolve(__dirname, '../../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: `${settings.cdn}/`,
// code...
}
}
复制代码
html-webpack-inline-source-plugin、gulp-inline-source:JS、CSS 资源内联工具。
commons-chunk-plugin:公共模块拆分工具。
gulp-rev、hashed-module-ids-plugin:MD5 签名生成工具。
gulp-zip:压缩工具。
其它经常使用 Gulp 工具:gulp-rename、gulp-replace-task、del
假定路由配置为:
{
"/demo": {
"view": "Demo",
"path": "demo/",
"query": [
"topic_id",
"service_id"
]
},
"/album": {
"view": "Album",
"path": "demo/"
}
}
复制代码
生成 routes.json 为:
{
"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)"
}
复制代码
开发环境经过 localhost:8080/demo?topic_id=&service_id= 访问 Demo 页面,形如 vue-router 为咱们构建的路由。而生产环境访问路径为 file:////dist/demo/Demo-2392a800be.html?uri=https%3A%2F%2Fbackend.igengmei.com%2Fdemo%3Ftopic_id%3D%26service_id%3D,获取参数需解析 uri。
因两大环境参数解析方式不一样,需自行封装 $router,例如 this.$router.query 的定义:
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
}
}
}
复制代码
可将 $router 绑定至 Vue.prototype:
App.install = (Vue, options) => {
Vue.prototype.$router = App.$router
}
export default App
复制代码
在 entry.js 执行:
Vue.use(App)
复制代码
此时任一 .vue 可直接调用 this.$router,无需 import。调用频率较高的 method 都可 bind 至 Vue.prototype,例如对请求的封装 this.$request。
缺陷:自制 router 仅支持 query 参数不支持 param 参数。
App 加载本地预置资源在 file:/// 域,没法直接将 Cookie 载入 Webview,对 file:/// 开放 Cookie 将致使安全问题。几种解决思路:
区分 file:/// 来源,断定来源安全则载入 Cookie,但 H5 依然没法将 Cookie 带到请求中。
伪造相似 http 请求造成假域。
Native 维护 Cookie 并提供获取接口,H5 拼接 Cookie 自行写入 Request Header。
Native 代发请求回传返回值,但没法实现大数据量 POST 请求(例 POST File)。
一般在页面 render 时服务器会将 CSRFToken 写入 Cookie,Request 时再将 CSRFToken 传回服务器防止跨域攻击。但加载本地 HTML 缺乏上述步骤,需额外注意 CSRFToken 的获取问题。
未完待续~
做者:呆恋小喵
个人后花园:sunmengyuan.github.io/garden/
个人 github:github.com/sunmengyuan
原文连接:sunmengyuan.github.io/garden/2018…