构建速度——记 Vue 项目中 Webpack 使用 DllPlugin

4,192 阅读6分钟

前言

使用 webpack 插件 DllPlugin 和 DllReferencePlugin ,是前端工程化中优化打包速度的重要途径。webpack中文网-DllPlugin

他们可以打包常用的且不经常更新的模块,生成 JS 和 json文件,一般放入 public 目录中;项目打包时不会再对这些依赖进行编译,而是通过在 html 中插入 script 标签来读取依赖。比如 vue,antd,echarts 等常用框架和资源库。这在项目依赖包达到一定规模时尤为明显,在速度的提升上是显著的。

网上对于插件的使用介绍繁多,但少有对于 vue-cli 构建的项目对于此插件使用做出详细的说明,以及插件的坑和注意事项。我也是在工作中配置此插件时或多或少的问题,我想在以后的将来应该还会遇到诸类问题。

vue-cli 中 webpack

vue-cli 构建的项目,默认和最简单的是使用 vue.config.js 来配置 webpack,并且有自己独特的语法和规则。webpack相关|vue-cli

插件的最关键部分在于 configureWebpack 选项提供的对象中(或函数方法)。按照文档,你需要基于环境有条件地配置行为,或者想要直接修改配置,按照函数方法 (该函数会在环境变量被设置之后懒执行)来使用。该方法的第一个参数会收到已经解析好的配置。在函数内,你可以直接修改配置,或者返回一个将会被合并的对象

// vue.config.js
module.exports = {
  configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      // 为生产环境修改配置...
    } else {
      // 为开发环境修改配置...
    }
  }
}

DllPlugin 配置

一般专门使用一个新的配置文件用来生成依赖库文件。该插件一般会在项目中 public/vender 目录位置生成依赖文件。

配置

webpack.dll.config.js

// 定义常用对象
const path = require('path')
const webpack = require('webpack')
const CleanWebpackPlugin = require('clean-webpack-plugin')

// dll文件存放的目录
const dllPath = 'public/vendor'


module.exports = {
  // 需要提取的库文件
  entry: {
    vue: ["vue", "vue-router", "vuex", 'axios'],
    antd: ["ant-design-vue"],
    echarts: ["echarts"],
  },
  output: {
    path: path.join(__dirname, dllPath),
    filename: '[name].dll.js',
    // vendor.dll.js中暴露出的全局变量名
    // 保持与 webpack.DllPlugin 中名称一致
    library: '[name]_[hash]'
  },
  plugins: [
    // 清除之前的dll文件
    new CleanWebpackPlugin(['*.*'], {
      root: path.join(__dirname, dllPath)
    }),
    // 定义插件
    new webpack.DllPlugin({
      path: path.join(__dirname, dllPath, '[name]-manifest.json'),
      // 保持与 output.library 中名称一致
      name: '[name]_[hash]',
      context: process.cwd()
    })
  ]
}

主要说明

  • 定义常用对象

clean-webpack-plugin 主要用于每次生成动态链接库时首先清空 vendor 目录。

  • dll 文件存放目录

一般定义为 public/vendor。

注意:一般将动态链接库放到项目的 public 目录下,而不要放在 dist 或其他目录中。

  • entry 入口

定义提取哪些库/依赖。

在该对象中,键名定义生成生成文件的前缀;键值为数组类型,定义依赖名。例如在上述代码中,将 “vue 全家桶” 定义为 vue;那样的定义方法,会让插件将这些依赖生成为三个文件。同时会对应生成三个个名为 manifest.json 的文件来描述动态链接库包含了哪些内容,这个文件是用于让 DllReferencePlugin 能够映射到相应的依赖上。

  • output 出口

path 选项使用 NodeJs 的 path 构建路径(绝对路径)。

filename 选项使用暴露出的 dll 的函数名作为文件前缀。

library 此插件与该选项相结合可以暴露出(也称为放入全局作用域)dll 函数,要与 Dllplugin 中的 name 一致,这里使用 hash 值以解决缓存和可能的重复。

  • plugins

定义插件。

path manifest.json 文件的 绝对路径(输出文件)。

name 暴露出的 DLL 的函数名(TemplatePaths:[fullhash] & [name] )。

context (可选): manifest 文件中请求的 context (默认值为 webpack 的 context),这里使用 process.cwd() 返回当前工作目录。

DllReferencePlugin 配置

此插件配置在 webpack 的主配置文件中,此插件会根据描述文件引用依赖到需要的预编译的依赖中,避免在公共区域重复编译依赖。

配置

vue.config.js

module.exports = {

  configureWebpack: config => {
    plugins: [
        ...
        // 避免在公共区域重复编译依赖
    	new webpack.DllReferencePlugin({
            context: process.cwd(),
            manifest: require(`./public/vendor/vue-manifest.json`)
        })
        new webpack.DllReferencePlugin({
            context: process.cwd(),
            manifest: require(`./public/vendor/antd-manifest.json`)
        })
        new webpack.DllReferencePlugin({
            context: process.cwd(),
            manifest: require(`./public/vendor/echarts-manifest.json`)
        })
    ]
  }
  
}

index.html 配置

在 index.html 中引入资源库js 文件。

index.html

<script src="/vendor/vue.dll.js"></script>
<script src="/vendor/antd.dll.js"></script>
<script src="/vendor/echarts.dll.js"></script>

注意:为避免在 Vue 路由的非根路径刷新页面导致报错,请使用绝对路径。

主要说明

  • context (绝对路径) manifest (或者是内容属性)中请求的上下文,这里使用 process.cwd() 返回当前工作目录。

  • manifest 包含 content 和 name 的对象,或者是一个字符串 —— 编译时用于加载 JSON manifest 的路径。

配置脚本命令使用

完成插件的配置。

在打包前运行脚本命令来生成动态资源库,通常情况下在很长一段时间内你只需运行一次,因为这些依赖并不会经常变动,且生成的资源库一般会上传到仓库。

配置

package.json

{
  ...
  "scripts": {
      "dll": "webpack --progress ./webpack.dll.config.js"
  }
}

运行命令

$ npm run dll

技巧

使用 add-asset-html-webpack-plugin 自动引入资源库

当你的依赖足够多,生成的资源库文件众多时;或你对依赖进行增删时,或重新规划你的资源库名字时,一条一条的在 html 中引入显然不是明智之举。

使用 add-asset-html-webpack-plugin 插件可以帮你解决这个问题。

注意

援引该插件的官方文档说明,在迁移到 webpack 4+ 后,需要在 HtmlWebpackPlugin 之后应用该插件,目的是来注册一个钩子,而以前的 webpack 版本并不需要如此。

配置

vue.config.js

module.exports = {

  configureWebpack: config => {
    plugins: [
        ...
        new HtmlWebpackPlugin({
          title: 'My Project',
          template: 'public/index.html',
          favicon: 'public/logo.png'
        })
        new AddAssetHtmlPlugin({
          // dll文件位置
          filepath: path.resolve(__dirname, './public/vendor/*.js'),
          // dll 引用路径,请使用 绝对路径!!!
          publicPath: '/vendor',
          // dll最终输出的目录
          outputPath: './vendor'
        })
    ]
  }
  
}

publicPath script 标签生成的 src 路径。如果你使用了 Vue 路由,在任何非根路径刷新页面,如果使用相对路径,都会导致资源库 js 文件路径前面拼接路由导致报错;因此此处必须使用绝对路径,切记!

filepath dll 文件的位置,配置 *.js 可以使插件加载目录下的所有资源库 js 文件。

注意:使用 HtmlWebpackPlugin 插件后,会根据模板重构页面,因此需要使用插件支持的暴露变量,原本的 BASE_URL 变量将会失效。使用 htmlWebpackPlugin.options.title ,且移除引用 favicon 的 link 标签。

index.html

<head>
    <title><%= htmlWebpackPlugin.options.title %></title>
    <!--  <link rel="icon" href="<%= BASE_URL %>favicon.ico"> -->
</head>

优化配置

DllReferencePlugin 插件配置时代码存在重复,进行优化。 在 webpack.dll.config.js 文件中我们其实可以拿到所有的 name,即可以对其进行遍历生成。

vue.config.js

const DllConfig = require('./webpack.dll.config')

module.exports = {

    configureWebpack: config => {
    	let plugins = [
        	...
        ]
        // 避免在公共区域重复编译依赖
        Object.keys(DllConfig.entry).forEach(key=>{
          plugins.push(new webpack.DllReferencePlugin({
              context: process.cwd(),
              manifest: require(`./public/vendor/${key}-manifest.json`)
          }))
        })
        // 整合插件
    	config.plugins = [...config.plugins, ...plugins]
    }
    
}

注意问题

环境限制

不要在 开发环境 下使用 add-asset-html-webpack-plugin ,这会影响到该环境下的热加载(主要是因为它的前置插件 HtmlWebpackPlugin 热更新时更新所有页面)。具体原因大概是 webpack 的缓存导致:在触发热加载后,webpack 不会再重新读取配置文件,重新刷新页面后,无法在 html 中插入 script 标签导致页面直接报错。

script 标签的路径

在使用 Vue Router 时: AddAssetHtmlPlugin 插件配置中的 publicPath 务必使用绝对路径,或直接在页面引入的 script 标签 也要使用绝对路径,否则会导致在非根路径刷新时导致页面报错。

这点在上面已作出说明和示例。

整合配置

一份整合的参考配置。

vue.config.js

const path = require('path')
const webpack = require('webpack')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const DllConfig = require('./webpack.dll.config')

module.exports = {
  ...
  configureWebpack: config => {
  
    let plugins = [
      new HtmlWebpackPlugin({
        title: 'My Project',
        template: 'public/index.html',
        favicon: 'public/logo.png'
      })
    ]
    
    if (process.env.NODE_ENV === 'production') {
      // 将生成的 dll 文件注入到 生成的 html 模板中
      plugins.push(new AddAssetHtmlPlugin({
        // dll文件位置
        filepath: path.resolve(__dirname, './public/vendor/*.js'),
        // dll 引用路径
        publicPath: '/vendor',
        // dll最终输出的目录
        outputPath: './vendor'
      }))
      // 避免在公共区域重复编译依赖
      Object.keys(DllConfig.entry).forEach(key=>{
        plugins.push(new webpack.DllReferencePlugin({
            context: process.cwd(),
            manifest: require(`./public/vendor/${key}-manifest.json`)
        }))
      })
    }
    
    // 整合插件
    config.plugins = [...config.plugins, ...plugins]
  }
  
}

webpack.dll.config.js

// 定义常用对象
const path = require('path')
const webpack = require('webpack')
const CleanWebpackPlugin = require('clean-webpack-plugin')

// dll文件存放的目录
const dllPath = 'public/vendor'


module.exports = {
  // 需要提取的库文件
  entry: {
    vue: ["vue", "vue-router", "vuex", 'axios'],
    antd: ["ant-design-vue"],
    echarts: ["echarts"],
  },
  output: {
    path: path.join(__dirname, dllPath),
    filename: '[name].dll.js',
    // vendor.dll.js中暴露出的全局变量名
    // 保持与 webpack.DllPlugin 中名称一致
    library: '[name]_[hash]'
  },
  plugins: [
    // 清除之前的dll文件
    new CleanWebpackPlugin(['*.*'], {
      root: path.join(__dirname, dllPath)
    }),
    // 定义插件
    new webpack.DllPlugin({
      path: path.join(__dirname, dllPath, '[name]-manifest.json'),
      // 保持与 output.library 中名称一致
      name: '[name]_[hash]',
      context: process.cwd()
    })
  ]
}