作为一名合格的前端程序员你需要了解的webpack的知识点!

138 阅读11分钟

引言

webpack中文官网:webpack.docschina.org/concepts/

webpack是一个javascript常用的静态模块打包工具,通过配置webpack,可以实现包括压缩文件,解析不同文件格式等强大的功能,作者发现很多同学对webpack的认知都停留在入口出口以及简单的loader和plugin配置上,对webpack的核心原理都一知半解。本文期望通过更深层的解读,让读者能更彻底地理解这个打包工具的来龙去脉。

为什么要用webpack

在webpack等打包工具出世之前,我们普通的H5项目是怎么处理错综复杂的脚本呢? 第一种方式:引用不同的脚本去使用不同的功能,但脚本太多的时候会导致网络瓶颈 第二种方式:使用一个大型js文件去引入所有代码,但这样会严重影响可读性,可维护性,作用域。

举个栗子: 由于浏览器不能直接解析less文件,我们可通过引入转换的插件(file watcher)把less实时转换为css并引入,但项目里面会多出一个map跟css文件,造成项目文件的臃肿。

用官方文档的说法:

node.js诞生可以让Javasrcipt在浏览器环境之外使用,而webpack运行在node.js中。CommonJS的require机制允许在文件中引用某个模块,如此一来就可以解决作用域的问题。

const HtmlWebpackPlugin = require('html-webpack-plugin')

webpack 关心性能和加载时间;它始终在改进或添加新功能,例如:异步地加载 chunk 和预取,以便为你的项目和用户提供最佳体验

核心概念

webpack有7个核心概念:

  1. 入口(entry)
  2. 输出(output)
  3. loader
  4. 插件(plugin)
  5. 模式(mode)
  6. 浏览器兼容性(brower compatibility)
  7. 环境(environment)

新建一个build文件夹,里面新建一个webpack.config.js

入口entry

这是打包的入口文件,所有的脚本将从这个入口文件开始

单入口

const path = require('path')
module.exports = {
  entry: path.resolve(__dirname, '../src/main.js')
}

多入口

使用对象语法配置,更好扩展,例如一个项目有前台网页跟后台管理两个项目可用多入口管理。

entry: {
  app: path.resolve(__dirname, '../src/main.js'),
    admin: path.resolve(__dirname, '../src/admin.js'),
    },

输出output

打包后输出的文件,[name]跟[hash:8]表示文件名跟入口的保持一致但后面加上了hash的后缀让每次生成的文件名是唯一的。

单入口

module.exports = {
  output: {
    filename:  '[name].[hash:8].js',
    path: path.resolve(__dirname, '../dist'),
  },
}

多入口

module.exports = {
  entry: {
    app: './src/app.js',
    admin: './src/admin.js',
  },
  output: {
    filename: '[name].js',
    path: __dirname + '/dist',
  },
};
// 写入到硬盘:./dist/app.js, ./dist/admin.js

loader转化器

用于模块的源码的转换,也是同学们在配置webpack的时候频繁接触的一个配置项。

举个例子,加载typescript跟css文件需要用到ts-loader跟css-loader、style-loader, 如果没有对应的loader,打包会直接error掉。

我们可以这么配置:先 npm i css-loader style-loader

module: {
  rules: [
    {
      test: /.css$/,
      use: ['style-loader','css-loader'] 
    },
    {
      test: /.ts$/,
      use: 'ts-loader'
    }

必须留意的是,loader的执行是从右到左,就是css-loader执行完,再交给style-loader执行,

plugin插件

这是webpack配置的核心,有一些loader无法实现的功能,就通过plugin去扩展,建立一个规范的插件系统,能让你每次搭建项目的时候省去很多成本。

举个例子,我们会使用HtmlWebpackPlugin这个插件去生成一个html,其中会引入入口文件main.js。 假设不用这个插件,会发生什么?

当然是不会生成这个html,因此HtmlWebpackPlugin插件也是webpack的必备配置之一

const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
  new HtmlWebpackPlugin({
    template: path.resolve(__dirname, '../public/index.html') // 同时引入main.js的hash名字
  }),
],

模式mode

mode一共有production,development,node三种,如果没有设置,会默认为production

不同的mode对于默认优化的选项有所不同,环境变量也不同,具体需要了解每个插件的具体使用

选项描述
development会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development
production会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin 和 TerserPlugin
none没优化选项
module.exports = {
  mode: 'production'
}

source-map 的解读

Sourcemap 本质上是一个信息文件,里面储存着代码转换前后的对应位置信息。它记录了转换压缩后的代码所对应的转换前的源代码位置,是源代码和生产代码的映射。 Sourcemap 解决了在打包过程中,代码经过压缩,去空格以及 babel 编译转化后,由于代码之间差异性过大,造成无法debug的问题

当mode为development时,devtool默认为‘eval’,当mode为production时,devtool默认为false。

sourceMap的分类

  • source-map:外部。可以查看错误代码准确信息和源代码的错误位置。
  • inline-source-map:内联。只生成一个内联 Source Map,可以查看错误代码准确信息和源代码的错误位置
  • hidden-source-map:外部用于生产环境。可以查看错误代码准确信息,但不能追踪源代码错误,只能提示到构建后代码的错误位置。
  • eval-source-map:内联。每一个文件都生成对应的 Source Map,都在 eval 中,可以查看错误代码准确信息 和 源代码的错误位置。
  • nosources-source-map:外部。可以查看错误代码错误原因,但不能查看错误代码准确信息,并且没有任何源代码信息。
  • cheap-source-map:外部。可以查看错误代码准确信息和源代码的错误位置,只能把错误精确到整行,忽略列。
  • cheap-module-source-map:外部用于生产环境。可以错误代码准确信息和源代码的错误位置,module 会加入 loader 的 Source Map。
  • eval-cheap-module-source-map: 内联,用于开发环境,构建跟热更新比较快

内联和外部的区别: 外部生成了文件(.map),内联没有。内联构建速度更快。

笔者用的两种配置分为是

// webpack.dev.js
devtool: 'eval-cheap-module-source-map',
// webpack.prod.js
devtool: 'cheap-module-source-map'

浏览器兼容性 brower compatibility

Webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)。webpack 的 import() 和 require.ensure() 需要 Promise。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要提前加载 polyfill

环境 enviroment

本文使用的是webpack5 ,要求Node.js V在10.13.0+

Loader的汇总

笔者汇总了一部分常用的Loader以及其配置事项

  1. 浏览器兼容性:babel-loader
  2. css相关: css/style/less/postcss-loader
  3. vue: vue-loader

在配置loader前,先了解一下基本的配置

  • test: 匹配的文件,多用正则匹配
  • use: 使用loader,多用数组
  • exclude: 调整Loader解析的范围,包括某个路径下的文件,不如node_modules
  • include: 调整Loader解析的范围,包括某个路径下的文件

解决浏览器兼容性:babel

转义语法的babel-loader

譬如把const转为浏览器认识的var,虽然现在大部分主流浏览器都认识ES5之后的语法。

npm i babel-loader @babel/preset-env @babel/core

在rules配置:

{
  test: /.js$/,
    use: {
    loader: 'babel-loader',
      options: {
      presets: ['@babel/preset-env']
    }
  },
  exclude: /node_modules/
}

转义ES API的babel/polyfill

如果只有babel-loader,浏览器并不能识别出新的API(promise,proxy,includes),如图:

因此还需要配置一个babel/polyfill,在入口里面:

// npm i @babel/polyfill

entry: ["@babel/polyfill",path.resolve(__dirname, '../src/main.js')],

解析vue的vue-loader

vue-loader: 解析vue vue-template-compiler: 编译vue模板

npm i vue-loader vue-template-compiler vue-style-loader
npm i vue

在rules跟plugins配置:

const { VueLoaderPlugin } = require('vue-loader') // vue3的引入跟vue2路径不同

rules:{
  {
    test: /.vue$/,
      use: ['vue-loader']
  }
},
plugins:[
  ...
  new VueLoaderPlugin()
  ...
]

配置完成后,vue文件就可以正常解析了

解析CSS文件

需要引入的Loader不止一个

  • 引入的基本Loader: style-loader,css-loader,如有less还需要less-loader
  • postcss-loader 添加不同浏览器的css前缀: 解决部分css语法在不同浏览器的写法不同的弊端
modules.exports = {
  modules: {
    rules: [{
      test: /.css$/,
      use: [
        'style-loader',
        'css-loader',
        'postcss-loader'
      ] // 解析css文件必须的style-loader,css-loader
    },
            {
              test: /.less$/,
              use: [
                'style-loader',
                'css-loader',
                {
                  loader: 'postcss-loader',
                  options: {
                    postcssOptions: {
                      plugins: [
                        [
                          'postcss-preset-env' // 解决css不同浏览器兼容性
                        ],
                      ],
                    },
                  }
                }, 'less-loader'
              ]
            },
            }
}

拆分css

mini-css-extract-plugin: 把css拆分出来用外链的形式引入css文件,然后会在dist生成css文件,为每一个包含了 CSS 的 JS 文件创建一个 CSS 文件, ps: 使用该插件不能重复使用style-loader


plugins: [
  ...
  new MiniCssExtractPlugin({
    filename: devMode ? "[name].css" : "[name].[hash].css",
    chunkFilename: devMode ? "[id].css" : "[id].[hash].css",
  }), 
  ...
],
  module:{
  rules: [{
    test: /.css$/,
    use: [
      MiniCssExtractPlugin.loader,
      // 'style-loader',
      'css-loader',
      'postcss-loader'
    ] // 解析css文件必须的style-loader,css-loader
  }]
}

打包图片,字体,媒体等文件

file-loader: 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件url),并将文件移动到输出的目录中

url-loader 一般与file-loader搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中


{
  test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(?.*)?$/, //媒体文件
    use: [{
    loader: 'url-loader',
    options: {
      limit: 10240,
      fallback: {
        loader: 'file-loader',
        options: {
          name: 'img/[name].[hash:8].[ext]'
        }
      }
    }
  }],
    include: [resolve('src/assets/image')],
    exclude: /node_modules/
},
{
  test: /.(jpg|png|gif)$/i,
    use: [{
    loader: 'url-loader',
    options: {
      limit: 10240, // KB
      fallback: {
        loader: 'file-loader',
        options: {
          name: 'img/[name].[hash:8].[ext]'
        }
      }
    }
  }],
    include: [resolve('src/assets/image')],
    exclude: /node_modules/
},
{
  test: /.(woff2?|eot|ttf|otf)(?.*)?$/i, // 字体
    use: [{
    loader: 'url-loader',
    options: {
      limit: 10240,
      fallback: {
        loader: 'file-loader',
        options: {
          name: 'fonts/[name].[hash:8].[ext]'
        }
      }
    }
  }],
    include: [resolve('src/assets/icon')],
    exclude: /node_modules/
},

使用多线程提升构建速度

js是单线程的工程,在构建工程的过程中,要消耗大量的时间在Loader的转换过程中,为了提升构建速度,这里使用了thread-loader将任务拆分为多个线程去处理。 其原理是把任务分配到各个worker线程中,之前多数人会使用happyPack,但webpack官方使用了thread-loader取代happypack。


{
  test: /.js$/,
    use: [{
    loader: 'babel-loader',
    options: {
      presets: ['@babel/preset-env', ],
      cacheDirectory: true,
    }
  },
          {
            loader: 'thread-loader', // 多线程解析模块
            options: {
              workers: 3, // 开启几个 worker 进程来处理打包,默认是 os.cpus().length - 1
            }
          }
         ],
    exclude: /node_modules/
}        
...

必须使用的插件Plugins

配置plugins必须注意的是,由于我们的模式(mode)区分为development跟production,因此plugins也需要按照实际需要,在config(公用),dev,prod三个配置文件分开加入。

首先先配置公用部分的plugins

公用plugins

清除打包残留文件

每次执行npm run build 会发现dist文件夹里会残留上次打包的文件,在打包输出前清空文件夹

const {
  CleanWebpackPlugin
} = require('clean-webpack-plugin')

用外链的形式引入css

当一个html文件里面的css太多,全部把css添加到html中会显得很臃肿,那我们可以用mini-css-extract-plugin 把css拆分成外链引入,为每一个包含了 CSS 的 JS 文件创建一个 CSS 文件。

需要留意的是不能跟style-loader同时使用,下面用了hash

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
....
plugins: [
  new MiniCssExtractPlugin({
    filename: devMode ? "[name].css" : "[name].[hash].css",
    chunkFilename: devMode ? "[id].css" : "[id].[hash].css",
  }, {
    filename: devMode ? "[name].css" : "[name].[hash].less",
    chunkFilename: devMode ? "[id].css" : "[id].[hash].less",
  }),
]
  ....

  module:{
  rules:[{
    {
    test: /.css$/,
    use: [
      MiniCssExtractPlugin.loader,
      // 'style-loader',
      'css-loader',
      'postcss-loader'
    ] 
  },
         }]
}
....

生产打包后的html

wepback必备的插件之一,上述举例也有提到。 主要是生产打包后的html, 同时由于main.js文件会随机生成新的hash名字,html在引入main.js文件时频繁改名字会很浪费时间,此插件会自动同步改文件名

const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
  new HtmlWebpackPlugin({
    template: path.resolve(__dirname, '../public/index.html') // 同时引入main.js的hash名字
  }),
],

开发环境Dev

热更新: webpack-dev-server

当我们修改文件的内容时,要重新build一次才能看到变化,这样对开发的效率不友好。

需要留意的是,webpack-dev-server只是在开发环境搭建一个服务帮助开发人员提高开发效率,实现了实时更新的功能,在生产环境并不会用到这一个插件.

同时注意在plugins中加入webpack自带的HotModuleReplacementPlugin。

webpack-dev-server这个插件功能十分强大,官方文档有详细的记录 (webpack.docschina.org/configurati…)

npm i webpack-dev-server --save-dev
module.exports = {
  devServer: {
    // 基本目录
    static: {
      directory: path.join(__dirname, 'dist'),
    },
    // 自动压缩代码
    compress: true,
    port: 9000,
    // 自动打开浏览器
    open: true,
    // 热加载,默认是true
    hot: true,
  },
  plugins: [
    new Webpack.HotModuleReplacementPlugin()
  ] 
}

生产环境Prod

由于生产环境对性能的要求跟开发不同,需要引入的插件比较丰富,也更需要对项目构建有更高的熟悉程度

压缩Js文件

webpack mode设置production的时候会自动压缩js代码。原则上不需要引入terser-webpack-plugin进行重复工作。但是optimize-css-assets-webpack-plugin压缩css的同时会破坏原有的js压缩,所以这里我们引入terser-webpack-plugin进行压缩

option很多,使用了dropconsole去除打印的内容

const TerserPlugin = require("terser-webpack-plugin");

...

optimization: {
  minimizer: [
    new TerserPlugin({
      terserOptions: {
        compress:{
          drop_console: true
        }
      } 
    }),
  ],
    },
...

压缩CSS

前面有使用mini-css-extract-plugin的插件去拆分css,但这个插件并不能压缩CSS体积, 使用css-minimizer-webpack-plugin 可以压缩css的体积,但不同的是它是被加入到optimization的minimizer中,跟上述的js压缩插件共同作用

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
...
optimization: {
  minimizer: [
    new CssMinimizerPlugin(),
    new TerserPlugin({
      terserOptions: {
        compress:{
          drop_console: true
        }
      } 
    }),
  ],
    },
...

抽离第三方模块

使用DllReferencePlugin把不需要经常变更的静态文件抽离出来,譬如element-ui组件,这样每次打包的时候就不会再去重新打包选中的静态文件了。

如此一来,当我们修改代码后,webpack只需要打包项目的代码而不需要重复去编译没有发生改变的第三方库。这样当我们没有升级第三方库时,webpack就不会再对这些库进行打包,从而提升项目构建的速度。

首先我们在同级目录下新建文件webpack.dll.config.js,在entry的vendor里面配置了vue跟element-ui。

// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
// 每次执行npm run build 会发现dist文件夹里会残留上次打包的文件,在打包输出前清空文件夹
const {
  CleanWebpackPlugin
} = require('clean-webpack-plugin')

module.exports = {
  mode: 'production',
  // 你想要打包的模块的数组
  entry: {
    vendor: ['vue','element-plus'] 
  },
  output: {
    path: path.resolve(__dirname, '../public/vendor'), // 打包后文件输出的位置,要在静态资源里面避免被打包转义
    filename: '[name].dll.js',
    library: 'vendor_library' 
    // 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
  },
  plugins: [
    new CleanWebpackPlugin(),
    new webpack.DllPlugin({
      path: path.resolve(__dirname, '[name]-manifest.json'),
      name: 'vendor_library', 
      context: __dirname
    })
  ]
};

同时在packake.json里面配置dll的命令

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

最后在webpack.prod.js 加入配置项

plugins: [
  new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require('./vendor-manifest.json')
  }),
  

随后执行命令npm run dll 在public/vendor会出现一个vendor.dll.js文件,我们需要在html文件引入这个文件.

<body>
  <!-- dll插件的配置路径,注意是打包后的  -->
  <script src="./vendor/vendor.dll.js"></script>  
  <!-- <img src="../assets/image/logo192.png" alt=""> -->
  <!-- <img src="../assets/image/loginbg.png" alt=""> -->

  <div id="app"></div>
</body>

配置完毕,这样我们在不需要更新第三方包的时候可以不用执行npm run dll,然后直接执行npm run build/dev的时候就会发现构建速度有所提高。

分析打包后的文件

使用webpack-bundle-analyzer,启动项目后会打开一个展示各个包的大小。从图中可以看出来,es6.promise.js这个包

  • stat size: webpack 从入口文件打包递归到的所有模块体积
  • parsed size: 解析与代码压缩后输出到dist目录的体积
  • gzipped size: 开启Gzip之后的体积

总结

webpack身为前端必备的一项技能,各位在学会基础的配置之后,千万别忘了因地制宜,看看哪些插件更适合自己的项目哦