多页应用 Webpack4 配置优化与踩坑记录
前言
最近新起了一个多页项目,之前都未使用 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/blo...
不过在 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/java...。那我必须在 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
出来已经挺久了,文章写的有点儿滞后了,所以很多我觉得应该大家都明白的地方就没详细写了。如果还有什么疑问的话,欢迎评论~~
原文地址:https://segmentfault.com/a/1190000016685119
多页应用 Webpack4 配置优化与踩坑记录的更多相关文章
- 在mac版virtual box中安装ubuntu虚拟机的NAT/Host-Only网络配置踩坑记录
之前用惯了vmware和parallels desktop,网络配置十分智能,基本不用自己配置.由于版权原因,工作电脑上换了免费的virtual box用,四五年都完全在虚拟机里干活的本菜鸡居然在虚拟 ...
- SpringBoot+SpringSecurity+Thymeleaf认证失败返回错误信息踩坑记录
Spring boot +Spring Security + Thymeleaf认证失败返回错误信息踩坑记录 步入8102年,现在企业开发追求快速,Springboot以多种优秀特性引领潮流,在众多使 ...
- 复杂业务下向Mysql导入30万条数据代码优化的踩坑记录
从毕业到现在第一次接触到超过30万条数据导入MySQL的场景(有点low),就是在顺丰公司接入我司EMM产品时需要将AD中的员工数据导入MySQL中,因此楼主负责的模块connector就派上了用场. ...
- VUE使用微信JDK(附踩坑记录)
VUE使用微信分享SDK(附踩坑记录) 微信分享官方文档 安装JS-SDK npm i -S weixin-jsapi 引入包 ES5 写法 const wx = require('weixin-js ...
- manjaro xfce 18.0 踩坑记录
manjaro xfce 18.0 踩坑记录 1 简介1.1 Manjaro Linux1.2 开发桌面环境2 自动打开 NumLock3 系统快照3.1 安装timeshift3.2 使用times ...
- CentOS7.4安装MySQL踩坑记录
CentOS7.4安装MySQL踩坑记录 time: 2018.3.19 CentOS7.4安装MySQL时网上的文档虽然多但是不靠谱的也多, 可能因为版本与时间的问题, 所以记录下自己踩坑的过程, ...
- SpringBoot + Shiro + shiro.ini 的踩坑记录
0.写在前面的话 好久没写博客了,诶,好多时候偷懒直接就抓网上的资料丢笔记里了,也就没有自己提炼,偷懒偷懒.然后最近参加了一个网络课程,要交作业的那种,为了能方便看下其他同学的作业,就写了个爬虫把作业 ...
- 你真的了解字典(Dictionary)吗? C# Memory Cache 踩坑记录 .net 泛型 结构化CSS设计思维 WinForm POST上传与后台接收 高效实用的.NET开源项目 .net 笔试面试总结(3) .net 笔试面试总结(2) 依赖注入 C# RSA 加密 C#与Java AES 加密解密
你真的了解字典(Dictionary)吗? 从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点.为了便于描述,我把上面 ...
- ABP框架踩坑记录
ABP框架踩坑记录 ASP.NET Boilerplate是一个专用于现代Web应用程序的通用应用程序框架. 它使用了你已经熟悉的工具,并根据它们实现最佳实践. 文章目录 使用MySQL 配置User ...
随机推荐
- Appium + Python自动化3 - 输入中文
在做app自动化过程中会踩很多坑,咱们都是用中文的app,所以首先要解决中文输入的问题!本篇通过屏蔽软键盘,绕过手机的软键盘方法,解决中文输入问题. 一.定位搜索 1.打开淘宝点击搜索按钮,进入搜索页 ...
- Python lambda(匿名函数)介绍【转】
引用: http://www.cnblogs.com/evening/archive/2012/03/29/2423554.html 在学习python的过程中,lambda的语法时常会使人感到困惑, ...
- 系统启动时,BIOS与影子内存_5
问题:“当我们按下电源开关时,电源开始供电,芯片组撤去RESET信号,CPU马上就从地址FFFF0H处开始执行指令,这个地址在系统BIOS的地址范围内,无论是Award BIOS还是AMI BIOS, ...
- Flume NG部署
本次配置单节点的Flume NG 1.下载flume安装包 下载地址:(http://flume.apache.org/download.html) apache-flume-1.6.0-bin.ta ...
- bash:haoop:command not found
今天重新搭建了一个3节点的Hadoop集群,想着在上面测试一个MapReduce实例,然后就出现了以下错误: [hadoop@master hadoop-]$ hadoop -bash: hadoop ...
- 一般的linux系统默认安装的vim是精简版
一般的linux系统默认安装的vim是精简版(vim-tiny),所以不能配置语法检查等属性或获取在线帮助.需要安装vim-x:x.x.x,vim-common,vim-runtime. :synta ...
- iShow UI for React 最佳实践
React 与 AJAX React只负责处理View这一层,它本身不涉及网络请求/AJAX,所以这里我们需求考虑两个问题: 第一,用什么技术从服务端获取数据: 第二,获取到的数据应该放在react组 ...
- Java并发(四):并发集合ConcurrentHashMap的源码分析
之前介绍了Java并发的基础知识和使用案例分析,接下来我们正式地进入Java并发的源码分析阶段,本文作为源码分析地开篇,源码参考JDK1.8 OverView: JDK1.8源码中的注释提到:Conc ...
- 父类和子类以及super关键字
super和this关键字的特点类似:super代表的是父类对象的引用. 当子父类的成员出现同名时,可以通过super来进行区分. 子类的构造方法中,通过super关键字调用父类的构造方法. publ ...
- 【Java/Android性能优 6】Android 图片SD卡缓存 使用简单 支持预取 支持多种缓存算法 支持不同网络类型 支持序列化
本文转自:http://www.trinea.cn/android/android-imagesdcardcache/ 本文主要介绍一个支持图片自动预取.支持多种缓存算法.支持数据保存和恢复的图片Sd ...