多页应用的webpack4配置优化

20 篇文章 0 订阅
7 篇文章 0 订阅

 

webpack这款工具虽然很难学,但是自由度很大,玩转之后有种随心所欲的感觉。

在学习webpack之前,有几个基础的概念:

  • JavaScript,
  • nodejs,
  • CommonJS,
  • 如果以上几个技能都具备,那么就可以开始学习webpack了。

webpack的打包原理,就是将各个模块变成字符串,存入健值或者数组之中,然后每个模块之间的关系,通过__webpack_require__这个方法来实现。最后通过eval这个函数将字符串变成可执行代码。

 

webpack从4开始,webpack分成了两个包一个webpack一个webpack-cli,所以安装的时候要安装两个包,以及这个包我们是工具,非网站所依赖的包,所以记得放在开发依赖包之中。

npm install webpack webpack-cli -save-dev

如果没有安装CLI,系统就会告诉你,cli是必不可少的。

One CLI for webpack must be installed.

webpack4开始支持零配置,如果不写webpack.config.js运行试试,出现了一个警告 

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

这个警告就是告诉我们,webpack4中的mode参数默认是production,所以如果是development的情况就一定要配置了。感觉是零配置,但是实际上还是需要手动配置的,因为这个零配置只是帮我们做掉了一些简单的事,比如线上就压缩JS,开发版就不压缩JS,还有一些默认的路径之类的。实际上开发的时候,默认的路径肯定是不够用的。

打包优化

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

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

  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...

不过在 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 等等,需要自己再定制啦。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值