《webpack》从配置到优化

1,420 阅读13分钟

webpack使用及配置package.json运行脚本

webpack安装

  1. 全局安装 yarn add webpack webpack-cli -G
  2. 局部安装(推荐)
    yarn add webpack webpack-cli -D

配置package.json运行脚本

由于是局部安装包所以在项目package.json中的scripts配置运行脚本可以方便我们开发与调试。

如:webpack零配置打包命令

"scripts": {
  "build": "webpack"
}

webpack4.0之后可以实现0配置打包构建,0配置的特点就是限制较多,无法自定义很多配置

webpack配置

webpack有四大核心概念

  • 入口(entry):程序的入口js
  • 输出(output):打包后存放的位置
  • loader:用于对模块的源代码进行转换
  • 插件(plugins):插件目的在于解决loader无法实现的其他事

基础配置

  1. 在当前项目根目录下新建webpack.config.js
// 因为webpack是在node中启动的,所以遵循Common Js规范进行配置
module.exports = {
/*
* 打包入口
* */
entry: {
  main: './src/main.js'
},
/*
* 输出配置
* path: 输出地址,必须是个绝对路径
* filename: 输出文件名,可以使用placeholder语法进行根据打包的文件名自动配置输出文件名
* */
output: {
  path: './src/index.js',
  filename: '[name].js',
},
/*
* 打包模式配置
* production: 默认值-生产环境打包,会压缩代码。
* development: 开发环境打包,不会进行压缩代码
* */
mode: 'development',
}
  1. package.json中的scripts配置运行脚本
"scripts": {
  "build": "webpack --config ./build/webpack.config.js"
}
  1. 运行yarn run build

开发配置

自动编译工具

每次编译完代码,都需要手动运行yarn run build就会变得很繁琐。
webpack中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码

  1. webpack's Watch Mode
  2. webpack-dev-server(推荐)
  3. webpack-dev-middleware

watch

webpack指令后面加上--webpack参数就可以开启 主要作用就是监视本地项目文件的变化,发现有修改的代码会自动编译打包,生成dist目录

两种使用方法:

  1. 配置package.json中的scripts
"scripts": {
  "watch": "webpack --watch"
}

运行yarn run watch 2. 配置webpack.config.js(推荐)

...
module.exports = {
  entry: ...,
  output: ...,
  mode: ...,
  * watch监听
  * 默认为false
  * 开启后会自动监听代码发生变化后自动编译打包 */
  watch: true,
}

运行yarn run build

webpack-dev-server(推荐)

devServer会在每一次打包后在内存中生成一个打包好后的main.js,专供开发时使用,打包效率搞,修改代码后会自动重新打包以及刷新浏览器,开发体验极佳。

  1. 安装devServer yarn add webpack-dev-server -D
  2. index.html中需要手动添加devServer打包好后的main.js文件 <script src="/main.js"></script> (下面会介绍自动添加插件用来解决此问题)

两种使用方法

  1. 配置package.json中的scripts
"scripts": {
  "dev": "webpack-dev-server --hot --open --port 8090
}

运行yarn run dev 2. 配置webpack.config.js(推荐)

...
module.exports = {
  entry: ...,
  output: ...,
  mode: ...,
  watch: ...,
    /*
  * 自动打包编译
  * port: 端口号
  * open: 第一次运行是否自动打开页面
  * hot: 是否开启热更新
  * */
  devServer: {
    port: 8090,
    open: true,
    hot: true
  },
}

修改package.json的scripts: "dev": "webpack-dev-server"

运行yarn run build

webpack-dev-middleware(了解)

webpack-dev-middleware是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。 webpack-dev-server 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。

  1. 安装expresswebpack-dev-middleware yarn add express webpack-dev-middleware -D
  2. src根目录下新建server.js文件
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');

const app = express();
const compiler = webpack(config);

app.use(webpackDevMiddleware(compiler, {
publicPath: '/'
}));

app.listen(3000, function () {
console.log('http://localhost:3000');
});
  1. 配置package.json中的scripts:"server": "node server.js"
  2. 运行: yarn run server

注意: 如果要使用webpack-dev-middleware, 必须使用html-webpack-plugin插件, 否则html文件无法正确的输出到express服务器的根目录

html插件

在使用devServer时我们还需要手动往index.html文件中引入打包后内存中的main.js,打包完成后,不会生成index.htmlhtml-webpack-plugin就可以帮助我们解决这些问题。

  1. 安装html-webpack-plugin yarn add html-webpack-plugin -D
  2. webpack.config.js中的plugins节点下配置
...
// 引入HtmlWebpackPlugin
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
entry: ...,
output: ...,
mode: ...,
watch: ...,
devServer: ...,
plugins: [
  /* 
  * Html插件
  * filename: 输出文件的文件名称,默认为index.html
  * template: 本地模板文件的位置
  *  */
  new HtmlWebpackPlugin({
    filename: 'index.html',
    template: './src/index.html',
  })
]
}
  1. 运行yarn run dev

clean-webpack-plugin

该插件在yarn run build时自动清除dist目录后重新生成,非常方便。

  1. 安装yarn add css-loader style-loader -D
  2. 配置webpack.config.js
...
// 引入清除插件
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
entry: ...,
output: ...,
mode: ...,
watch: ...,
devServer: ...,
plugins: [
  new HtmlWebpackPlugin(...),
  new CleanWebpackPlugin()
]
}

sourceMap控制台调试代码工具

SourceMap是一种映射关系。当项目运行后,如果出现错误,错误信息只能定位到打包后文件中错误的位置。如果想查看在源文件中错误的位置,则需要使用映射关系,找到对应的位置。 使用:

...
module.exports = {
  entry: ...,
  output: ...,
  mode: ...,
  watch: ...,
  devServer: ...,
  plugins: [...],
  /* 
  * JavaScript的SourceMap功能--可以在控制台看到源码对应行
  * none: 推荐在生产环境下使用,这样打包速度会更快
  * eval-cheap-module-source-map: 开发环境使用,可以看到对应代码的映射关系,方便调试
  *  */
  devtool: 'eval-cheap-module-source-map',
}

loader配置

处理css

  1. 安装yarn add clean-webpack-plugin -D
  2. 配置webpack.config.js
...
const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
entry: ...,
output: ...,
mode: ...,
watch: ...,
devServer: ...,
plugins: [...],
devtool: '...',
module: {
rules: [
  // css
  {
    /*
    * test: 匹配以.css结尾的文件进行打包
    * use: 使用的解析loader
    * loader的执行顺序时从右到左以管道的方式链式调用     
    * css-loader:解析css文件
    * style-loader:将解析出来的结果,放到html中,使其生效
    * */
    // 用正则匹配当前访问的文件的后缀名是  .css
    test: /\.css$/,
    use: ['style-loader', 'css-loader'] // webpack底层调用这些包的顺序是从右到左
  }
]
}
}

处理less与sass

  1. 安装yarn add less less-loader sass-loader node-sass -D
  2. 配置webpack.config.js
...
module.exports = {
entry: ...,
output: ...,
mode: ...,
watch: ...,
devServer: ...,
plugins: [...],
devtool: '...',
module: {
rules: [
  // css
  {...},
  // less
  {
    test: /\.less$/,
    use: ['style-loader', 'css-loader', 'less-loader']
  },
  // scss
  {
    test: /\.s[ac]ss$/,
    use: ['css-loader', 'postcss-loader' , 'sass-loader'],
  },
]
}
}

处理图片和字体

  1. 安装yarn add file-loader url-loader -D
  2. 配置webpack.config.js
...
module.exports = {
entry: ...,
output: ...,
mode: ...,
watch: ...,
devServer: ...,
plugins: [...],
devtool: '...',
module: {
rules: [
  // css
  {...},
  // less
  {...},
  // scss
  {...},
  // img
  {
    test: /\.(png|jpg|gif)$/,
    use: {
      loader: 'url-loader',
      options: {
        limit: 5 * 1024,
        outputPath: 'images',
        name: '[name]-[hash:4].[ext]',
      }
    }
  },
  // icon
  {
    test: /\.(woff|woff2|eot|svg|ttf)$/,
    use: 'url-loader'
  },
]
}
}

Babel

  1. 安装yarn add babel-loader @babel/core @babel/preset-env webpack -D
  2. 如果需要支持更高级的ES6语法, 可以继续安装插件: yarn add @babel/plugin-proposal-class-properties -D
  3. 配置webpack.config.js
...
module.exports = {
entry: ...,
output: ...,
mode: ...,
watch: ...,
devServer: ...,
plugins: [...],
devtool: '...',
module: {
rules: [
  // css
  {...},
  // less
  {...},
  // scss
  {...},
  // img
  {...},
  // icon
  {...},
  // js-babel
  {
    test: /\.js$/,
    use: {
      loader: 'babel-loader',
      options: {
        presets: ['@babel/env'],
        plugins: ['@babel/plugin-proposal-class-properties']
      }
    },
    exclude: /node_modules/
  }
]
}
}

但是官方更建议的做法是在项目根目录下新建一个.babelrc的babel配置文件

{
"presets": ["@babel/env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

plugins配置

copy-webpack-plugin

默认webpack打包不会把静态资源复制到dist文件夹下,此插件可以在webpack打包完成后会把静态资源复制到dist文件夹下。

  1. 配置webpack.config.js
...
// 引入插件
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
entry: ...,
output: ...,
mode: ...,
watch: ...,
devServer: ...,
plugins: [
  new HtmlWebpackPlugin(...),
  new CleanWebpackPlugin(),
  /*
  * 打包静态资源插件
  */
  new CopyWebpackPlugin([
    {
      from: path.join(__dirname, 'assets'),
      to: 'assets'
    }
  ])
]
}

BannerPlugin

这是一个webpack的内置插件,用于给打包的JS文件加上版权注释信息

  1. 安装yarn add copy-webpack-plugin -D
  2. 配置webpack.config.js
...
// 引入插件
const webpack = require('webpack')

module.exports = {
entry: ...,
output: ...,
mode: ...,
watch: ...,
devServer: ...,
plugins: [
  new HtmlWebpackPlugin(...),
  new CleanWebpackPlugin(),
  /*
  * 打包静态资源插件
  */
  new CopyWebpackPlugin([
    {
      from: path.join(__dirname, 'assets'),
      to: 'assets'
    }
  ]),
  new webpack.BannerPlugin('需要注释的信息')
]
}

webpack高级配置

HTML中img标签的图片资源处理

  1. 安装yarn add html-withimg-loader -S
  2. 配置webpack.config.js
...
module.exports = {
entry: ...,
output: ...,
mode: ...,
watch: ...,
devServer: ...,
plugins: [...],
devtool: '...',
module: {
rules: [
  // css
  {...},
  // less
  {...},
  // scss
  {...},
  // img
  {...},
  // icon
  {...},
  // js-babel
  {...},
  // 处理img标签中src引入的图片
  {
    test: /\.(htm|html)$/i,
    loader: 'html-withimg-loader'
  }
]
}
}

使用时,只需要在html中正常引用图片即可,webpack会找到对应的资源进行打包,并修改html中的引用路径

多页应用打包

  1. 在webpack.config.js中修改入口和出口配置
...
module.exports = {
// 1. 增加打包入口
entry: {
  main: './src/main.js',
  other: './src/other.js'
},
output: {...},
mode: 'development',
watch: false,
devServer: {...},
plugins: [
  new HtmlWebpackPlugin({
    filename: 'index.html',
    template: './src/index.html',
    chunks: ['main']
  }),
  2. 增加html文件
  new HtmlWebpackPlugin({
    filename: 'index.html',
    template: './src/index.html',
    // chunks: ['other', 'main']
    chunks: ['other']
  }),
  new CleanWebpackPlugin(),
  new CopyWebpackPlugin(...)
],
devtool: '...',
module: {...}
}

第三方库的两种引入方式

可以通过expose-loader进行全局变量的注入,同时也可以使用内置插件webpack.ProvidePlugin对每个模块的闭包空间,注入一个变量,自动加载模块,而不必到处 importrequire

  • expose-loader 将库引入到全局作用域
  1. 安装yarn add expose-loader -D
  2. 配置loader
...
module.exports = {
entry: {...},
output: {...},
mode: 'development',
watch: false,
devServer: {...},
plugins: [...],
devtool: 'eval-cheap-module-source-map',
module: {
  rules: [
    // css
    {...},
    // less
    {...},
    // scss
    {...},
    // img
    {...},
    // icon
    {...},
    // js-babel
    {...},
    // 处理img标签中src引入的图片
    {...},
    // 全局引入第三方库
    {
      test: require.resolve('jquery'),
      use: {
        loader: 'expose-loader',
        options: '$'
      }
    }
  ]
}
}

tips: require.resolve 用来获取模块的绝对路径。所以这里的loader只会作用于 jquery 模块。并且只在 main 中使用到它时,才进行处理。

  • webpack.ProvidePlugin 将库自动加载到每个模块
  1. 配置plugins
...
module.exports = {
entry: {...},
output: {...},
mode: 'development',
watch: false,
devServer: {...},
plugins: [
   ...
   new webpack.ProvidePlugin({
   $: 'jquery',
   jQuery: 'jquery'
   })
],
devtool: 'eval-cheap-module-source-map',
module: {...}
}

Development / Production不同配置文件打包

项目开发时一般需要使用两套配置文件,用于开发阶段打包(不压缩代码,不优化代码,增加效率)和上线阶段打包(压缩代码,优化代码,打包后直接上线使用) 抽取三个配置文件:

  • webpack.base.js
  • webpack.prod.js
  • webpack.dev.js

步骤如下:

  1. 将开发环境和生产环境公用的配置放入base中,不同的配置各自放入prod或dev文件中(例如:mode)
  2. 然后在dev和prod中使用webpack-merge把自己的配置与base的配置进行合并后导出 yarn add -D webpack-merge
  3. package.json中的脚本参数进行修改,通过--config手动指定特定的配置文件

定义环境变量

除了区分不同的配置文件进行打包,还需要在开发时知道当前的环境是开发阶段或上线阶段,所以可以借助内置插件DefinePlugin来定义环境变量。最终可以实现开发阶段与上线阶段的api地址自动切换。

  1. 配置plugins
...
module.exports = {
entry: {...},
output: {...},
mode: 'development',
watch: false,
devServer: {...},
plugins: [
   ...
  new webpack.DefinePlugin({
  // 设置全局变量属性
  IS_DEV: false
  })
],
devtool: 'eval-cheap-module-source-map',
module: {...}
}

Proxy--使用devServer解决跨域问题

在开发阶段很多时候需要使用到跨域,何为跨域?请看下图:

跨域.png 开发阶段往往会遇到上面这种情况,也许将来上线后,前端项目会和后端项目部署在同一个服务器下,并不会有跨域问题,但是由于开发时会用到webpack-dev-server,所以一定会产生跨域的问题

目前解决跨域主要的方案有:

  1. jsonp(淘汰)
  2. cors
  3. http proxy 此处介绍的使用devServer解决跨域,其实原理就是http proxy

将所有ajax请求发送给devServer服务器,再由devServer服务器做一次转发,发送给数据接口服务器

由于ajax请求是发送给devServer服务器的,所以不存在跨域,而devServer由于是用node平台发送的http请求,自然也不涉及到跨域问题,可以完美解决!

解决跨域.png 前端需要配置devServer的proxy功能,在webpack.dev.js中进行配置:

  devServer: {
    open:  false,
    hot: true,
    compress: true,
    port: 3000,
    proxy: {
      /*
      * 代理(开发时跨域)
      * 当前端请求/api地址时,会将请求转发到http://localhost:9999/api
      * */
      // '/api': 'http://localhost:9999'
      '/api': {
        target: 'http://localhost:9999',
        // 转发请求时不会携带 /api
        // http://localhost:9999/getUserInfo
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  },

webpack优化

production模式打包自带优化

  • tee shaking

tree shaking 是一个术语,通常用于打包时移除 JavaScript 中的未引用的代码(dead-code),它依赖于 ES6 模块系统中 importexport的静态结构特性。
开发时引入一个模块后,如果只使用其中一个功能,上线打包时只会把用到的功能打包进main,其他没用到的功能都不会打包进来,可以实现最基础的优化

  • scope hoisting

scope hoisting的作用是将模块之间的关系进行结果推测, 可以让 Webpack 打包出来的代码文件更小、运行的更快 scope hoisting 的实现原理其实很简单:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。 因此只有那些被引用了一次的模块才能被合并。
由于 scope hoisting 需要分析出模块之间的依赖关系,因此源码必须采用 ES6 模块化语句,不然它将无法生效。 原因和tree shaking一样。

  • 代码压缩

所有代码使用UglifyJsPlugin插件进行压缩、混淆

css优化

将css提取到独立的文件中

mini-css-extract-plugin是用于将CSS提取为独立的文件的插件,对每个包含css的js文件都会创建一个CSS文件,支持按需加载csssourceMap

有如下优势:

  • 异步加载
  • 不重复编译,性能很好
  • 容易使用
  • 只针对CSS

使用方法:

  1. 安装yarn add mini-css-extract-plugin -D

  2. 在webpack配置文件中引入插件 const MiniCssExtractPlugin = require('mini-css-extract-plugin')

  3. 创建插件对象,配置抽离的css文件名,支持placeholder语法

new MiniCssExtractPlugin({
  filename: '[name].css'
})
  1. 将原来配置的所有style-loader替换为MiniCssExtractPlugin.loader`
{
  test: /\.css$/,
    use: [MiniCssExtractPlugin.loader, 'css-loader']
},
// less
{
  test: /\.less$/,
    use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
},
// scss
{
  test: /\.s[ac]ss$/,
    use: [MiniClassExtractPlugin.loader, 'css-loader', 'postcss-loader' , 'sass-loader'],
},

自动添加css前缀

使用postcss,需要用到postcss-loaderautoprefixer插件

  1. 安装yarn add postcss-loader autoprefixer -D

  2. 修改webpack配置文件中的loader,将postcss-loader放置在css-loader的右边(调用链从右到左)

// css
{
  test: /\.css$/,
    use: ['MiniCssExtractPlugin.loader', 'css-loader', `postcss-loader`]
},
// less
{
  test: /\.less$/,
    use: ['MiniCssExtractPlugin.loader', 'css-loader', `postcss-loader`, 'less-loader']
},
// scss
{
  test: /\.s[ac]ss$/,
    use: [MiniClassExtractPlugin.loader, 'css-loader', `postcss-loader`, 'postcss-loader' , 'sass-loader'],
},
  1. 项目根目录下添加postcss的配置文件:postcss.config.js

  2. postcss的配置文件中使用插件

module.exports = {
  plugins: [require('autoprefixer')]
}

开启css压缩

需要使用optimize-css-assets-webpack-plugin插件来完成css压缩

但是由于配置css压缩时会覆盖掉webpack默认的优化配置,导致JS代码无法压缩,所以还需要手动把JS代码压缩插件导入进来:terser-webpack-plugin

  1. 安装 yarn add optimize-css-assets-webpack-plugin terser-webpack-plugin -D

  2. 导入插件

const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
  1. 在webpack配置文件中添加配置节点
optimization: {
  minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},

Js优化

Js代码分离

Code Splittingwebpack打包时用到的重要的优化特性之一,此特性能够把代码分离到不同的main中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 main,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

有三种常用的代码分离方法:

  • 入口起点(entry points):使用entry配置手动地分离代码。
  • 防止重复(prevent duplication):使用 SplitChunksPlugin去重和分离 chunk
  • 动态导入(dynamic imports):通过模块的内联函数调用来分离代码。

手动配置多入口

手动配置多入口方式如果入口chunks之间包含重复的模块,那些重复模块都会被引入到各个main
所以这种方法不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码。

抽取公共代码

  1. 修改webpack配置文件(后面会详细解说此配置)
module.exports = {
  entry: {...},
  output: {...},
  mode: 'development',
  watch: false,
  devServer: {...},
  plugins: [...],
  devtool: 'eval-cheap-module-source-map',
  module: {...},
  // 抽取公共代码
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
}

动态导入 (懒加载)

webpack默认是允许import语法动态导入的,但是需要babel的插件支持,最新版babel的插件包为:@babel/plugin-syntax-dynamic-import,以前老版本不是@babel开头,已经无法使用,需要注意

动态导入最大的好处是实现了懒加载,用到哪个模块才会加载哪个模块,可以提高SPA应用程序的首屏加载速度,VueReactAngular框架的路由懒加载原理一样

  1. 安装babel插件
    yarn add @babel/plugin-syntax-dynamic-import -D

  2. 修改.babelrc配置文件,添加@babel/plugin-syntax-dynamic-import插件

{
  "presets": ["@babel/env"],
  "plugins": [
  "@babel/plugin-proposal-class-properties",
  "@babel/plugin-transform-runtime",
  "@babel/plugin-syntax-dynamic-import"
]
}
  1. 将jQuery模块进行动态导入
function getComponent() {
  return import('jquery').then(({ default: $ }) => {
    return $('<div></div>').html('main')
  })
}
  1. 给某个按钮添加点击事件,点击后调用getComponent函数创建元素并添加到页面
window.onload = function () {
  document.getElementById('btn').onclick = function () {
    getComponent().then(item => {
      item.appendTo('body')
    })
  }
}

动态导入方法会在点击按钮之后才会去加载jQuery,对于首屏加载速度提升巨大。

SplitChunksPlugin配置参数

SplitChunksPlugin的配置,只需要在webpack配置文件中的optimization节点下的splitChunks进行修改即可,如果没有任何修改,则会使用默认配置

默认的SplitChunksPlugin 配置适用于绝大多数用户

webpack 会基于如下默认原则自动分割代码:

  • 公用代码块或来自 node_modules 文件夹的组件模块。
  • 打包的代码块大小超过 30k(最小化压缩之前)。
  • 按需加载代码块时,同时发送的请求最大数量不应该超过 5。
  • 页面初始化时,同时发送的请求最大数量不应该超过 3。

以下是SplitChunksPlugin的默认配置:

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 只对异步加载的模块进行拆分,可选值还有all | initial
      minSize: 30000, // 模块最少大于30KB才拆分
      maxSize: 0,  // 模块大小无上限,只要大于30KB都拆分
      minChunks: 1, // 模块最少引用一次才会被拆分
      maxAsyncRequests: 5, // 异步加载时同时发送的请求数量最大不能超过5,超过5的部分不拆分
      maxInitialRequests: 3, // 页面初始化时同时发送的请求数量最大不能超过3,超过3的部分不拆分
      automaticNameDelimiter: '~', // 默认的连接符
      name: true, // 拆分的chunk名,设为true表示根据模块名和CacheGroup的key来自动生成,使用上面连接符连接
      cacheGroups: { // 缓存组配置,上面配置读取完成后进行拆分,如果需要把多个模块拆分到一个文件,就需要缓存,所以命名为缓存组
        vendors: { // 自定义缓存组名
          test: /[\\/]node_modules[\\/]/, // 检查node_modules目录,只要模块在该目录下就使用上面配置拆分到这个组
          priority: -10 // 权重-10,决定了哪个组优先匹配,例如node_modules下有个模块要拆分,同时满足vendors和default组,此时就会分到vendors组,因为-10 > -20
        },
        default: { // 默认缓存组名
          minChunks: 2, // 最少引用两次才会被拆分
          priority: -20, // 权重-20
          reuseExistingChunk: true // 如果主入口中引入了两个模块,其中一个正好也引用了后一个,就会直接复用,无需引用两次
        }
      }
    }
  }
};

noParse

在引入一些第三方模块时,例如jQuerybootstrap等,我们知道其内部肯定不会依赖其他模块,因为最终我们用到的只是一个单独的js文件或css文件

所以此时如果webpack再去解析他们的内部依赖关系,其实是非常浪费时间的,我们需要阻止webpack浪费精力去解析这些明知道没有依赖的库

可以在webpack配置文件的module节点下加上noParse,并配置正则来确定不需要解析依赖关系的模块

module: {
...
  noParse: /jquery|bootstrap/
}

IgnorePlugin

在引入一些第三方模块时,例如moment,内部会做i18n国际化处理,所以会包含很多语言包,而语言包打包时会比较占用空间,如果我们项目只需要用到中文,或者少数语言,可以忽略掉所有的语言包,然后按需引入语言包

从而使得构建效率更高,打包生成的文件更小

需要忽略第三方模块内部依赖的其他模块,只需要三步:

  1. 首先要找到moment依赖的语言包是什么
  2. 使用IgnorePlugin插件忽略其依赖
  3. 需要使用某些依赖时自行手动引入

具体实现如下:

  1. 通过查看moment的源码来分析:
function loadLocale(name) {
  var oldLocale = null;
  // TODO: Find a better way to register and load all the locales in Node
  if (!locales[name] && (typeof module !== 'undefined') &&
    module && module.exports) {
    try {
      oldLocale = globalLocale._abbr;
      var aliasedRequire = require;
      aliasedRequire('./locale/' + name);
      getSetGlobalLocale(oldLocale);
    } catch (e) {}
  }
  return locales[name];
}

观察上方代码,同时查看moment目录下确实有locale目录,其中放着所有国家的语言包,可以分析得出:locale目录就是moment所依赖的语言包目录

  1. 使用IgnorePlugin插件来忽略掉moment模块的locale目录

在webpack配置文件中安装插件,并传入配置项

参数1:表示要忽略的资源路径

参数2:要忽略的资源上下文(所在哪个目录)

两个参数都是正则对象

new webpack.IgnorePlugin(/\.\/locale/, /moment/)
  1. 使用moment时需要手动引入语言包,否则默认使用英文
import moment from 'moment'
import 'moment/locale/zh-cn'
moment.locale('zh-CN')
console.log(moment().subtract(6, 'days').calendar())

DllPlugin

在引入一些第三方模块时,例如Vue,React,angular等框架,这些框架的文件一般都是不会修改的,而每次打包都需要去解析它们,也会影响打包速度,哪怕做拆分,也只是提高了上线后用户访问速度,并不会提高构建速度,所以如果需要提高构建速度,应该使用动态链接库的方式,类似于Windows中的dll文件。

借助DllPlugin插件实现将这些框架作为一个个的动态链接库,只构建一次,以后每次构建都只生成自己的业务代码,可以大大提高构建效率!

主要思想在于,将一些不做修改的依赖文件,提前打包,这样我们开发代码发布的时候就不需要再对这部分代码进行打包,从而节省了打包时间。

涉及两个插件:

  1. DllPlugin

使用一个单独webpack配置创建一个dll文件。并且它还创建一个manifest.json。DllReferencePlugin使用该json文件来做映射依赖性。(这个文件会告诉我们的哪些文件已经提取打包好了)

配置参数:

  • context (可选): manifest文件中请求的上下文,默认为该webpack文件上下文。
  • name: 公开的dll函数的名称,和output.library保持一致即可。
  • path: manifest.json生成的文件夹及名字
  1. DllReferencePlugin

这个插件用于主webpack配置,它引用的dll需要预先构建的依赖关系。

  • context: manifest文件中请求的上下文。

  • manifest: DllPlugin插件生成的manifest.json

  • content(可选): 请求的映射模块id(默认为manifest.content)

  • name(可选): dll暴露的名称

  • scope(可选): 前缀用于访问dll的内容

  • sourceType(可选): dll是如何暴露(libraryTarget)

将Vue项目中的库抽取成Dll

  1. 准备一份将Vue打包成DLL的webpack配置文件
    在build目录下新建一个文件:webpack.vue.js
    配置入口:将多个要做成dll的库全放进来
    配置出口:一定要设置library属性,将打包好的结果暴露在全局
    配置plugin:设置打包后dll文件名和manifest文件所在地
const path = require('path')
const webpack = require('webpack')
module.exports = {
  mode: 'development',
  entry: {
    vue: [
      'vue/dist/vue.js',
      'vue-router'
    ]
  },
  output: {
    filename: '[name]_dll.js',
    path: path.resolve(__dirname, '../dist'),
    library: '[name]_dll'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_dll',
      path: path.resolve(__dirname, '../dist/manifest.json')
    })
  ]
}
  1. 在webpack.config.js中进行插件的配置
    使用DLLReferencePlugin指定manifest文件的位置即可
new webpack.DllReferencePlugin({
  manifest: path.resolve(__dirname, '../dist/manifest.json')
})
  1. 安装add-asset-html-webpack-plugin
  2. 配置插件自动添加script标签到HTML中
new AddAssetHtmlWebpackPlugin({
  filepath: path.resolve(__dirname, '../dist/vue_dll.js')
})

浏览器缓存

在做了众多代码分离的优化后,其目的是为了利用浏览器缓存,达到提高访问速度的效果,所以构建项目时做代码分割是必须的,例如将固定的第三方模块抽离,下次修改了业务代码,重新发布上线不重启服务器,用户再次访问服务器就不需要再次加载第三方模块了

但此时会遇到一个新的问题,如果再次打包上线不重启服务器,客户端会把以前的业务代码和第三方模块同时缓存,再次访问时依旧会访问缓存中的业务代码,所以会导致业务代码也无法更新

需要在output节点的filename中使用placeholder语法,根据代码内容生成文件名的hash

output: {
  // path.resolve() : 解析当前相对路径的绝对路径
  // path: path.resolve('./dist/'),
  // path: path.resolve(__dirname, './dist/'),
  path: path.join(__dirname, '..', './dist/'),
    // filename: 'main.js',
    filename: '[name].[contenthash:8].main.js',
    publicPath: '/'
},

之后每次打包业务代码时,如果有改变,会生成新的hash作为文件名,浏览器就不会使用缓存了,而第三方模块不会重新打包生成新的名字,则会继续使用缓存