详解多页应用 Webpack4 配置优化与踩坑记录


Posted in Javascript onOctober 16, 2018

前言

最近新起了一个多页项目,之前都未使用 webpack4,于是准备上手实践一下。这篇文章主要就是一些配置介绍,对于正准备使用 webpack4 的同学,可以做一些参考。

webpack4 相比之前的 2 与 3,改变很大。最主要的一点是很多配置已经内置,使得 webpack 能“开箱即用”。当然这个开箱即用不可能满足所有情况,但是很多以往的配置,其实可以不用了。比如在之前,压缩混淆代码,需要增加uglify插件,作用域提升(scope hosting)需要增加ModuleConcatenationPlugin。而在 webpack4 中,只需要设置 mode 为 production即可。当然,如果再强行增加这些插件也不会报错。

所以我建议,如果大家想迁移到 webpack4,还是从 0 开始做加法,参考历史,重新做一个配置。而不是从历史的配置里删删减减,再升级为 webpack4。这样 webpack4 的配置会显得更精简。

打包优化

打包优化主要就是多页应用构建时,对所有页面加载的依赖进行合理打包。这个目前业界都已经有了很多实践,包括 webpack4,也有很多文章介绍。我再补充几个不容易注意的小细节。有些点我不详细介绍,不熟悉 webpack 配置的同学可能会不明白,可以搜索对应关键词,网上肯定有非常详细的文章介绍。

首先,构建多页应用,往往会抽离如下几个 chunk 包:

  • common:将被多个页面同时引用的依赖包打到一个 common chunk 中。网上大部分教程是被引入两次即打入 common。我建议可以根据自己页面数量来调整,在我的工程中,我设置引入次数超过页面数量的 1/3 时,才会打入 common 包。
  • dll: 将每个页面都会引用的且基本不会改变的依赖包,如 react/react-dom 等再抽离出来,不让其他模块的变化污染 dll 库的 hash 缓存。
  • manifest: webpack 运行时(runtime)代码。每当依赖包变化,webpack 的运行时代码也会发生变化,如若不将这部分抽离开来,增加了 common 包 hash 值变化的可能性。
  • 页面入口文件对应的page.js

然后我们会给打出的 chunk 包名,注入 contentHash,以实现最大缓存效果。在我们分 chunk 的过程中,最关键的一个思想就是,每次迭代发布,尽量减少 chunk hash 值的改变。这个在业界也有很多非常多的实践,比如这篇文章:https://github.com/pigcan/blog/issues/9

不过在 webpack4 中,我们不用再增加这么多插件啦,一个 optimization 配置完全就能搞定。

我先贴上我的 webpack 的 optimization 配置,然后我再对其做一些介绍,加深大家印象

const commonOptions = {
 chunks: 'all',
 reuseExistingChunk: true
}

export default {
 namedChunks: true,
 moduleIds: 'hashed',
 runtimeChunk: {
  name: 'manifest'
 },
 splitChunks: {
  maxInitialRequests: 5,
  cacheGroups: {
   polyfill: {
    test: /[\\/]node_modules[\\/](core-js|raf|@babel|babel)[\\/]/,
    name: 'polyfill',
    priority: 2,
    ...commonOptions
   },
   dll: {
    test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
    name: 'dll',
    priority: 1,
    ...commonOptions
   },
   commons: {
    name: 'commons',
    minChunks: Math.ceil(pages.length / 3), // 至少被1/3页面的引入才打入common包
    ...commonOptions
   }
  }
 }
}

runtimeChunk

在 webpack4 之前,抽离 manifest,需要使用 CommonsChunkPlugin,配置一个指定 name 属性为'manifest'的 chunk。在 webpack4 中,无需手动引入插件,配置 runtimeChunk 即可。

splitChunks

这个配置能让我们以一定规则抽离想要的包,我们可能会抽好几个包,如 verdor + common,所以 splitChunks 中提供 cacheGroups 字段,cacheGroups 每增加一个 key,就相当于多一个抽包规则。

在网上很多教程中,dll 往往是专门再加一个 webpack 配置,使用 DllPlugin 来构建 dll 库,再在自己项目工程的 webpack 中利用 DllReferencePlugin 来映射 dll 库。虽然这样构建速度会快不少,但是,哎,是真 TM 烦.....

我是一个很怕烦的人,我情愿在 webpack4 中利用 splitChunks,配好规则,再抽离对应的 dll 包。当然这个大家可以自己根据实际情况选择方案。

除了 dll 与 common 两个 chunk,我还加了一个 polyfill。这是因为我们用的某些新的库或者使用某些 ES6+语法(如 async/await)需要 runtime 垫片。比如我工程中使用了 react16,需要增加Map/Set/requestAnimationFrame (https://reactjs.org/docs/javascript-environment-requirements.html)那我必须在 dll 库加载之前增加 polyfill,因此我将所有 core-js 与 babel 引入的包专门打进 polyfill,保证后续加载的 chunk 能执行。priority字段用来配置 chunk 的引入优先级,一般的项目应该都是 polyfill > dll > common > page。

splitChunks 中配置项maxInitialRequests表示在一个入口(entry)中,最大初始请求 chunk 数(不包含按需加载的,即 dom 中 script 引入的 chunk),默认值是 3。我现在 cacheGroups 中已经有三个,又因为配置了 runtimeChunk,会打出 manifest,故而总共有 4 个 chunk 包,超出了默认 3 个,因此需要重新配置值。

moduleIds

稍微了解过 webpack 运行机制的同学会知道,项目工程中加载的 module,webpack 会为其分配一个 moduleId,映射对应的模块。这样产生的问题是一旦工程中模块有增删或者顺序变化,moduleId 就会发生变化,进而可能影响所有 chunk 的 content hash 值。只是因为 moduleId 变化就导致缓存失效,这肯定不是我们想要的结果。

在 webpack4 以前,通过 HashedModuleIdsPlugin 插件,我们可以将模块的路径映射成 hash 值,来替代 moduleId,因为模块路径是基本不变的,故而 hash 值也基本不变。

但在 webpack4 中,只需要optimization的配置项中设置 moduleIds 为 hashed 即可。

namedChunks

除了 moduleId,我们知道分离出的 chunk 也有其 chunkId。同样的,chunkId 也有因其 chunkId 发生变化而导致缓存失效的问题。由于manifest与打出的 chunk 包中有chunkId相关数据,所以一旦如“增删页面”这样的操作导致 chunkId 发生变化,可能会影响很多的 chunk 缓存失效。

在 webpack4 以前,通过增加NamedChunksPlugin,使用 chunkName 来替换 chunkId,实现固化 chunkId,保持缓存的能力。在 webpack4 中,只需在optimization的配置项中设置 namedChunks 为 true 即可。

css 相关

在 webpack4 以前,使用 extract-text-webpack-plugin 插件将 css 从 js 包中分离出来单独打包。在 webpack 中则需要换成 MiniCssExtractPlugin。并且在生产环境或者需要 HMR(模块热替换)时,要用 MiniCssExtractPlugin.loader 替换 style-loader。

注意,这里有个坑。由于开发环境我们会配置热更新,css 的热更新目前MiniCssExtractPlugin.loader自身还待支持,故而还需要增加 css-hot-loader。 切记,css-hot-loader一定不能在生产环境下使用。否则每次构建过程所有 js chunk 包的 contentHash 值都会不一致,进而导致所有 js 缓存失效。 因为生产环境增加这个配置不会有任何报错,页面也能正常构建,故而容易忽视。

简化多页应用的入口文件

使用react/vue等框架的同学知道,我们一般需要一个入口index.js,如这样:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'

ReactDOM.render(<App />, document.getElementById('root'))

如果你还需要使用dva,或者给所有 react 页面增加一个 layout 功能的话,可能就会变成这样:

import React from 'react'
import dva from 'dva'
import Model from './model'
import Layout from '~@/layout'
import App from './app'

const app = dva()
app.router(() => (
 <Layout>
  <App />
 </Layout>
))
app.model(Model)
app.start(document.getElementById('root'))

如果每个页面都这样,略略有点儿难受,因为程序员最怕写重复的东西了。但是它又必须要有,没办法抽离成一个单独文件。因为这个是入口文件,而多页工程,每个页面必须要有自己的入口文件,即使他们长得一模一样。于是,我们的资源目录就会是这样:

- src
 - layout.js
 - pages
  - pageA
   - index.js
   - app.js
   - model.js
  - pageB
   - index.js
   - app.js
   - model.js

因为所有的 index 都一样,我理想中的页面的入口文件仅仅需要app.js就好,像这样:

- src
 - layout.js
 - pages
  - pageA
   - app.js
   - model.js
  - pageB
   - app.js
   - model.js

作为一名前端开发工程师,Node 对于我们来说,应该是熟练运用的工具,而不是仅仅拿别人已经封装好的各类工具。

在这个问题中,我们大可以在 webpack 构建前,通过Node的文件系统(File System),对应我们的每个页面,通过同一个入口文件模板,创建一些临时入口文件:

- src
 - .entires
  - pageA.js
  - pageB.js
 - layout.js
 - pages

然后将这些临时文件,作为 webpack 的 entry 配置。代码如下:

const path = require('path')
const fs = require('fs')
const glob = require('glob')
const rimraf = require('rimraf')
const entriesDir = path.resolve(process.cwd(), './src/.entries')
const srcDir = path.resolve(process.cwd(), './src')

// 返回webpack entry配置
module.exports = function() {
 if (fs.existsSync(entriesDir)) {
  rimraf.sync(entriesDir)
 }
 fs.mkdirSync(entriesDir)
 return buildEntries(srcDir)
}

function buildEntries(srcDir) {
 return getPages(srcDir).reduce((acc, current) => {
  acc[current.pageName] = buildEntry(current)
  return acc
 }, {})
}
// 获取页面数据,只考虑一级目录
function getPages(srcDir) {
 const pagesDir = `${srcDir}/pages`
 const pages = glob.sync(`${pagesDir}/**/app.js`)
 return pages.map(pagePath => {
  return {
   pageName: path.relative(pagesDir, p).replace('/app.js', ''), // 取出page文件夹名
   pagePath: pagePath
  }
 })
}
// 构建临时入口文件
function buildEntry({ pageName, pagePath }) {
 const fileContent = buildFileContent(pagePath)
 const entryPath = `${entriesDir}/${pageName}.js`
 fs.writeFileSync(entryPath, fileContent)
 return entryPath
}
// 替换模板中的 App 模块地址,返回临时入口文件内容
function buildFileContent(pagePath) {
 return `
  import React from 'react'
  import dva from 'dva'
  import Model from './model'
  import Layout from '~@/layout'
  import App from 'PAGE_APP_PATH'

  const app = dva()
  app.router(() => (
   <Layout>
    <App />
   </Layout>
  ))
  app.model(Model)
  app.start(document.getElementById('root'))
 `.replace(PAGE_APP_PATH, pagePath)
}

这样一来,我们就简单的去掉了重复的入口文件,还增加了一个 layout 的功能。这只是简单的代码,实际项目可能还有多级目录,多个 model 等等,需要自己再定制啦。

webpack4出来已经挺久了,文章写的有点儿滞后了,所以很多我觉得应该大家都明白的地方就没详细写了。如果还有什么疑问的话,欢迎评论~~

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
贴一个在Mozilla中常用的Javascript代码
Jan 09 Javascript
JS input 数字验证代码
Jul 30 Javascript
JS随机生成不重复数据的实例方法
Jul 17 Javascript
Js获取下拉框选定项的值和文本的实现代码
Feb 26 Javascript
jQuery中slice()方法用法实例
Jan 07 Javascript
Web表单提交之disabled问题js解决方法
Jan 13 Javascript
JavaScript实现在页面间传值的方法
Apr 07 Javascript
javascript函数式编程实例分析
Apr 25 Javascript
理解javascript闭包
Dec 15 Javascript
微信小程序 网络请求(GET请求)详解
Nov 16 Javascript
深入理解Angularjs 脏值检测
Oct 12 Javascript
vue prop传值类型检验方式
Jul 30 Javascript
js拖动滑块和点击水波纹效果实例代码
Oct 16 #Javascript
ajax与jsonp的区别及用法
Oct 16 #Javascript
JS调用安卓手机摄像头扫描二维码
Oct 16 #Javascript
QRCode.js二维码生成并能长按识别
Oct 16 #Javascript
移动端图片上传旋转、压缩问题的方法
Oct 16 #Javascript
JavaScript实现表单注册、表单验证、运算符功能
Oct 15 #Javascript
从零开始搭建vue移动端项目到上线的步骤
Oct 15 #Javascript
You might like
简单的PHP图片上传程序
2008/03/27 PHP
php过滤html中的其他网站链接的方法(域名白名单功能)
2014/04/24 PHP
phpword插件导出word文件时中文乱码问题处理方案
2014/08/19 PHP
PHP动态页生成静态页的3种常用方法
2014/11/13 PHP
PHP中的事务使用实例
2015/05/26 PHP
php根据数据id自动生成编号的实现方法
2016/10/16 PHP
PHP中cookie知识点学习
2018/05/06 PHP
PHP开发实现快递查询功能详解
2019/04/08 PHP
php实现统计IP数及在线人数的示例代码
2020/07/22 PHP
RGB颜色值转HTML十六进制(HEX)代码的JS函数
2009/04/25 Javascript
javascript中关于break,continue的特殊用法与介绍
2012/05/24 Javascript
innerHTML,outerHTML,innerText,outerText的用法及区别解析
2013/12/16 Javascript
JS实现简单的顶部定时关闭层效果
2014/06/15 Javascript
如何用js 实现依赖注入的思想,后端框架思想搬到前端来
2015/08/03 Javascript
Bootstrap每天必学之表格
2015/11/23 Javascript
浅谈Node 调试工具入门教程
2018/03/20 Javascript
详解angular2 控制视图的封装模式
2018/12/27 Javascript
在Vue中使用Echarts可视化库的完整步骤记录
2020/11/18 Vue.js
[00:52]黑暗之门更新 新英雄孽主驾临DOTA2
2016/08/24 DOTA
python编程通过蒙特卡洛法计算定积分详解
2017/12/13 Python
python 将dicom图片转换成jpg图片的实例
2020/01/13 Python
利用 PyCharm 实现本地代码和远端的实时同步功能
2020/03/23 Python
解决Keras 中加入lambda层无法正常载入模型问题
2020/06/16 Python
详解Django自定义图片和文件上传路径(upload_to)的2种方式
2020/12/01 Python
使用Html5多媒体实现微信语音功能
2019/07/26 HTML / CSS
LACOSTE波兰官网:Polo衫、服装和鞋类
2020/09/29 全球购物
建筑装饰学院室内设计专业个人自我评价
2013/12/07 职场文书
珠宝店促销方案
2014/03/21 职场文书
群众路线教育实践活动个人对照检查材料
2014/09/22 职场文书
2014年网络管理员工作总结
2014/12/01 职场文书
2015年光棍节活动总结
2015/03/24 职场文书
2016七夕情人节寄语
2015/12/04 职场文书
原型和原型链 prototype和proto的区别详情
2021/11/02 Javascript
基于Redis6.2.6版本部署Redis Cluster集群的问题
2022/04/01 Redis
Windows Server 版本 20H2 于 8 月 9 日停止支持,Win10 版本 21H1 将于 12 月结束支
2022/07/23 数码科技
MySQL下载安装配置详细教程 附下载资源
2022/09/23 MySQL