一, 前言
这篇文章会分享一下我是如何针对混合开发 (Hybird h5 端) 进行 gong
内容涉及到如下几点:
(1)混合开发 h5 端页面特点以及需求
(2)如何利用 webpack4 进行多入口的代码分割以达到较优的缓存利用率
(3)如何针对首屏渲染进行优化
(4)使用 webpack4 如何同时输出 ES next(现代)与 es5(向后兼容)的包
(5)客户端打包 h5 资源到包内策略
代码仓库 https://github.com/smallcatcat-joe/webpack-esnext-cli , 大家也可以边看代码边理解, 当然能自己动手搭建一次是最好的
二, 混合开发 h5 端页面特点以及需求
混合开发的 h5 端页面有什么特点?
入口繁杂, 部分页面仅文案, 需要较快的首屏渲染速度, 部分页面的使用率不高, 依赖网络的速度快慢
(1)入口繁杂
入口繁杂其实意味着你的前端工程搭建必须是以多入口为起点搭建的, 如 webpack 你可以配置 entry, 自行写一个脚本在构建时获取每一个页面的 js 入口, 而多入口意味着你必须考虑页面之间共享的模块应该如何抽取以达到一个较优的模块利用率, 这点我们在文章下一节详细讲.
(2)部分页面仅文案, 且利用率不高
其实有做过混合开发你自然会发现有一些页面是只有文案, 没有任何 js 交互的, 这类的页面, 类似用户协议, 常见问题之类的文案类页面, 使用率不高, 对首屏又有要求的我们完全可以将 html 写在 html 文件中, 不需要用 vue 这类的框架去写虚拟 DOM 依赖框架去渲染, 这样可以节省掉请求脚本以及框架运行渲染的时间, 保证文字可以快速出现在页面当中.
(3)较快的首屏渲染以及网络的快慢
因为我们的 h5 页面是比较依赖手机网络的, 它不像客户端资源都是大包到本地的, 现在虽然 4g 普及了, 但是用户很多时候网络其实并不好, 那在弱网的情况下对页面的打开速度就有了一定的挑战了, 从工程角度上考虑, 我们可不可以像客户端那样把 h5 需要的资源也打包进本地呢? 答案当然是可以的, 后面我会陆续讲到.
三, 如何利用 webpack4 进行代码分割
根据上面第二点我们提到的页面特点和需求, 我们的多页面共享的模块应该是下面这样的:
1, 每个入口基本都需要用到的包应该长期缓存(hash 值不变)
2, 共享的 chunk 可自由分割
3, 共享的 chunk 可在页面配置引用
那我们先来看看在 webpack4 中我们可以通过什么手段保证如上三点
例如在 vue 的工程当中, 我们的每个入口基本都依赖于 vue.js, 而在我们打出的包中 vue 所占的资源大小比重也是比较大的, 而这部分就是我们需要长期缓存的. 关于这个点我在我的另一篇文章中讲过. 这里我们在 webpack4 中会单独将 vue 打包成 vendor 作为项目的基础包供页面引入(当然你也可以将其余的模块打包)
- ...
- optimization: {
- splitChunks: {
- ...
- 'vendor': {
- test: /node_modules\/vue/,
- name: 'vendor',
- chunks: 'all',
- enforce: true,
- priority: 2
- },
- ...
- }
- },
- plugins: [
- // 稳定 moduleId, 避免引入了一个新模块后, 导致模块 ID 变更使得 vender 和 common 的 hash 变化后缓存失效
- new webpack.HashedModuleIdsPlugin(),
- ]
- ...
复制代码
单独将 vue 打包进 vendor 后, 我们就保证了一个基础模块是稳定的, 但我们还需要一些灵活性, 比如我们有一些复杂的页面可能会做成单页面, 这时候我们就需要引入 vue-router,vuex 这类的工具.
你可能会说为什么不把这些包打包进 vendor 中? 因为混合开发中页面的入口是非常繁杂的, 如果用户打开一个普通的页面, 仅依赖 vue 就可以了, 但是因为你抽取的时候把 vue-router 也打包进去了, 导致用户下载了一个它可能用不到的又比较大的文件, 同时这样也会影响到其他页面的渲染速度, 因为 js 包太大了, 导致下载时间过长, 而分开打包能起到一个增量下载的作用.
这时候我们在 splitChunks 中增加一项 spa-vendor 配置:
- optimization: {
- splitChunks: {
- ...
- // 项目基础包
- 'vendor': {
- test: /node_modules\/vue/,
- name: 'vendor',
- chunks: 'all',
- enforce: true,
- priority: 2
- },
- // 单页面需要引入 vue-router, vuex, 这里单独分割出来
- 'spa-vendor': {
- test: /node_modules\/vue-router/g,
- name: 'spa-vendor',
- chunks: 'all',
- enforce: true,
- priority: 10
- },
- ...
- }
- },
复制代码
好了, 到这里, 我们已经把项目一些比较大的, 不常变更的包独立分割出来并且做到持久缓存了, 那剩余的大小不那么大的包我们就可以让 webpack 根据大小和引用率去自动打包了, 这里我们加一个 commons 包的配置
- optimization: {
- splitChunks: {
- ...
- // 项目基础包
- 'vendor': {
- test: /node_modules\/vue/,
- name: 'vendor',
- chunks: 'all',
- enforce: true,
- priority: 2
- },
- // 单页面需要引入 vue-router, vuex, 这里单独分割出来
- 'spa-vendor': {
- test: /node_modules\/vue-router/g,
- name: 'spa-vendor',
- chunks: 'all',
- enforce: true,
- priority: 10
- },
- // 剩余 chunk 自动分割
- 'commons': {
- name: 'commons',
- minChunks: 5, // 引用次数大于 5 则打包进 commons
- minSize: 3000, // chunk 大小大于这个值才允许打包进 commons
- chunks: 'all',
- priority: 1
- }
- ...
- }
- },
复制代码
大家看到这里会看到 splitChunk 中每个 chunk 的 priority(优先级)是不一样的, commons 的优先级是最低的, 因为要等到 spa-vendor 和 vendor 抽取完成后才会到 commons 抽取
完成后, 打出的包是下面这样的
vendor(60k):
spa-vendor(23k, 还是比较大的):
commons:
自由分割和长期缓存我们已经做到了, 那剩下的就是 chunk 在页面中自由引入了. 在我写的 webpack-esnext-cli 中, 我是用了 nunjucks 模板引擎去做页面的资源引入的, 利用 webpack4 和 webpack-manifest-plugin 插件在打包后输出的资源表, 对资源进行页面的自由配置
比如, 需要引入 spa-vendor 的单页, 我们会引入 manifest,vendor,spa-vendor,commons 包, 页面入口 js(业务文件)默认引入
- <!DOCTYPE html>
- <html lang="en" bgc-f7f7f7>
- <head>
- ...
- </head>
- <body>
- <div id="app"></div>
- <!-- 以注释的方式添加模板语法, addAssets 方法可以注入对应模块组(按顺序) -->
- <!-- {{ 'js' | addAssets(['manifest', 'vendor', 'spa-vendor', 'commons']) }} -->
- </body>
- </html>
复制代码
如果仅仅是普通的只需要基于 vue 的, 我们会引入 manifest,vendor,commons(这里的 addAssets 方法传入空数组默认引入这几个 chunk), 这样我们在页面打开时, 就不需要加载 spa-vendor 了, 达到一个模块冗余的作用
- <!DOCTYPE html>
- <html lang="en" bgc-f7f7f7>
- <head>
- ...
- </head>
- <body>
- <div id="app"></div>
- <!-- 以注释的方式添加模板语法, addAssets 方法可以注入对应模块组(按顺序) -->
- <!-- {{ 'js' | addAssets([]) }} -->
- </body>
- </html>
复制代码
当然你可以更细致的区分你的 chunk 应该如何去分割, 这里只是演示一下.
四, 使用 webpack4 输出 ES next 语法的包
PHILIP WALTON https://philipwalton.com/articles/deploying-es2015-code-in-production-today/ 这篇文章讲解了输出 es next 的原理, 那时候看得我是激情澎湃, 所以就自己花了时间去实现自己的一套
这里就讲一下思路吧, 代码实现大家可以自行去看仓库代码
在打包的时候, 我们需要输出两套包
原理其实就是通过改变 broswerList 让 babel 编译出不同语法的包
modern(es6):
legacy(es5):
构建 es6 语法的包后我们需要输出 es5 的入口文件, 为了避免输出的资源表重叠的情况需要给 es5 的入口重新命名
其中 a.js 为我们的 es6 构建入口, 脚本创建的 a-legacy.js 为我们的 es5 包构建入口, 内容如下:
- // a-legacy.js
- import './a.js'
复制代码
打包时, 我们根据 js 入口生成对应的 html 文件后, 根据每次打包生成的资源表选择资源进行插入
资源表:
如上图, 资源表对应了原本的路径与输出后的资源路径, 在输出 html 时根据路径去做资源匹配就可以了
输出后的 html 如下(modern 包的 manifest 文件内联了):
其中支持 type=module 语法的浏览器就会自动加载 es6 语法的包, 不支持则加载 es5 的向后兼容包
这么做其实效益是非常大的, 下面引用两张 PHILIP WALTON 文章的图
可见输出的 es6 的包不管在 size 还是解析速度都是优于 es5 语法的包的, 对于移动端加速效果还是非常大的
五, 加速首屏与将 h5 资源打包进客户端
加速首屏要做什么? 就是让内容尽快的出现啊.
在之前我们已经通过模块分割和输出 es next 包让我们的资源利用率和大小, 还有代码解析的速度都得到了提升了, 接下来我们应该要考虑一下如何利用客户端的能力进行优化了.
加速首屏, 无非就是加快 webview 的启动速度, 和减少包的下载时间嘛, 因为首屏的速度很大一部分原因是因为资源下载导致页面阻塞.
设想一下, 如果 webview 可以拦截我们的资源请求, 那我们是不是就可以把我们页面的 js 与 CSS 等静态资源一起打包到客户端中, 在客户端开启 webview 后, 通过拦截 url, 对本地资源进行 url 的匹配, 命中则读取本地文件, 文件过期则再次从服务器上拉取, 甚至可以让服务端做一个推送服务更新资源文件, 能做到这样 h5 页面在客户端基本能达到秒开了, 也能减轻服务器的压力, 在弱网的情况下优化非常明显.
当然首屏你还可以通过构建预渲染一部分 html 到 html 文件中, 我的另一篇文章中有讲 -- 如何在 webpack 中做预渲染降低首屏空白时间, 这里就不再说了
六, 总结
Hybird h5 的工程搭建, 其实更多是根据需求去做的, 使用 webpack 只是一种方式, 更重要的我觉得是对资源加载, 缓存的理解和运用. 不说了, 不说了, 该时候搬砖了. 有什么疑问或者建议欢迎提出.
来源: https://juejin.im/post/5b80b1bfe51d4538d51763d0