详解多页应用 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 相关文章推荐
Jquery命名冲突解决的五种方案分享
Mar 16 Javascript
文本框中禁止非数字字符输入比如手机号码、邮编
Aug 19 Javascript
javascript页面加载完执行事件代码
Feb 11 Javascript
Javascript学习笔记之数组的构造函数
Nov 23 Javascript
Vue.js实现无限加载与分页功能开发
Nov 03 Javascript
js+html制作简单日历的方法
Jun 27 Javascript
详解Vue项目引入CreateJS的方法(亲测可用)
May 30 Javascript
一次让你了解全部JavaScript的作用域
Jun 24 Javascript
javascript简单实现深浅拷贝过程详解
Oct 08 Javascript
适合前端Vue开发童鞋的跨平台Weex的使用详解
Oct 16 Javascript
highcharts.js数据绑定方式代码实例
Nov 13 Javascript
vue 微信分享回调iOS和安卓回调出现错误的解决
Sep 07 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调用Google translate_tts api实现代码
2013/08/07 PHP
PHP通过内置函数memory_get_usage()获取内存使用情况
2014/11/20 PHP
php设计模式之单例模式代码
2016/06/11 PHP
php查找字符串中第一个非0的位置截取
2017/02/27 PHP
thinkPHP5.0框架URL访问方法详解
2017/03/18 PHP
Laravel 实现添加多语言提示信息
2019/10/25 PHP
PHP设计模式之外观模式(Facade)入门与应用详解
2019/12/13 PHP
JavaScript 函数调用规则
2009/09/14 Javascript
引用 js在IE与FF之间的区别详细解析
2013/11/20 Javascript
node.js中的url.format方法使用说明
2014/12/10 Javascript
JQuery的ON()方法支持的所有事件罗列
2015/02/28 Javascript
简单谈谈javascript中this的隐式绑定
2016/02/22 Javascript
promise处理多个相互依赖的异步请求(实例讲解)
2017/08/03 Javascript
vue脚手架中配置Sass的方法
2018/01/04 Javascript
Vue引入jquery实现平滑滚动到指定位置
2018/05/09 jQuery
重新认识vue之事件阻止冒泡的实现
2018/08/02 Javascript
vue 动态绑定背景图片的方法
2018/08/10 Javascript
swiper在angularjs中使用循环轮播失效的解决方法
2018/09/27 Javascript
详解原生JS动态添加和删除类
2019/03/26 Javascript
mpvue小程序循环动画开启暂停的实现方法
2019/05/15 Javascript
Python的__builtin__模块中的一些要点知识
2015/05/02 Python
使用Python的urllib2模块处理url和图片的技巧两则
2016/02/18 Python
python之从文件读取数据到list的实例讲解
2018/04/19 Python
实列教程 一款基于jquery和css3的响应式二级导航菜单
2014/11/13 HTML / CSS
详解HTML5将footer置于页面最底部的方法(CSS+JS)
2018/10/11 HTML / CSS
去加拿大的旅行和假期:Canadian Affair
2016/10/25 全球购物
英国高档百货连锁店:John Lewis
2017/11/20 全球购物
巧克力蛋糕店创业计划书
2014/01/14 职场文书
小学运动会入场式解说词
2014/02/18 职场文书
党务公开方案
2014/05/06 职场文书
全国税务系统先进集体事迹材料
2014/05/19 职场文书
党员干部观看《周恩来四个昼夜》思想汇报
2014/09/10 职场文书
收款委托书范本
2014/09/11 职场文书
2015年财务经理工作总结
2015/05/13 职场文书
2015年第31个教师节致辞
2015/07/31 职场文书
Python四款GUI图形界面库介绍
2022/06/05 Python