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

前言

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

webpack4 相比以前的 2 与 3,改变很大。最主要的一点是不少配置已经内置,使得 webpack 能“开箱即用”。固然这个开箱即用不可能知足全部状况,可是不少以往的配置,其实能够不用了。好比在以前,压缩混淆代码,须要增长uglify插件,做用域提高(scope hosting)须要增长ModuleConcatenationPlugin。而在 webpack4 中,只须要设置 modeproduction便可。固然,若是再强行增长这些插件也不会报错。css

因此我建议,若是你们想迁移到 webpack4,仍是从 0 开始作加法,参考历史,从新作一个配置。而不是从历史的配置里删删减减,再升级为 webpack4。这样 webpack4 的配置会显得更精简。html

打包优化

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

首先,构建多页应用,每每会抽离以下几个 chunk 包:vue

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

而后咱们会给打出的 chunk 包名,注入 contentHash,以实现最大缓存效果。在咱们分 chunk 的过程当中,最关键的一个思想就是,每次迭代发布,尽可能减小 chunk hash 值的改变。这个在业界也有不少很是多的实践,好比这篇文章:https://github.com/pigcan/blo...java

不过在 webpack4 中,咱们不用再增长这么多插件啦,一个 optimization 配置彻底就能搞定。node

我先贴上个人 webpack 的 optimization 配置,而后我再对其作一些介绍,加深你们印象react

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 便可。webpack

splitChunks

这个配置能让咱们以必定规则抽离想要的包,咱们可能会抽好几个包,如 verdor + common,因此 splitChunks 中提供 cacheGroups 字段,cacheGroups 每增长一个 key,就至关于多一个抽包规则。git

在网上不少教程中,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的配置项中设置 moduleIdshashed 便可。

namedChunks

除了 moduleId,咱们知道分离出的 chunk 也有其 chunkId。一样的,chunkId 也有因其 chunkId 发生变化而致使缓存失效的问题。因为manifest与打出的 chunk 包中有chunkId相关数据,因此一旦如“增删页面”这样的操做致使 chunkId 发生变化,可能会影响不少的 chunk 缓存失效。

在 webpack4 之前,经过增长NamedChunksPlugin,使用 chunkName 来替换 chunkId,实现固化 chunkId,保持缓存的能力。在 webpack4 中,只需在optimization的配置项中设置 namedChunkstrue 便可。

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

相关文章
相关标签/搜索