详解多页应用 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/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出来已经挺久了,文章写的有点儿滞后了,所以很多我觉得应该大家都明白的地方就没详细写了。如果还有什么疑问的话,欢迎评论~~

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

(0)

相关推荐

  • webpack4 + react 搭建多页面应用示例

    webpack 升级到4之后还没好好的同步一个可实用的webpack架子,接下来用webpack4来搭建一个简单的react的多界面应用,废话不说 直接撸码 创建工程 $ mkdir demo && cd demo $ npm init -y webpack 配置 安装react + babel 依赖 $ npm i -S react@16.3.0 react-dom@16.3.0 $ npm i -D webpack@4.4.1 webpack-cli@2.0.13 webpack-de

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

    前言 最近新起了一个多页项目,之前都未使用 webpack4,于是准备上手实践一下.这篇文章主要就是一些配置介绍,对于正准备使用 webpack4 的同学,可以做一些参考. webpack4 相比之前的 2 与 3,改变很大.最主要的一点是很多配置已经内置,使得 webpack 能"开箱即用".当然这个开箱即用不可能满足所有情况,但是很多以往的配置,其实可以不用了.比如在之前,压缩混淆代码,需要增加uglify插件,作用域提升(scope hosting)需要增加ModuleConca

  • 详解Spring Cloud Feign 熔断配置的一些小坑

    1.在使用feign做服务调用时,使用继承的方式调用服务,加入Hystrix的熔断处理fallback配置时,会报错,已解决. 2.使用feign默认配置,熔断不生效,已解决. 最近在做微服务的学习,发现在使用feign做服务调用时,使用继承的方式调用服务,加入Hystrix的熔断处理fallback配置时,会报错,代码如下: @RequestMapping("/demo/api") public interface HelloApi { @GetMapping("user/

  • 详解 Nginx 301重定向的配置

    详解 Nginx 301重定向的配置 301重定向是很常见的需求,比如访问 nowamagic.net,自动跳到 www.nowamagic.net.或者倒过来,访问 www.nowamagic.net 跳到 nowamagic.net.Nginx 中配置 301 重定向(301 redirect)很容易,下面介绍下方法. 打开 nginx.conf 文件,找到你的 server 配置段: server { listen 80; server_name nowamagic.net www.now

  • 详解log4j.properties的简单配置和使用

    本文介绍了详解log4j.properties的简单配置和使用,分享给大家,具体如下: 简单log4j.properties配置示例 ### set log levels ### log4j.rootLogger = INFO , console , debug , error ### console ### log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = Syst

  • 详解nginx使用ssl模块配置支持HTTPS访问

    背景: 项目开发中用到了微信小程序,但是服务器配置URL必须是HTTPS,所以需要通过配置nginx的SSL模块来支持HTTPS访问,也就是说,要做一个网站域名为 dmsdbj.com 要求通过HTTPS://dmsdbj.com进行访问. SSL英文名为Secure Socket Layer,安全套接字层.SSL是一种数字证书,它使用ssl协议在浏览器和web server之间建立一条安全通道,数据信息在client与server之间的安全传输. 本篇博客是对这个操作步骤的详解. 前提: 1.

  • 详解VMware12安装centOS8的配置图文教程(vm虚拟机安装centos8教程)

    前几天Centos8发布了,尽管他是8的第一个版本,那么今天我们就在VM12上面安装centOS8吧,8这个图形化界面我个人感觉有点丑 首先下载iso文件百度下点击进入官网 点击马上获得centos 然后选择这个 选择离你近的镜像地址,点击下载 打开vm12点击新建虚拟机 点击下一步,如下图这样选择,再点击下一步 如下图选择点击下一步 叫什么名字没什么所谓反正可以改的,但是安装最好不要安装到c盘,我是安装到D盘 如果你的物理cpu是4核心,在这里建议使用4核心,这样后期虚拟机运行快.因为我是8核

  • 详解ASP.NET Core中配置监听URLs的五种方式

    默认情况下,ASP. NET Core应用会监听一下2个Url: http://localhost:5000 https://localhost:5001 在本篇博文中,我将展示如何使用五种不同的方式改变应用监听的URLs. 在ASP.NET Core项目启动时,有多种配置监听Url的方式,在我之前的一篇博客中,已经展示了在ASP.NET Core 1.0中如何应用不同的方式配置,在ASP.NET Core 3.x中,大部分方式还是一样的. UseUrls() - 在Program.cs配置程序

  • 详解Xshell 常见问题及相关配置

    本文介绍Xshell 常见的问题以及相关的配置.本文的配置主要是针对 Xshell 5 或 Xshell 6 版本的. 说明:涉及到对"属性"进行的配置,如果当前Xshell已经连接到了一台服务器,那么在此会话窗口中进行的属性配置,只针对该服务器会话生效:如果想要对所有的会话属性进行配置,则需要在未连接服务器的会话窗口中进行相关的属性配置操作. 1. vi编辑器中,INSERT模式下Backspace按键无法删除字符的问题 要解决上述问题,需要进行以下设置: a)点击"属性&

  • 详解ASP.NET Core3.0 配置的Options模式

    上一章讲到了配置的用法及内部处理机制,对于配置,ASP.NET Core还提供了一种Options模式. 一.Options的使用 上一章有个配置的绑定的例子,可以将配置绑定到一个Theme实例中.也就是在使用对应配置的时候,需要进行一次绑定操作.而Options模式提供了更直接的方式,并且可以通过依赖注入的方式提供配置的读取.下文中称每一条Options配置为Option. 1.简单的不为Option命名的方式 依然采用这个例子,在appsettings.json中存在这样的配置: { "Th

  • 详解python日志输出使用配置文件格式

    python脚本日志输出使用配置文件的形式,不需要在每个脚本里面配置日志. 需求简述: 如我要写2个脚本(a.py和b.py),a.py日志输出到/var/log/a.log,b.py日志输出到/var/log/b.log,并且日志按日期切割.如果每个脚本都去配置一遍日志的话,浪费时间也不利于后期维护. 现在我要使用配置文件的格式去统一管理python脚本的代码日志输出,后续所有python脚本日志都在这个配置文件里面配置,脚本读取.方便后续维护和增加脚本的可读性. 需求实现: 我配置文件路径及

随机推荐