乐趣区

关于前端:webpack-从入门到放弃

webpack

webpack 于 2012 年 3 月 10 号诞生,作者是 Tobias(德国)。参考 GWT(Google Web Toolkit)的 code splitting 性能在 webpack 中进行实现。而后在 2014 年 Instagram 团队分享性能优化时,提出应用 webpack 的 code splitting 个性从而大火。当初 webpack 的呈现含糊了工作和构建的边界在 webpack 呈现之前,咱们应用 gulp、grunt 做工作的,构建是用其余工具实现,而当初 webpack 使其融为一体。

之前咱们在 html 加载 js 资源的时候,须要应用 script 标签,加载 css 也须要编写 css 文件进行加载,这样咱们每次 html 加载的时候就须要加载多个资源。而 webpack 将所有的资源都打包到 js 中,会有一个 entry 入口文件,entry 引入了 js、css 等资源文件,打包到一个 bundle 文件中,这样就加载一个资源。

webpack 最后外围解决的问题就是代码合并与拆分,它的核心理念是将资源都视为模块,对立进行打包和解决,而后再按规定进行拆分,提供了 loader 和 plugin 实现性能扩大。

webpack5 常识体系:https://gitmind.cn/app/docs/m1foeg1o

外围概念

  • entry:入口模块文件门路
  • output: 输入 bundle 文件门路
  • module:模块,webpack 构建 对象
  • bundle:输入文件,webpack 构建产物
  • chunk:构建生成 bundle 过程中,产生的两头文件,webpack 构建的两头产物
  • loader:文件转换器
  • plugin:插件,执行特定工作
  • mode:工作模式,默认采纳 production 模式,

我的项目初始化流程

  1. 创立 npm 我的项目
  2. 装置 webpack 依赖 webpack 和 webpack-cli
  3. 创立 js 入口文件
  4. 创立 webpack 配置文件
  5. 配置 package.json 的 build 命令
  6. 执行 npm run build 打包

Webpack 工作模式

Webpack 4 新增了一个工作模式的用法,这种用法大大简化了 Webpack 配置的复杂程度。你能够把它了解为针对不同环境的几组预设配置:

  • production 模式下,启动内置优化插件,主动优化打包后果,打包速度偏慢;
  • development 模式下,主动优化打包速度,增加一些调试过程中的辅助插件;
  • none 模式下,运行最原始的打包,不做任何额定解决。

针对工作模式的选项,如果你没有配置一个明确的值,打包过程中命令行终端会打印一个对应的配置正告。在这种状况下 Webpack 将默认应用 production 模式去工作。

production 模式下 Webpack 外部会主动启动一些优化插件,例如,主动压缩打包后的代码。这对理论生产环境是十分敌对的,然而打包的后果就无奈浏览了。

批改 Webpack 工作模式的形式有两种:

  • 通过 CLI –mode 参数传入;
  • 通过配置文件设置 mode 属性。

上述三种 Webpack 工作模式的具体差别咱们不再赘述了,你能够在官网文档中查看:https://webpack.js.org/configuration/mode/

打包后果运行原理

为了更好的了解打包后的代码,咱们先将 Webpack 工作模式设置为 none,这样 Webpack 就会依照最原始的状态进行打包,所失去的后果更容易了解和浏览。

依照 none 模式打包实现后,咱们关上最终生成的 bundle.js 文件,如下图所示:

咱们能够先把代码全副折叠起来,以便于理解整体的构造

整体生成的代码其实就是一个立刻执行函数,这个函数是 Webpack 工作入口(webpackBootstrap),它接管一个 modules 参数,调用时传入了一个数组。

开展这个数组,外面的元素均是参数列表雷同的函数。这里的函数对应的就是咱们源代码中的模块,也就是说每个模块最终被包裹到了这样一个函数中,从而实现模块公有作用域,如下图所示:

咱们再来开展 Webpack 工作入口函数,如下图所示:

这个函数外部并不简单,而且正文也很清晰,最开始定义了一个 installedModules 对象用于寄存或者缓存加载过的模块。紧接着定义了一个 require 函数,顾名思义,这个函数是用来加载模块的。再往后就是在 require 函数上挂载了一些其余的数据和工具函数,这些临时不必关怀。

这个函数执行到最初调用了 require 函数,传入的模块 id 为 0,开始加载模块。模块 id 实际上就是模块数组的元素下标,也就是说这里开始加载源代码中所谓的入口模块。

Webpack 资源模块加载

Webpack 想要实现的是整个前端我的项目的模块化,我的项目中的各种资源(包含 CSS 文件、图片等)都应该属于须要被治理的模块。换句话说,Webpack 不仅是 JavaScript 模块打包工具,还是整个前端我的项目(前端工程)的模块打包工具。也就是说,咱们能够通过 Webpack 去治理前端我的项目中任意类型的资源文件。

首先,咱们尝试通过 Webpack 打包我的项目中的一个 CSS 文件,将 Webpack 配置中的入口文件门路指定为 main.css 的文件门路,让 Webpack 间接打包 CSS 资源文件,具体配置如下所示:

module.exports = {
  // 款式文件门路
  entry: './src/main.css',
  output: {filename: 'bundle.js'}
}

配置实现过后回到命令行终端运行 Webpack 打包命令,此时你会发现命令行报出了一个模块解析谬误,如下所示:

错误信息大体的意思是说,在解析模块过程中遇到了非法字符,而且谬误呈现的地位就是在咱们的 CSS 文件中。

呈现这个谬误的起因是因为 Webpack 外部默认只可能解决 JS 模块代码,也就是说在打包过程中,它默认把所有遇到的文件都当作 JavaScript 代码进行解析,然而此处咱们让 Webpack 解决的是 CSS 代码,而 CSS 代码是不合乎 JavaScript 语法的,所以天然会报出模块解析谬误。

这里有一个十分重要的提醒:You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.(咱们须要用适当的加载器来解决这种文件类型,而以后并没有配置一个能够用来解决此文件的加载器)。

依据这个谬误阐明,咱们发现 Webpack 是用 Loader(加载器)来解决每个模块的,而外部默认的 Loader 只能解决 JS 模块,如果须要加载其余类型的模块就须要配置不同的 Loader。

解决下面的问题须要的是一个能够加载 CSS 模块的 Loader,最罕用到的是 css-loader。咱们须要通过 npm 先去装置这个 Loader,而后在配置文件中增加对应的配置,具体操作和配置如下所示:

$ npm install css-loader --save-dev 
# or yarn add css-loader --dev
// ./webpack.config.js
module.exports = {
  entry: './src/main.css',
  output: {filename: 'bundle.js'},
  module: {
    rules: [
      {
        test: /\.css$/, // 依据打包过程中所遇到文件门路匹配是否应用这个 loader
        use: 'css-loader' // 指定具体的 loader
      }
    ]
  }
}

在配置对象的 module 属性中增加一个 rules 数组。这个数组就是咱们针对资源模块的加载规定配置,其中的每个规定对象都须要设置两个属性:

  • 首先是 test 属性,它是一个正则表达式,用来匹配打包过程中所遇到文件门路,这里咱们是以 .css 结尾;
  • 而后是 use 属性,它用来指定匹配到的文件须要应用的 loader,这里用到的是 css-loader。

配置实现过后,咱们回到命令行终端从新运行打包命令,打包过程就不会再呈现谬误了,因为这时 CSS 文件会交给 css-loader 解决过后再由 Webpack 打包。

尝试在页面中应用这里输入的 bundle.js 文件,会发现刚刚的这个 main.css 模块并没有工作。咱们找到刚刚生成的 bundle.js 文件,因为这个文件是 Webpack 打包后的后果,所有的模块都应该在这个文件中呈现。因为默认打包入口在 Webpack 输入的后果中就是第一个模块,所以咱们只须要看第一个模块目前是什么样的,如下图所示:

仔细阅读这个文件,你会发现 css-loader 的作用是将 CSS 模块转换为一个 JS 模块,具体的实现办法是将咱们的 CSS 代码 push 到一个数组中,这个数组是由 css-loader 外部的一个模块提供的,然而整个过程并没有任何中央应用到了这个数组。

因而这里款式没有失效的起因是:css-loader 只会把 CSS 模块加载到 JS 代码中,而并不会应用这个模块。

所以这里咱们还须要在 css-loader 的根底上再应用一个 style-loader,把 css-loader 转换后的后果通过 style 标签追加到页面上。

装置完 style-loader 之后,咱们将配置文件中的 use 属性批改为一个数组,将 style-loader 也放进去。这里须要留神的是,一旦配置多个 Loader,执行程序是从后往前执行的,所以这里肯定要将 css-loader 放在最初,因为必须要 css-loader 先把 CSS 代码转换为 JS 模块,才能够失常打包,具体配置如下:

// ./webpack.config.js
module.exports = {
  entry: './src/main.css',
  output: {filename: 'bundle.js'},
  module: {
    rules: [
      {
        test: /\.css$/,
        // 对同一个模块应用多个 loader,留神程序
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
}

配置实现之后,再次回到命令行从新打包,此时 bundle.js 文件中会额定多出两个模块。篇幅的关系,咱们这里不再认真解读。style-loader 的作用总结一句话就是,将 css-loader 中所加载到的所有款式模块,通过创立 style 标签的形式增加到页面上。

Webpack 导入资源模块

个别 Webpack 打包的入口还是 JavaScript。因为从某种程度上来说,打包入口就是利用的运行入口,而目前前端利用中的业务是由 JS 驱动的,所以更正当的做法还是把 JS 文件作为打包的入口,而后在 JS 代码中通过 import 语句去加载 CSS 文件。

即使是通过 JS 代码去加载的 CSS 模块,css-loader 和 style-loader 依然能够失常工作。因为 Webpack 在打包过程中会循环遍历每个模块,而后依据配置将每个遇到的模块交给对应的 Loader 去解决,最初再将解决完的后果打包到一起。

其实 Webpack 不仅是倡议咱们在 JavaScript 中引入 CSS,还会倡议咱们在代码中引入以后业务所须要的任意资源文件。因为真正须要这个资源的并不是整个利用,而是你此时正在编写的代码。这就是 Webpack 的设计哲学。

可能你乍一想如同不太容易了解,那你能够做一个假如:假如咱们在开发页面上的某个部分性能时,须要用到一个款式模块和一个图片文件。如果你还是将这些资源文件独自引入到 HTML 中,而后再到 JS 中增加对应的逻辑代码。试想一下,如果前期这个部分性能不必了,你就须要同时删除 JS 中的代码和 HTML 中的资源文件引入,也就是同时须要保护这两条线。而如果你遵循 Webpack 的这种设计,所有资源的加载都是由 JS 代码管制,前期也就只须要保护 JS 代码这一条线了。

所以说,通过 JavaScript 代码去引入资源文件,或者说是建设 JavaScript 和资源文件的依赖关系,具备显著的劣势。因为 JavaScript 代码自身负责实现整个利用的业务性能,放大来说就是驱动了整个前端利用,而 JavaScript 代码在实现业务性能的过程中须要用到款式、图片等资源文件。如果建设这种依赖关系:

  • 一来逻辑上比拟正当,因为 JS 的确须要这些资源文件配合能力实现整体性能;
  • 二来配合 Webpack 这类工具的打包,能确保在上线时,资源不会缺失,而且都是必要的。

常见的 loader

目前 webpack 社区提供了十分多的资源加载器,基本上你能想到的需要都有对应的 loader。

名称 形容 链接
style-loader 将 css-loader 转换后的后果,通过 style 标签的形式追加到页面中 https://webpack.js.org/loaders/style-loader
css-loader 将 css 文件转换为 js 模块 https://webpack.js.org/loaders/css-loader
file-loader 把文件输入到一个文件夹中,在代码中通过绝对 URL 去援用输入文件 https://webpack.js.org/loaders/file-loader
url-loader 和 file-loader 相似,然而能在文件很小的状况下以 base64 的形式把文件内容注入到代码中去 https://webpack.js.org/loaders/url-loader
source-map-loader 加载额定的 source Map 文件,以不便断点调试 https://webpack.docschina.org/loaders/source-map-loader/
image-loader 加载并且压缩图片文件 https://github.com/tcoopman/image-webpack-loader
babel-loader 把 ES6 转换成 ES5 https://webpack.js.org/loaders/babel-loader
eslint-loader 通过 ESLint 查看 JavaScript 代码 https://github.com/webpack-contrib/eslint-loader
sass-loader 加载 Sass/SCSS 文件并将其编译为 CSS https://webpack.js.org/loaders/sass-loader
postcss-loader 解决 CSS 的加载器 PostCSS https://webpack.js.org/loaders/postcss-loader
vue-loader Vue 单文件组件的 webpack 加载器 https://github.com/vuejs/vue-loader

上述 loader 大体分为三类:

  • 编译转换类:将加载到的资源模块转换为 JavaScript 代码,例如 css-loader。
  • 文件操作类:将加载到的资源模块拷贝到输入的目录,同时将拜访门路向外导出,例如 file-loader。
  • 代码查看类:对加载到的资源文件代码去进行校验的加载器,目标是对立代码格调,进步代码品质,例如 eslint-loader。

::: tip
留神:在 Webpack 中,loader 的执行程序是从右向左执行的,因为 Webpack 抉择了 compose 这样的函数式编程形式,这种形式的表达式执行时从右向左的。
:::

文件资源加载器

大多数加载器都相似于 css-loader,都是将资源模块转换为 JS 代码的实现形式去工作,然而还有一些咱们罕用的资源文件,例如我的项目当中的图片或字体,这些文件无奈通过 JS 的形式去示意,对于这类的资源文件,咱们须要用到文件资源加载器,也就是 file-loader。

{
    test: /.png$/,
    use: 'file-loader'
}

webpack 在打包时遇到了图片文件,而后依据配置文件的配置,找到文件加载器,文件加载器先是将导入的文件拷贝到输入的目录,而后将文件拷贝到输入目录后的门路作为以后模块的返回值返回,对于利用来说须要的资源就被公布进去了,通过模块的导出成员拿到资源的拜访门路。

除了 file-loader 这种拷贝物理文件的模式解决资源以外,还有一种通过 Data URLs 的模式去示意文件。Data URLs 是一种非凡的 URL 协定,它能够用来间接去示意一个文件,传统的 url 须要服务器上有一个对应的文件,而后通过申请地址失去服务器上的这个文件。而 Data URLs 是一种以后能够间接示意文件内容的一种形式。

所以这种 url 当中的文本曾经蕴含了文件的内容,那么咱们在拜访这种 url 的时候不会发送任何的 http 申请。例如data:text/html;charset=UTF-8,<h1>html content</h1>,然而像图片和字体这种无奈通过文本示意的二进制文件,能够通过将其 base64 编码,以编码后果字符串示意内容data:image/png;base64,iVBORw0GgoAAAANSUHE...SuQmCC。在 webpack 打包动态资源是也能够应用这种形式去实现,应用 Data URLs 示意任何类型的文件了,这时咱们须要一个专门的加载器 url-loader。

{
    test: /.png$/,
    use: 'url-loader'
}

这样遇到 png 文件就会将其转换成 url 的模式。这种形式比拟适宜于我的项目中体积比拟小的资源,如果体积过大就会造成打包后果也会很大,从而影响运行速度。

那么最佳实际是小文件应用 Data URLs 的形式缩小申请次数,而大文件独自提取寄存,进步加载速度。

{
    test: /.png$/,
    use:{
        loader: 'url-loader',
        options:{limit: 10*1024 //10KB}
    }
}

这样只将 10KB 一下的文件转换为 Data URLs,超过 10KB 的依然会交给 file-loader。

自定义 loader

开发一个 Loader

Loader 作为 Webpack 的外围机制,外部的工作原理却非常简单。接下来咱们一起来开发一个本人的 Loader,通过这个开发过程再来深刻理解 Loader 的工作原理。

这里我的需要是开发一个能够加载 markdown 文件的加载器,以便能够在代码中间接导入 md 文件。咱们都应该晓得 markdown 个别是须要转换为 html 之后再出现到页面上的,所以我心愿导入 md 文件后,间接失去 markdown 转换后的 html 字符串,如下图所示:

在我的项目根目录下创立一个 markdown-loader.js 文件

// ./markdown-loader.js

module.exports = source => {
  // 加载到的模块内容 => '# About\n\nthis is a markdown file.'
  console.log(source)
  // 返回值就是最终被打包的内容
  return 'hello loader ~'
}

每个 Webpack 的 Loader 都须要导出一个函数,这个函数就是咱们这个 Loader 对资源的处理过程,它的输出就是加载到的资源文件内容,输入就是咱们加工后的后果。

实现当前,咱们回到 Webpack 配置文件中增加一个加载器规定,这里匹配到的扩展名是 .md,应用的加载器就是咱们刚刚编写的这个 markdown-loader.js 模块,具体代码如下所示:

// ./webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {filename: 'bundle.js'},
  module: {
    rules: [
      {
        test: /\.md$/,
        // 间接应用相对路径
        use: './markdown-loader'
      }
    ]
  }
}

::: tip
这里的 use 中不仅能够应用模块名称,还能够应用模块文件门路,这点与 Node 中的 require 函数是一样的。
:::

配置实现后,咱们关上命令行终端运行打包命令,如下图所示:

打包过程中命令行的确打印进去了咱们所导入的 Markdown 文件内容,这就意味着 Loader 函数的参数的确是文件的内容。

但同时也报出了一个解析谬误,说的是:You may need an additional loader to handle the result of these loaders.(咱们可能还须要一个额定的加载器来解决以后加载器的后果)。

那这到底是为什么呢?其实 Webpack 加载资源文件的过程相似于一个工作管道,你能够在这个过程中顺次应用多个 Loader,然而最终这个管道完结过后的后果必须是一段规范的 JS 代码字符串。

所以咱们这里才会呈现下面提到的谬误提醒,那解决的方法也就很显著了:

  • 间接在这个 Loader 的最初返回一段 JS 代码字符串;
  • 再找一个适合的加载器,在前面接着解决咱们这里失去的后果。

咱们将返回的字符串内容批改为 console.log(‘hello loader\~’),而后再次运行打包,此时 Webpack 就不再会报错了,代码如下所示:

// ./markdown-loader.js

module.exports = source => {
  // 加载到的模块内容 => '# About\n\nthis is a markdown file.'
  console.log(source)
  // 返回值就是最终被打包的内容
  // return 'hello loader ~'
  return 'console.log("hello loader ~")'
}

咱们关上输入的 bundle.js,找到最初一个模块(因为这个 md 文件是后引入的),如下图所示:

这个模块外面非常简单,就是把咱们刚刚返回的字符串间接拼接到了该模块中。这也解释了刚刚 Loader 管道最初必须返回 JS 代码的起因,因为如果轻易返回一个内容,放到这里语法就不通过了。

实现 Loader 的逻辑

装置一个可能将 Markdown 解析为 HTML 的模块,叫作 marked。装置实现后,咱们在 markdown-loader.js 中导入这个模块,而后应用这个模块去解析咱们的 source。这里解析完的后果就是一段 HTML 字符串,如果咱们间接返回的话同样会面临 Webpack 无奈解析模块的问题,正确的做法是把这段 HTML 字符串拼接为一段 JS 代码。

此时咱们心愿返回的代码是通过 module.exports 导出这段 HTML 字符串,这样外界导入模块时就能够接管到这个 HTML 字符串了。如果只是简略地拼接,那 HTML 中的换行和引号就都可能会造成语法错误,所以我这里应用了一个小技巧,具体操作如下所示:

// ./markdown-loader.js
const marked = require('marked')
module.exports = source => {
  // 1. 将 markdown 转换为 html 字符串
  const html = marked(source)
  // html => '<h1>About</h1><p>this is a markdown file.</p>'
  // 2. 将 html 字符串拼接为一段导出字符串的 JS 代码
  const code = `module.exports = ${JSON.stringify(html)}`
  return code 
  // code => 'export default"<h1>About</h1><p>this is a markdown file.</p>"'
}

先通过 JSON.stringify() 将字段字符串转换为规范的 JSON 字符串,而后再参加拼接,这样就不会有问题了。

咱们回到命令行再次运行打包,打包后的后果就是咱们所须要的了。

除了 module.exports 这种形式,Webpack 还容许咱们在返回的代码中应用 ES Modules 的形式导出,例如,咱们这里将 module.exports 批改为 export default,而后运行打包,后果同样是能够的,Webpack 外部会主动转换 ES Modules 代码。

// ./markdown-loader.js
const marked = require('marked')

module.exports = source => {const html = marked(source)
  // const code = `module.exports = ${JSON.stringify(html)}`
  const code = `export default ${JSON.stringify(html)}`
  return code 
}

多个 Loader 的配合

咱们还能够尝试一下刚刚说的第二种思路,就是在咱们这个 markdown-loader 中间接返回 HTML 字符串,而后交给下一个 Loader 解决。这就波及多个 Loader 相互配合工作的状况了。

咱们回到代码中,这里咱们间接返回 marked 解析后的 HTML,代码如下所示:

// ./markdown-loader.js
const marked = require('marked')
module.exports = source => {
  // 1. 将 markdown 转换为 html 字符串
  const html = marked(source)
  return html
}

而后咱们再装置一个解决 HTML 的 Loader,叫作 html-loader,代码如下所示:

// ./webpack.config.js

module.exports = {
  entry: './src/main.js',
  output: {filename: 'bundle.js',},
  module: {
    rules: [
      {
        test: /\.md$/,
        use: [
          'html-loader',
          './markdown-loader'
        ]
      }
    ]
  }
}

装置实现过后回到配置文件,这里同样把 use 属性批改为一个数组,以便顺次应用多个 Loader。不过同样须要留神,这里的执行程序是从后往前,也就是说咱们应该把先执行的 markdown-loader 放在前面,html-loader 放在后面。

通过以上的尝试发现了 Loader 的外部原理非常简单,就是负责资源文件从输出到输入的转换,除此之外还理解了 Loader 其实是一种管道的概念,咱们能够将此次 Loader 的后果交给下一个 Loader 去解决,通过多个 Loader 实现一个性能,例如 css-loader 与 style-loader 的配合。

Webpack 插件

插件机制是 Webpack 的另外一个重要的外围个性,Webpack 插件机制的目标是为了加强 Webpack 在我的项目自动化构建方面的能力。咱们都晓得 Loader 就是负责实现我的项目中各种各样资源模块的加载,从而实现整体我的项目的模块化,而 Plugin 则是用来解决我的项目中除了资源模块打包以外的其余自动化工作,所以说 Plugin 的能力范畴更广,用处天然也就更多。

几个插件最常见的利用场景:

  • 实现主动在打包之前革除 dist 目录(上次的打包后果);
  • 主动生成利用所须要的 HTML 文件;
  • 依据不同环境为代码注入相似 API 地址这种可能变动的局部;
  • 拷贝不须要参加打包的资源文件到输入目录;
  • 压缩 Webpack 打包实现后输入的文件;
  • 主动公布打包后果到服务器实现主动部署。

总之,有了 Plugin 的 Webpack 简直“无所不能”。借助插件,咱们就能够轻松实现前端工程化中绝大多数常常用到的性能,这也正是很多初学者会认为“Webpack 就是前端工程化,或者前端工程化就是 Webpack”的起因。

插件开发

通过后面的介绍,咱们晓得相比于 Loader,插件的能力范畴更宽,因为 Loader 只是在模块的加载环节工作,而插件的作用范畴简直能够涉及 Webpack 工作的每一个环节。

那么,这种插件机制是如何实现的呢?其实说起来也非常简单,Webpack 的插件机制就是咱们在软件开发中最常见的钩子机制。

钩子机制也特地容易了解,它有点相似于 Web 中的事件。在 Webpack 整个工作过程会有很多环节,为了便于插件的扩大,Webpack 简直在每一个环节都埋下了一个钩子。这样咱们在开发插件的时候,通过往这些不同节点上挂载不同的工作,就能够轻松扩大 Webpack 的能力。

具体有哪些事后定义好的钩子,咱们能够参考官网文档的 API:

  • Compiler Hooks
  • Compilation Hooks
  • JavascriptParser Hooks

接下来,咱们来开发一个本人的插件,看看具体如何往这些钩子上挂载工作。

这里我的需要是,心愿咱们开发的这个插件可能主动革除 Webpack 打包后果中的正文,这样一来,咱们的 bundle.js 将更容易浏览,如下图所示:

在我的项目根目录下增加一个独自的 JS 文件。

Webpack 要求咱们的插件必须是一个函数或者是一个蕴含 apply 办法的对象,个别咱们都会定义一个类型,在这个类型中定义 apply 办法。而后在应用时,再通过这个类型来创立一个实例对象去应用这个插件。

所以咱们这里定义一个 RemoveCommentsPlugin 类型,而后在这个类型中定义一个 apply 办法,这个办法会在 Webpack 启动时被调用,它接管一个 compiler 对象参数,这个对象是 Webpack 工作过程中最外围的对象,外面蕴含了咱们此次构建的所有配置信息,咱们就是通过这个对象去注册钩子函数,具体代码如下:

// ./remove-comments-plugin.js
class RemoveCommentsPlugin {apply (compiler) {console.log('RemoveCommentsPlugin 启动')
    // compiler => 蕴含了咱们此次构建的所有配置信息
  }
}

晓得这些过后,还须要明确咱们这个工作的执行机会,也就是到底应该把这个工作挂载到哪个钩子上。

咱们的需要是删除 bundle.js 中的正文,也就是说只有当 Webpack 须要生成的 bundle.js 文件内容明确过后才可能施行。

那依据 API 文档中的介绍,咱们找到一个叫作 emit 的钩子,这个钩子会在 Webpack 行将向输入目录输入文件时执行,十分合乎咱们的需要。

咱们回到代码中,通过 compiler 对象的 hooks 属性拜访到 emit 钩子,再通过 tap 办法注册一个钩子函数,这个办法接管两个参数:

  • 第一个是插件的名称,咱们这里的插件名称是 RemoveCommentsPlugin
  • 第二个是要挂载到这个钩子上的函数

依据 API 文档中的提醒,这里咱们在这个函数中接管一个 compilation 对象参数,这个对象能够了解为此次运行打包的上下文,所有打包过程中产生的后果,都会放到这个对象中。

咱们能够应用这个对象中的 assets 属性获取行将写入输入目录的资源文件信息,它是一个对象,咱们这里通过 for in 去遍历这个对象,其中键就是每个文件的名称,咱们尝试把它打印进去,具体代码如下:

// ./remove-comments-plugin.js

class RemoveCommentsPlugin {apply (compiler) {
    compiler.hooks.emit.tap('RemoveCommentsPlugin', compilation => {
      // compilation => 能够了解为此次打包的上下文
      for (const name in compilation.assets) {console.log(name) // 输入文件名称
      }
    })
  }
}

实现当前,咱们将这个插件利用到 Webpack 的配置中,而后回到命令行从新打包,此时打包过程就会打印咱们输入的文件名称,代码如下:

咱们再回到代码中,来打印一下每个资源文件的内容,文件内容须要通过遍历的值对象中的 source 办法获取,具体代码如下:

// ./remove-comments-plugin.js
class RemoveCommentsPlugin {apply (compiler) {
    compiler.hooks.emit.tap('RemoveCommentsPlugin', compilation => {
      // compilation => 能够了解为此次打包的上下文
      for (const name in compilation.assets) {// console.log(name)
        console.log(compilation.assets[name].source()) // 输入文件内容
      }
    })
  }
}

回到命令行,再次打包,此时输入的文件内容也能够失常被打印。

可能拿到文件名和文件内容后,咱们回到代码中。这里须要先判断文件名是不是以 .js 结尾,因为 Webpack 打包还有可能输入别的文件,而咱们的需要只须要解决 JS 文件。

那如果是 JS 文件,咱们将文件内容失去,再通过正则替换的形式移除掉代码中的正文,最初笼罩掉 compilation.assets 中对应的对象,在笼罩的对象中,咱们同样裸露一个 source 办法用来返回新的内容。另外还须要再裸露一个 size 办法,用来返回内容大小,这是 Webpack 外部要求的格局,具体代码如下:

// ./remove-comments-plugin.js

class RemoveCommentsPlugin {apply (compiler) {
    compiler.hooks.emit.tap('RemoveCommentsPlugin', compilation => {
      // compilation => 能够了解为此次打包的上下文
      for (const name in compilation.assets) {if (name.endsWith('.js')) {const contents = compilation.assets[name].source()
          const noComments = contents.replace(/\/\*{2,}\/\s?/g, '')
          compilation.assets[name] = {source: () => noComments,
            size: () => noComments.length}
        }
      }
    })
  }
}

实现当前回到命令行终端,再次打包,打包实现过后,咱们再来看一下 bundle.js,此时 bundle.js 中每行结尾的正文就都被移除了。

以上就是咱们实现一个移除正文插件的过程,通过这个过程咱们理解了:插件都是通过往 Webpack 生命周期的钩子中挂载工作函数实现的。

罕用的 plugin

  • html-webpack-plugin:该插件将为你生成一个 HTML5 文件,在 body 中应用 script 标签引入你所有 webpack 生成的 bundle
  • copy-webpack-plugin:在 webpack 中拷贝文件和文件夹
  • clean-webpack-plugin:用于删除 / 清理构建文件夹的 webpack 插件。
  • ProvidePlugin:注册全局援用的变量
  • mini-css-extract-plugin:将 css 提取到独自的文件中,为每个蕴含 css 的 js 文件创建一个 css 文件,并且反对 css 和 SourceMaps 的按需加载。
  • webpack-dev-server:提供了一个根本的 web server,并且具备 livereloading(实时从新加载) 性能
  • optimize-css-assets-webpack-plugin:压缩 css 文件
  • define-plugin:定义环境变量
  • uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码
  • webpack-parallel-uglify-plugin:多核压缩,进步压缩速度
  • webpack-bundle-analyzer:可视化 webpack 输入文件的体积

clean-webpack-plugin

Webpack 每次打包的后果都是间接笼罩到 dist 目录。而在打包之前,dist 目录中就可能曾经存入了一些在上一次打包操作时遗留的文件,当咱们再次打包时,只能笼罩掉同名文件,而那些曾经移除的资源文件就会始终累积在外面,最终导致部署上线时呈现多余文件,这显然十分不合理。

更为正当的做法就是在每次残缺打包之前,主动清理 dist 目录,这样每次打包过后,dist 目录中就只会存在那些必要的文件。

clean-webpack-plugin 这个插件就很好的实现了这一需要。它是一个第三方的 npm 包,咱们须要先通过 npm 装置一下,具体操作如下:

$ npm install clean-webpack-plugin --save-dev

装置过后,咱们回到 Webpack 的配置文件中,而后导入 clean-webpack-plugin 插件,这个插件模块导出了一个叫作 CleanWebpackPlugin 的成员,咱们先把它解构进去,具体代码如下。

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

回到配置对象中,增加一个 plugins 属性,这个属性就是专门用来配置插件的中央,它是一个数组,增加一个插件就是在这个数组中增加一个元素。

绝大多数插件模块导出的都是一个类型,咱们这里的 CleanWebpackPlugin 也不例外,应用它,就是通过这个类型创立一个实例,放入 plugins 数组中,具体代码如下:

// ./webpack.config.js
const {CleanWebpackPlugin} = require('clean-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {filename: 'bundle.js'},
  plugins: [new CleanWebpackPlugin()
  ]
}

实现当前咱们来测试一下 clean-webpack-plugin 插件的成果。回到命令行终端,再次运行 Webpack 打包,此时之前的打包后果就不会存在了,dist 目录中寄存的就都是咱们本次打包的后果。

一般来说,当咱们有了某个自动化的需要过后,能够先去找到一个适合的插件,而后装置这个插件,最初将它配置到 Webpack 配置对象的 plugins 数组中,这个过程惟一有可能不一样的中央就是,有的插件可能须要有一些配置参数。

html-webpack-plugin

咱们的 HTML 文件个别都是通过硬编码的形式,独自寄存在我的项目根目录下的,这种形式有两个问题:

  • 我的项目公布时,咱们须要同时公布根目录下的 HTML 文件和 dist 目录中所有的打包后果,十分麻烦,而且上线过后还要确保 HTML 代码中的资源文件门路是正确的。
  • 如果打包后果输入的目录或者文件名称发生变化,那 HTML 代码中所对应的 script 标签也须要咱们手动批改门路。

解决这两个问题最好的方法就是让 Webpack 在打包的同时,主动生成对应的 HTML 文件,让 HTML 文件也参加到整个我的项目的构建过程。这样的话,在构建过程中,Webpack 就能够主动将打包的 bundle 文件引入到页面中。

相比于之前写死 HTML 文件的形式,主动生成 HTML 的劣势在于:

  • HTML 也输入到 dist 目录中了,上线时咱们只须要把 dist 目录公布进来就能够了;
  • HTML 中的 script 标签是主动引入的,所以能够确保资源文件的门路是失常的。

具体的实现形式就须要借助于 html-webpack-plugin 插件来实现,这个插件也是一个第三方的 npm 模块,咱们这里同样须要独自装置这个模块,具体操作如下:

$ npm install html-webpack-plugin --save-dev

装置实现过后,回到配置文件,载入这个模块,不同于 clean-webpack-plugin,html-webpack-plugin 插件默认导出的就是插件类型,不须要再解构外部成员,具体如下:

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

有了这个类型过后,回到配置对象的 plugins 属性中,同样须要增加一下这个类型的实例对象,实现这个插件的应用,具体配置代码如下:

// ./webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {filename: 'bundle.js'},
  plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin()]
}

最初咱们回到命令行终端,再次运行打包命令,此时打包过程中就会主动生成一个 index.html 文件到 dist 目录。咱们找到这个文件,能够看到文件中的内容就是一段应用了 bundle.js 的空白 HTML,具体后果如下:

至此,Webpack 就能够动静生成利用所需的 HTML 文件了,然而这里依然存在一些须要改良的中央:

  • 对于生成的 HTML 文件,页面 title 必须要批改;
  • 很多时候还须要咱们自定义页面的一些 meta 标签和一些根底的 DOM 构造。

也就是说,还须要咱们可能充沛自定义这个插件最终输入的 HTML 文件。

如果只是简略的自定义,咱们能够通过批改 HtmlWebpackPlugin 的参数来实现。

咱们回到 Webpack 的配置文件中,这里咱们给 HtmlWebpackPlugin 构造函数传入一个对象参数,用于指定配置选项。其中,title 属性设置的是 HTML 的题目,咱们把它设置为 Webpack Plugin Simple。meta 属性须要以对象的模式设置页面中的元数据标签,这里咱们尝试为页面增加一个 viewport 设置,具体代码如下:

// ./webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {filename: 'bundle.js'},
  plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Webpack Plugin Sample',
      meta: {viewport: 'width=device-width'}
    })
  ]
}

实现当前回到命令行终端,再次打包,而后咱们再来看一下生成的 HTML 文件,此时这里的 title 和 meta 标签就会依据配置生成,具体后果如下:

如果须要对 HTML 进行大量的自定义,更好的做法是在源代码中增加一个用于生成 HTML 的模板,而后让 html-webpack-plugin 插件依据这个模板去生成页面文件。

咱们这里在 src 目录下新建一个 index.html 文件作为 HTML 文件的模板,而后依据咱们的须要在这个文件中增加相应的元素。对于模板中动静的内容,能够应用 Lodash 模板语法输入,模板中能够通过 htmlWebpackPlugin.options 拜访这个插件的配置数据,例如咱们这里输入配置中的 title 属性,具体代码如下:

<!-- ./src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
  <div class="container">
    <h1> 页面上的根底构造 </h1>
    <div id="root"></div>
  </div>
</body>
</html>

有了模板文件过后,回到配置文件中,咱们通过 HtmlWebpackPlugin 的 template 属性指定所应用的模板,具体配置如下:

// ./webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {filename: 'bundle.js'},
  plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Webpack Plugin Sample',
      template: './src/index.html'
    })
  ]
}

实现当前咱们回到命令行终端,运行打包命令,而后再来看一下生成的 HTML 文件,此时 HTML 中就都是依据模板生成的内容了,具体后果如下:

至此,你应该理解了如何通过 html-webpack-plugin 自定义输入 HTML 文件内容。

对于 html-webpack-plugin 插件,除了自定义输入文件的内容,同时输入多个 HTML 文件也是一个十分常见的需要,除非咱们的利用是一个单页应用程序,否则肯定须要输入多个 HTML 文件。

如果须要同时输入多个 HTML 文件,其实也非常简单,咱们回到配置文件中,这里通过 HtmlWebpackPlugin 创立的对象就是用于生成 index.html 的,那咱们齐全能够再创立一个新的实例对象,用于创立额定的 HTML 文件。

例如,这里咱们再来增加一个 HtmlWebpackPlugin 实例用于创立一个 about.html 的页面文件,咱们须要通过 filename 指定输入文件名,这个属性的默认值是 index.html,咱们把它设置为 about.html,具体配置如下:

// ./webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {filename: 'bundle.js'},

  plugins: [new CleanWebpackPlugin(),
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      title: 'Webpack Plugin Sample',
      template: './src/index.html'
    }),

    // 用于生成 about.html
    new HtmlWebpackPlugin({filename: 'about.html'})
  ]
}

实现当前咱们再次回到命令行终端,运行打包命令,而后咱们开展 dist 目录,此时 dist 目录中就同时生成了 index.html 和 about.html 两个页面文件。

依据这个尝试咱们就应该晓得,如果须要创立多个页面,就须要在插件列表中退出多个 HtmlWebpackPlugin 的实例对象,让每个对象负责一个页面文件的生成。

当然了,对于同时输入多个 HTML,个别咱们还会配合 Webpack 多入口打包的用法,这样就能够让不同的 HTML 应用不同的打包后果。

copy-webpack-plugin

在咱们的我的项目中个别还有一些不须要参加构建的动态文件,那它们最终也须要公布到线上,例如网站的 favicon、robots.txt 等。

个别咱们倡议,把这类文件对立放在我的项目根目录下的 public 或者 static 目录中,咱们心愿 Webpack 在打包时一并将这个目录下所有的文件复制到输入目录。

咱们们能够应用 copy-webpack-plugin 插件来帮咱们实现。先装置一下 copy-webpack-plugin 插件,装置实现过后,回到配置文件中,导入这个插件类型。而后同样在 plugins 属性中增加一个这个类型的实例,具体代码如下:

// ./webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {filename: 'bundle.js'},
  plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Webpack Plugin Sample',
      template: './src/index.html'
    }),

    new CopyWebpackPlugin({patterns: ['public'] // 须要拷贝的目录或者门路通配符
    })
  ]
}

这个插件类型的构造函数须要咱们传入一个字符串数组,用于指定须要拷贝的文件门路。它能够是一个通配符,也能够是一个目录或者文件的相对路径。咱们这里传入的是 public 目录,示意将这个目录下所有文件全副拷贝到输入目录中。当然了,你还能够在这个数组中持续增加其它门路,这样它在工作时能够同时拷贝。

配置实现当前回到命令行终端,再次运行 Webpack,此时 public 目录下的文件就会同时拷贝到输入目录中。

加强 Webpack 的开发体验

理解了 Webpack 的相干概念、根本用法,以及外围工作原理,看似如同曾经把握了 Webpack,然而如果以目前的认知状态去应答日常的开发工作,其实还远远不够。

因为“编写源代码 → Webpack 打包 → 运行利用 → 浏览器查看”这种周而复始的开发方式过于原始,在理论开发过程中,如果你还是依照这种形式来工作,开发效率必然会非常低下。那到底该如何进步咱们的开发效率呢?这里我先对一个较为理想的开发环境做出构想:

  • 首先,它必须可能应用 HTTP 服务运行而不是文件模式预览。这样的话,一来更靠近生产环境状态,二来咱们的我的项目可能须要应用 AJAX 之类的 API,以文件模式拜访会产生诸多问题。
  • 其次,在咱们批改完代码过后,Webpack 可能主动实现构建,而后浏览器能够即时显示最新的运行后果,这样就大大减少了开发过程中额定的反复操作,同时也会让咱们更加专一,效率天然失去晋升。
  • 最初,它还须要能提供 Source Map 反对。这样一来,运行过程中呈现的谬误就能够疾速定位到源代码中的地位,而不是打包后后果中的地位,更便于咱们疾速定位谬误、调试利用。

对于以上的这些需要 Webpack 都曾经提供了绝对应的性能实现了。

Webpack 主动编译

如果咱们每次批改完代码,都是通过命令行手动反复运行 Webpack 命令,从而失去最新的打包后果,那么这样的操作过程基本没有任何开发体验可言。

针对上述这个问题,咱们能够应用 Webpack CLI 提供的另外一种 watch 工作模式来解决。

如果你之前理解过其它的一些构建工具,你应该对 watch 模式并不生疏。在这种模式下,Webpack 实现首次构建过后,我的项目中的源文件会被监督,一旦产生任何改变,Webpack 都会主动从新运行打包工作。

具体的用法也非常简单,就是在启动 Webpack 时,增加一个 –watch 的 CLI 参数,这样的话,Webpack 就会以监督模式启动运行。在打包实现过后,CLI 不会立刻退出,它会期待文件变动再次工作,直到咱们手动完结它或是呈现不可控的异样。

在 watch 模式下咱们就只需专一编码,不用再去手动实现编译工作了,相比于原始手动操作的形式,有了很显著的提高。

咱们还能够再开启另外一个命令行终端,同时以 HTTP 模式运行咱们的利用,而后关上浏览器去预览利用。

咱们能够将浏览器移至屏幕的左侧,而后将编辑器移至右侧,此时咱们尝试批改源代码,保留过后,以 watch 模式工作的 Webpack 就会主动从新打包,而后咱们就能够在浏览器中刷新页面查看最新的后果。那此时咱们的开发体验就是:批改代码 → Webpack 主动打包 → 手动刷新浏览器 → 预览运行后果。

::: tip
这里我应用的动态文件服务器是一个 npm 模块,叫作 serve
:::

如果浏览器可能在 Webpack 打包过后主动刷新,那咱们的开发体验将会更好一些。

如果你曾经理解过一个叫作 BrowserSync 的工具,你应该晓得 BrowserSync 就能够帮咱们实现文件变动过后浏览器主动刷新的性能。

所以,咱们就能够应用 BrowserSync 工具替换 serve 工具,启动 HTTP 服务,这里还须要同时监听 dist 目录下文件的变动,具体命令如下:

# 能够先通过 npm 全局装置 browser-sync 模块,而后再应用这个模块
$ npm install browser-sync --global
$ browser-sync dist --watch

# 或者也能够应用 npx 间接应用远端模块
$ npx browser-sync dist --watch

启动过后,咱们回到编辑器,而后尝试批改源文件,保留实现当前浏览器就会主动刷新,显示最新后果。

它的原理就是 Webpack 监督源代码变动,主动打包源代码到 dist 中,而 dist 中文件的变动又被 BrowserSync 监听了,从而实现主动编译并且主动刷新浏览器的性能,整个过程由两个工具别离监督不同的内容。

这种 watch 模式 + BrowserSync 尽管也实现了咱们的需要,然而这种办法有很多弊病:

  • 操作繁缛,咱们须要同时应用两个工具,那么须要理解的内容就会更多,学习老本大大提高;
  • 效率低下,因为整个过程中,Webpack 会将文件写入磁盘,BrowserSync 再进行读取。过程中波及大量磁盘读写操作,必然会导致效率低下。

Webpack Dev Server

webpack-dev-server 是 Webpack 官网推出的一款开发工具,依据它的名字咱们就应该晓得,它提供了一个开发服务器,并且将主动编译和主动刷新浏览器等一系列对开发敌对的性能全副集成在了一起。

Webpack 官网推出 webpack-dev-server 这款工具的初衷,就是为了进步开发者日常的开发效率,应用这个工具就能够解决我在结尾所提出的问题。而且它是一个高度集成的工具,应用起来非常的不便。

webpack-dev-server 同样也是一个独立的 npm 模块,所以咱们须要通过 npm 将 webpack-dev-server 作为我的项目的开发依赖装置。装置实现过后,这个模块为咱们提供了一个叫作 webpack-dev-server 的 CLI 程序,咱们同样能够间接通过 npx 间接去运行这个 CLI,或者把它定义到 npm scripts 中,具体操作如下:

# 装置 webpack-dev-server
$ npm install webpack-dev-server --save-dev

# 运行 webpack-dev-server
$ npx webpack-dev-server

运行 webpack-dev-server 这个命令时,它外部会启动一个 HTTP Server,为打包的后果提供动态文件服务,并且主动应用 Webpack 打包咱们的利用,而后监听源代码的变动,一旦文件发生变化,它会立刻从新打包,大抵流程如下:

不过这里须要留神的是,webpack-dev-server 为了进步工作速率,它并没有将打包后果写入到磁盘中,而是临时寄存在内存中,外部的 HTTP Server 也是从内存中读取这些文件的。这样一来,就会缩小很多不必要的磁盘读写操作,大大提高了整体的构建效率。

咱们还能够为 webpack-dev-server 命令传入一个 –open 的参数,用于主动唤起浏览器关上咱们的利用。关上浏览器过后,此时如果你有两块屏幕,就能够把浏览器放到另外一块屏幕上,而后体验一边编码,一边即时预览的开发环境了。

配置选项

Webpack 配置对象中能够有一个叫作 devServer 的属性,专门用来为 webpack-dev-server 提供配置,具体如下:

// ./webpack.config.js
const path = require('path')

module.exports = {
  // ...
  devServer: {contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000
    // ...
    // 具体配置文档:https://webpack.js.org/configuration/dev-server/
  }
}
动态资源拜访

webpack-dev-server 默认会将构建后果和输入文件全副作为开发服务器的资源文件,也就是说,只有通过 Webpack 打包可能输入的文件都能够间接被拜访到。然而如果你还有一些没有参加打包的动态文件也须要作为开发服务器的资源被拜访,那你就须要额定通过配置“通知”webpack-dev-server。

具体的办法就是在 webpack-dev-server 的配置对象中增加一个对应的配置。咱们回到配置文件中,找到 devServer 属性,它的类型是一个对象,咱们能够通过这个 devServer 对象的 contentBase 属性指定额定的动态资源门路。这个 contentBase 属性能够是一个字符串或者数组,也就是说你能够配置一个或者多个门路。具体配置如下:

// ./webpack.config.js
module.exports = {
  // ...
  devServer: {contentBase: 'public'}
}

咱们这里将这个门路设置为我的项目中的 public 目录。可能有人会有疑难,之前咱们在应用插件的时候曾经将这个目录通过 copy-webpack-plugin 输入到了输入目录,依照刚刚的说法,所有输入的文件都能够间接被 serve,也就是能间接拜访到,按情理应该不须要再作为开发服务器的动态资源门路了。

的确是这样的,而且如果你能想到这一点,也就证实你真正了解了 webpack-dev-server 的文件加载规定。

然而在理论应用 Webpack 时,咱们个别都会把 copy-webpack-plugin 这种插件留在上线前的那一次打包中应用,而开发过程中个别不会用它。因为在开发过程中,咱们会频繁反复执行打包工作,假如这个目录下须要拷贝的文件比拟多,如果每次都须要执行这个插件,那打包过程开销就会比拟大,每次构建的速度也就天然会升高。

至于如何实现某些插件只在生产模式打包时应用,是额定的话题,这里咱们先移除 CopyWebpackPlugin,确保这里的打包不会输入 public 目录中的动态资源文件,而后回到命令行再次执行 webpack-dev-server。

启动过后,咱们关上浏览器,这里咱们拜访的页面文件和 bundle.js 文件均来自于打包后果。咱们再尝试拜访 favicon.ico,因为这个文件曾经没有参加打包了,所以这个文件必然来源于 contentBase 中配置的目录了。

以上就是 contentBase 额定为开发服务器指定查找资源目录的操作形式。

Proxy 代理

因为 webpack-dev-server 是一个本地开发服务器,所以咱们的利用在开发阶段是独立运行在 localhost 的一个端口上,而后端服务又是运行在另外一个地址上。然而最终上线过后,咱们的利用个别又会和后端服务部署到同源地址下。

那这样就会呈现一个十分常见的问题:在理论生产环境中可能间接拜访的 API,回到咱们的开发环境后,再次拜访这些 API 就会产生跨域申请问题。

可能有人会说,咱们能够用跨域资源共享(CORS)解决这个问题。的确如此,如果咱们申请的后端 API 反对 CORS,那这个问题就不成立了。然而并不是每种状况下服务端的 API 都反对 CORS。如果前后端利用是同源部署,也就是协定 / 域名 / 端口统一,那这种状况下,基本没必要开启 CORS,所以跨域申请的问题依然是不可避免的。

那解决这种开发阶段跨域申请问题最好的方法,就是在开发服务器中配置一个后端 API 的代理服务,也就是把后端接口服务代理到本地的开发服务地址。

webpack-dev-server 就反对间接通过配置的形式,增加代理服务。接下来,咱们来看一下它的具体用法。这里咱们假设 GitHub 的 API 就是咱们利用的后端服务,那咱们的指标就是将 GitHub API 代理到本地开发服务器中。

GitHub API 的 Endpoint 都是在根目录下,也就是说不同的 Endpoint 只是 URL 中的门路局部不同,例如 https://api.github.com/users 和 https://api.github.com/events。

晓得 API 地址的规定过后,咱们回到配置文件中,在 devServer 配置属性中增加一个 proxy 属性,这个属性值须要是一个对象,对象中的每个属性就是一个代理规定配置。

属性的名称是须要被代理的申请门路前缀,个别为了分别,我都会设置为 /api。值是所对应的代理规定配置,咱们将代理指标地址设置为 https://api.github.com,具体代码如下:

// ./webpack.config.js

module.exports = {
  // ...
  devServer: {
    proxy: {
      '/api': {target: 'https://api.github.com'}
    }
  }
}

那此时咱们申请 http://localhost:8080/api/users,就相当于申请了 https://api.github.com/api/users

而咱们真正心愿申请的地址是 https://api.github.com/users,所以对于代理门路结尾的 /api 咱们要重写掉。咱们能够增加一个 pathRewrite 属性来实现代理门路重写,重写规定就是把门路中结尾的 /api 替换为空,pathRewrite 最终会以正则的形式来替换申请门路。

// ./webpack.config.js

module.exports = {
  // ...
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.github.com',
        pathRewrite: {'^/api': '' // 替换掉代理地址中的 /api}
      }
    }
  }
}

这样咱们代理的地址就失常了。

除此之外,咱们还需设置一个 changeOrigin 属性为 true。这是因为默认代理服务器会以咱们理论在浏览器中申请的主机名,也就是 localhost:8080 作为代理申请中的主机名。而个别服务器须要依据申请的主机名判断是哪个网站的申请,那 localhost:8080 这个主机名,对于 GitHub 的服务器来说,必定无奈失常申请,所以须要批改。

将代理规定配置的 changeOrigin 属性设置为 true,就会以理论代理申请地址中的主机名去申请,也就是咱们失常申请这个地址的主机名是什么,理论申请 GitHub 时就会设置成什么。

// ./webpack.config.js

module.exports = {
  // ...
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.github.com',
        pathRewrite: {'^/api': '' // 替换掉代理地址中的 /api},
        changeOrigin: true // 确保申请 GitHub 的主机名就是:api.github.com
      }
    }
  }
}

实现当前,关上命令行终端,运行 webpack-dev-server。而后关上浏览器,这里咱们间接尝试申请 http://localhost:8080/api/users,失去的就是 GitHub 的用户数据。因为这个地址曾经被代理到了 GitHub 的用户数据接口。

SourceMap

通过构建或者编译之类的操作,咱们将开发阶段编写的源代码转换为可能在生产环境中运行的代码,这种提高同时也意味着咱们理论运行的代码和咱们真正编写的代码之间存在很大的差别。

在这种状况下,如果须要调试咱们的利用,或是利用运行的过程中呈现意料之外的谬误,那咱们将无从下手。因为无论是调试还是报错,都是基于构建后的代码进行的,咱们只能看到错误信息在构建后代码中具体的地位,却很难间接定位到源代码中对应的地位。

SourceMap 简介

Source Map(源代码地图)就是解决此类问题最好的方法,从它的名字就可能看出它的作用:映射转换后的代码与源代码之间的关系。一段转换后的代码,通过转换过程中生成的 Source Map 文件就能够逆向解析失去对应的源代码。

目前很多第三方库在公布的文件中都会同时提供一个 .map 后缀的 Source Map 文件。例如 jQuery。咱们能够关上它的 Source Map 文件看一下,如下图所示:

这是一个 JSON 格局的文件,这个 JSON 外面记录的就是转换后和转换前代码之间的映射关系,次要存在以下几个属性:

  • version 是指定所应用的 Source Map 规范版本
  • sources 中记录的是转换前的源文件名称,因为有可能呈现多个文件打包转换为一个文件的状况,所以这里是一个数组
  • names 是源代码中应用的一些成员名称,咱们都晓得个别压缩代码时会将咱们开发阶段编写的有意义的变量名替换为一些简短的字符,这个属性中记录的就是原始的名称
  • mappings 属性,这个属性最为要害,它是一个叫作 base64-VLQ 编码的字符串,外面记录的信息就是转换后代码中的字符与转换前代码中的字符之间的映射关系

个别咱们会在转换后的代码中通过增加一行正文的形式来去引入 Source Map 文件。不过这个个性只是用于开发调试的,例如//# sourceMappingURL=jquery-3.4.1.min.map

这样咱们在 Chrome 浏览器中如果关上了开发人员工具,它就会主动申请这个文件,而后依据这个文件的内容逆向解析进去源代码,以便于调试。同时因为有了映射关系,所以代码中如果呈现了谬误,也就能主动定位找到源代码中的地位了。

Webpack 中配置 Source Map

咱们应用 Webpack 打包的过程,同样反对为打包后果生成对应的 Source Map。用法上也很简略,不过它提供了很多不同模式,导致大部分初学者操作起来可能会比拟懵。那接下来咱们就一起钻研一下在 Webpack 中如何开启 Source Map,而后再来理解一下几种不同的 Source Map 模式之间存在哪些差别。

咱们回到配置文件中,这里咱们要应用的配置属性叫作 devtool。这个属性就是用来配置开发过程中的辅助工具,也就是与 Source Map 相干的一些性能。咱们能够先将这个属性设置为 source-map,具体代码如下:

// ./webpack.config.js

module.exports = {devtool: 'source-map' // source map 设置}

而后关上命令行终端,运行 Webpack 打包。打包实现过后,咱们关上 dist 目录,此时这个目录中就会生成咱们 bundle.js 的 Source Map 文件,与此同时 bundle.js 中也会通过正文引入这个 Source Map 文件。

如果你只是须要应用 Source Map 的话,操作到这里就曾经实现了。然而只会应用这种最一般的 Source Map 模式还远远不够。因为现阶段 Webpack 反对的 Source Map 模式有很多种。每种模式下所生成的 Source Map 成果和生成速度都不一样。显然,成果好的个别生成速度会比较慢,而生成速度快的个别就没有什么成果。Webpack 中的 devtool 配置,除了能够应用 source-map 这个值,它还反对很多其余的选项,具体的咱们能够参考文档中的不同模式的比照表。

devtool 取值 首次构建 从新构建 适宜生产环境 品质
(none) 最快 最快
eval 最快 最快 转换后代码
cheap-eval-source-map 更快 转换后代码(只有行信息)
cheap-module-eval-source-map 更快 源代码(只有行信息)
eval-source-map 最慢 残缺源代码
cheap-source-map 转换后代码(只有行信息)
cheap-module-source-map 更慢 源代码(只有行信息)
inline-cheap-source-map 转换后代码(只有行信息)
inline-cheap-module-source-map 更慢 源代码(只有行信息)
source-map 最慢 最慢 残缺源代码
inline-source-map 最慢 最慢 残缺源代码
hidden-source-map 最慢 最慢 残缺源代码
nosources-source-map 最慢 最慢 无源码内容,只有行列信息

上表别离从首次构建速度、监督模式从新构建速度、是否适宜生成环境应用,以及 Source Map 的品质,这四个维度去横向比照了不同的 Source Map 模式之间的差别。

比照不同的 devtools 模式

eval

它就是将模块代码放到 eval 函数中执行,并且通过 sourceURL 标注所属文件门路,在这种模式下没有 Source Map 文件,所以只能定位是哪个文件出错

eval-source-map

这个模式也是应用 eval 函数执行模块代码,不过这里有所不同的是,eval-source-map 模式除了定位文件,还能够定位具体的行列信息。相比于 eval 模式,它可能生成 Source Map 文件,能够反推出源代码

cheap-eval-source-map

阉割版的 eval-source-map,因为它尽管也生成了 Source Map 文件,然而这种模式下的 Source Map 只能定位到行,而定位不到列,所以在成果上差了一点点,然而构建速度会晋升很多

cheap-module-eval-source-map

这里就是在 cheap-eval-source-map 的根底上多了一个 module,这种模式同样也只能定位到行

inline-source-map

它跟一般的 source-map 成果雷同,只不过这种模式下 Source Map 文件不是以物理文件存在,而是以 data URLs 的形式呈现在代码中。咱们后面遇到的 eval-source-map 也是这种 inline 的形式。

hidden-source-map

在这个模式下,咱们在开发工具中看不到 Source Map 的成果,然而它也的确生成了 Source Map 文件,这就跟 jQuery 一样,尽管生成了 Source Map 文件,然而代码中并没有援用对应的 Source Map 文件,开发者能够本人抉择应用。

nosources-source-map

在这个模式下,咱们能看到谬误呈现的地位(蕴含行列地位),然而点进去却看不到源代码。这是为了爱护源代码在生产环境中不裸露。

模块热替换

当你理论去应用 Webpack Dev Server 主动刷新的个性去实现具体的开发工作时,你会发现还是有一些不难受的中央。当咱们批改完编辑器文本对应的款式过后,本来想着能够即时看到最新的界面成果,然而这时编辑器中的内容却没有了。

呈现这个问题的起因,是因为咱们每次批改完代码,Webpack 都能够监督到变动,而后主动打包,再告诉浏览器主动刷新,一旦页面整体刷新,那页面中的任何操作状态都将会失落,所以才会呈现咱们下面所看到的状况。

解决办法是可能实现在页面不刷新的状况下,代码也能够及时的更新到浏览器的页面中,从新执行,防止页面状态失落。针对这个需要,Webpack 同样能够满足。

介绍

HMR 全称 Hot Module Replacement,翻译过去叫作“模块热替换”或“模块热更新”。

计算机行业常常听到一个叫作热拔插的名词,指的就是咱们能够在一个正在运行的机器上随时插拔设施,机器的运行状态不会受插拔的影响,而且插上去的设施能够立刻工作,例如咱们电脑上的 USB 端口就能够热拔插。

模块热替换中的“热”和这里提到的“热拔插”是雷同的意思,都是指在运行过程中的即时变动。

Webpack 中的模块热替换,指的是咱们能够在利用运行过程中,实时的去替换掉利用中的某个模块,而利用的运行状态不会因而而扭转。例如,咱们在利用运行过程中批改了某个模块,通过主动刷新会导致整个利用的整体刷新,那页面中的状态信息都会失落;而如果应用的是 HMR,就能够实现只将批改的模块实时替换至利用中,不用齐全刷新整个利用。

开启 HMR

HMR 曾经集成在了 webpack 模块中了,所以不须要再独自装置什么模块。应用这个个性最简略的形式就是,在运行 webpack-dev-server 命令时,通过 –hot 参数去开启这个个性。或者也能够在配置文件中通过增加对应的配置来开启这个性能。那咱们这里关上配置文件,这里须要配置两个中央:

  • 首先须要将 devServer 对象中的 hot 属性设置为 true
  • 而后须要载入一个插件,这个插件是 webpack 内置的一个插件,所以咱们先导入 webpack 模块,有了这个模块过后,这里应用的是一个叫作 HotModuleReplacementPlugin 的插件
// ./webpack.config.js
const webpack = require('webpack')

module.exports = {
  // ...
  devServer: {
    // 开启 HMR 个性,如果资源不反对 HMR 会 fallback 到 live reloading
    hot: true
    // 只应用 HMR,不会 fallback 到 live reloading
    // hotOnly: true
  },

  plugins: [
    // ...
    // HMR 个性所须要的插件
    new webpack.HotModuleReplacementPlugin()]
}

生产环境优化

上述一些用法和个性都是为了在开发阶段可能领有更好的开发体验。而随着这些体验的晋升,一个新的问题呈现在咱们背后:咱们的打包后果会变得越来越臃肿。

这是因为在这个过程中 Webpack 为了实现这些个性,会主动往打包后果中增加一些内容。例如咱们之前用到的 Source Map 和 HMR,它们都会在输入后果中增加额定代码来实现各自的性能。

然而这些额定的代码对生产环境来说是冗余的。因为生产环境和开发环境有很大的差别,在生产环境中咱们强调的是以更大量、更高效的代码实现业务性能,也就是重视运行效率。而开发环境中咱们重视的只是开发效率。

那针对这个问题,Webpack 4 推出了 mode 的用法,为咱们提供了不同模式下的一些预设配置,其中生产模式下就曾经包含了很多优化配置。

同时 Webpack 也倡议咱们为不同的工作环境创立不同的配置,以便于让咱们的打包后果能够实用于不同的环境。

不同环境下的配置

咱们先为不同的工作环境创立不同的 Webpack 配置。创立不同环境配置的形式次要有两种:

  • 在配置文件中增加相应的判断条件,依据环境不同导出不同配置;
  • 为不同环境独自增加一个配置文件,一个环境对应一个配置文件。

咱们别离尝试一下通过这两种形式,为开发环境和生产环境创立不同配置。

首先咱们来看在配置文件中增加判断的形式。咱们回到配置文件中,Webpack 配置文件还反对导出一个函数,而后在函数中返回所须要的配置对象。这个函数能够接管两个参数,第一个是 env,是咱们通过 CLI 传递的环境名参数,第二个是 argv,是运行 CLI 过程中的所有参数。具体代码如下:

// ./webpack.config.js
module.exports = (env, argv) => {
  return {// ... webpack 配置}
}

那咱们就能够借助这个特点,为开发环境和生产环境创立不同配置。我先将不同模式下公共的配置定义为一个 config 对象,具体代码如下:

// ./webpack.config.js

module.exports = (env, argv) => {
  const config = {// ... 不同模式下的公共配置}
  return config
}

而后通过判断,再为 config 对象增加不同环境下的非凡配置。具体如下:

// ./webpack.config.js

module.exports = (env, argv) => {
  const config = {// ... 不同模式下的公共配置}

  if (env === 'development') {
    // 为 config 增加开发模式下的非凡配置
    config.mode = 'development'
    config.devtool = 'cheap-eval-module-source-map'
  } else if (env === 'production') {
    // 为 config 增加生产模式下的非凡配置
    config.mode = 'production'
    config.devtool = 'nosources-source-map'
  }

  return config

}

例如这里,咱们判断 env 等于 development(开发模式)的时候,咱们将 mode 设置为 development,将 devtool 设置为 cheap-eval-module-source-map;而当 env 等于 production(生产模式)时,咱们又将 mode 和 devtool 设置为生产模式下须要的值。

当然,你还能够别离为不同模式设置其余不同的属性、插件,这也都是相似的。

通过这种形式实现配置过后,咱们关上命令行终端,这里咱们再去执行 webpack 命令时就能够通过 –env 参数去指定具体的环境名称,从而实现在不同环境中应用不同的配置。

那这就是通过在 Webpack 配置文件导出的函数中对环境进行判断,从而实现不同环境对应不同配置。这种形式是 Webpack 倡议的形式。

不同环境的配置文件

通过判断环境名参数返回不同配置对象的形式只实用于中小型我的项目,因为一旦我的项目变得复杂,咱们的配置也会一起变得复杂起来。所以对于大型的我的项目来说,还是倡议应用不同环境对应不同配置文件的形式来实现。

个别在这种形式下,我的项目中起码会有三个 webpack 的配置文件。其中两个用来别离适配开发环境和生产环境,另外一个则是公共配置。因为开发环境和生产环境的配置并不是齐全不同的,所以须要一个公共文件来形象两者雷同的配置。具体配置文件构造如下:

.
├── webpack.common.js ···························· 公共配置
├── webpack.dev.js ······························· 开发模式配置
└── webpack.prod.js ······························ 生产模式配置

首先咱们在我的项目根目录下新建一个 webpack.common.js,在这个文件中导出不同模式下的公共配置;而后再来创立一个 webpack.dev.js 和一个 webpack.prod.js 别离定义开发和生产环境非凡的配置。

在不同环境的具体配置中咱们先导入公共配置对象,而后这里能够应用 Object.assign 办法把公共配置对象复制到具体环境的配置对象中,并且同时去笼罩其中的一些配置。具体如下:

// ./webpack.common.js
module.exports = {// ... 公共配置}

// ./webpack.prod.js
const common = require('./webpack.common')

module.exports = Object.assign(common, {// 生产模式配置})
// ./webpack.dev.js
const common = require('./webpack.common')
module.exports = Object.assign(common, {// 开发模式配置})

如果你相熟 Object.assign 办法,就应该晓得,这个办法会齐全笼罩掉前一个对象中的同名属性。这个特点对于一般值类型属性的笼罩都没有什么问题。然而像配置中的 plugins 这种数组,咱们只是心愿在原有公共配置的插件根底上增加一些插件,那 Object.assign 就做不到了。

所以咱们须要更适合的办法来合并这里的配置与公共的配置。你能够应用 Lodash 提供的 merge 函数来实现,不过社区中提供了更为业余的模块 webpack-merge,它专门用来满足咱们这里合并 Webpack 配置的需要。

咱们能够先通过 npm 装置一下 webpack-merge 模块。具体命令如下:

$ npm i webpack-merge --save-dev 
# or yarn add webpack-merge --dev

装置实现过后咱们回到配置文件中,这里先载入这个模块。那这个模块导出的就是一个 merge 函数,咱们应用这个函数来合并这里的配置与公共的配置。具体代码如下:

// ./webpack.common.js
module.exports = {// ... 公共配置}

// ./webpack.prod.js
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {// 生产模式配置})

// ./webpack.dev.jss
const merge = require('webpack-merge')
const common = require('./webpack.common')

module.exports = merge(common, {// 开发模式配置})

应用 webpack-merge 过后,咱们这里的配置对象就能够跟一般的 webpack 配置一样,须要什么就配置什么,merge 函数外部会主动解决合并的逻辑。

别离配置实现过后,咱们再次回到命令行终端,而后尝试运行 webpack 打包。不过因为这里曾经没有默认的配置文件了,所以咱们须要通过 –config 参数来指定咱们所应用的配置文件门路。例如:

$ webpack --config webpack.prod.js

生产模式下的优化插件

在 Webpack 4 中新增的 production 模式下,外部就主动开启了很多通用的优化性能。对于使用者而言,开箱即用是十分不便的,然而对于学习者而言,这种开箱即用会导致咱们疏忽掉很多须要理解的货色。以至于呈现问题无从下手。

如果想要深刻理解 Webpack 的应用,能够独自钻研每一个配置背地的作用。这里咱们看一下 production 模式下几个次要的优化性能,顺便理解一下 Webpack 如何优化打包后果。

Define Plugin

首先是 DefinePlugin,DefinePlugin 是用来为咱们代码中注入全局成员的。在 production 模式下,默认通过这个插件往代码中注入了一个 process.env.NODE\_ENV。很多第三方模块都是通过这个成员去判断运行环境,从而决定是否执行例如打印日志之类的操作。

这里咱们来独自应用一下这个插件。咱们回到配置文件中,DefinePlugin 是一个内置的插件,所以咱们先导入 webpack 模块,而后再到 plugins 中增加这个插件。这个插件的构造函数接管一个对象参数,对象中的成员都能够被注入到代码中。具体代码如下:

// ./webpack.config.js
const webpack = require('webpack')
module.exports = {
  // ... 其余配置
  plugins: [
    new webpack.DefinePlugin({API_BASE_URL: 'https://api.example.com'})
  ]
}

例如咱们这里通过 DefinePlugin 定义一个 API\_BASE\_URL,用来为咱们的代码注入 API 服务地址,它的值是一个字符串。

而后咱们回到代码中打印这个 API\_BASE\_URL。具体代码如下:

// ./src/main.js

console.log(API_BASE_URL)

这里咱们发现 DefinePlugin 其实就是把咱们配置的字符串内容间接替换到了代码中,而目前这个字符串的内容为 https://api.example.com,字符串中并没有蕴含引号,所以替换进来 …

// ./webpack.config.js
const webpack = require('webpack')

module.exports = {
  // ... 其余配置
  plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一个代码片段
      API_BASE_URL: '"https://api.example.com"'
    })
  ]
}

这样代码内的 API\_BASE\_URL 就会被替换为 “https://api.example.com”

这里有一个十分罕用的小技巧,如果咱们须要注入的是一个值,就能够通过 JSON.stringify 的形式来失去示意这个值的字面量。这样就不容易出错了。具体实现如下:

// ./webpack.config.js
const webpack = require('webpack')

module.exports = {
  // ... 其余配置
  plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一个代码片段
      API_BASE_URL: JSON.stringify('https://api.example.com')
    })
  ]
}

DefinePlugin 的作用尽管简略,然而却十分有用,咱们能够用它在代码中注入一些可能变动的值。

Tree-shaking

Tree Shaking 翻译过去的意思就是“摇树”。随同着摇树的动作,树上的枯树枝和树叶就会掉落下来。

咱们这里要介绍的 Tree-shaking 也是同样的情理,不过通过 Tree-shaking“摇掉”的是代码中那些没有用到的局部,这部分没有用的代码更业余的说法应该叫作未援用代码(dead-code)。

Tree-shaking 最早是 Rollup 中推出的一个个性,Webpack 从 2.0 过后开始反对这个个性。

咱们应用 Webpack 生产模式打包的优化过程中,就应用主动开启这个性能,以此来检测咱们代码中的未援用代码,而后主动移除它们。去除冗余代码是生产环境优化中一个很重要的工作,Webpack 的 Tree-shaking 性能就很好地实现了这一点。

试想一下,如果咱们在我的项目中引入 Lodash 这种工具库,大部分状况下咱们只会应用其中的某几个工具函数,而其余没有用到的局部就是冗余代码。通过 Tree-shaking 就能够极大地缩小最终打包后 bundle 的体积。

须要留神的是,Tree-shaking 并不是指 Webpack 中的某一个配置选项,而是一组性能搭配应用过后实现的成果,这组性能在生产模式下都会主动启用,所以应用生产模式打包就会有 Tree-shaking 的成果。

开启 Tree Shaking

咱们关上 Webpack 的配置文件,在配置对象中增加一个 optimization 属性,这个属性用来集中配置 Webpack 内置优化性能,它的值也是一个对象。

在 optimization 对象中咱们能够先开启一个 usedExports 选项,示意在输入后果中只导出内部应用了的成员,具体配置代码如下:

// ./webpack.config.js

module.exports = {
  // ... 其余配置项
  optimization: {
    // 模块只导出被应用的成员
    usedExports: true
  }
}

如果咱们开启压缩代码性能,就能够主动压缩掉这些没有用到的代码。咱们能够回到配置文件中,尝试在 optimization 配置中开启 minimize,具体配置如下:

// ./webpack.config.js

module.exports = {
  // ... 其余配置项
  optimization: {
    // 模块只导出被应用的成员
    usedExports: true,
    // 压缩输入后果
    minimize: true
  }
}

这就是 Tree-shaking 的实现,整个过程用到了 Webpack 的两个优化性能:

  • usedExports – 打包后果中只导出内部用到的成员;
  • minimize – 压缩打包后果。
合并模块(扩大)

除了 usedExports 选项之外,咱们还能够应用一个 concatenateModules 选项持续优化输入。

一般打包只是将一个模块最终放入一个独自的函数中,如果咱们的模块很多,就意味着在输入后果中会有很多的模块函数。

concatenateModules 配置的作用就是尽可能将所有模块合并到一起输入到一个函数中,这样既晋升了运行效率,又缩小了代码的体积。

咱们回到配置文件中,这里咱们在 optimization 属性中开启 concatenateModules。同时,为了更好地看到成果,咱们先敞开 minimize,具体配置如下:

// ./webpack.config.js
module.exports = {
  // ... 其余配置项
  optimization: {
    // 模块只导出被应用的成员
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中
    concatenateModules: true,
    // 压缩输入后果
    minimize: false
  }
}

而后回到命令行终端再次运行打包。那此时 bundle.js 中就不再是一个模块对应一个函数了,而是把所有的模块都放到了一个函数中。这个个性又被称为 Scope Hoisting,也就是作用域晋升,它是 Webpack 3.0 中增加的一个个性。如果再配合 minimize 选项,打包后果的体积又会减小很多。

联合 babel-loader 的问题

因为晚期的 Webpack 倒退十分快,那变动也就比拟多,所以当咱们去找材料时,失去的后果不肯定实用于以后咱们所应用的版本。而 Tree-shaking 的材料更是如此,很多材料中都示意“为 JS 模块配置 babel-loader,会导致 Tree-shaking 生效”。

针对这个问题,这里我对立阐明一下:

首先你须要明确一点:Tree-shaking 实现的前提是 ES Modules,也就是说:最终交给 Webpack 打包的代码,必须是应用 ES Modules 的形式来组织的模块化。

咱们都晓得 Webpack 在打包所有的模块代码之前,先是将模块依据配置交给不同的 Loader 解决,最初再将 Loader 解决的后果打包到一起。

很多时候,咱们为了更好的兼容性,会抉择应用 babel-loader 去转换咱们源代码中的一些 ECMAScript 的新个性。而 Babel 在转换 JS 代码时,很有可能解决掉咱们代码中的 ES Modules 局部,把它们转换成 CommonJS 的形式。

当然了,Babel 具体会不会解决 ES Modules 代码,取决于咱们有没有为它配置应用转换 ES Modules 的插件。

很多时候,咱们为 Babel 配置的都是一个 preset(预设插件汇合),而不是某些具体的插件。例如,目前市面上应用最多的 @babel/preset-env,这个预设外面就有转换 ES Modules 的插件。所以当咱们应用这个预设时,代码中的 ES Modules 局部就会被转换成 CommonJS 形式。那 Webpack 再去打包时,拿到的就是以 CommonJS 形式组织的代码了,所以 Tree-shaking 不能失效。

而在最新版本(8.x)的 babel-loader 中,曾经主动帮咱们敞开了对 ES Modules 转换的插件,你能够参考对应版本 babel-loader 的源码,所以最新版本的 babel-loader 并不会导致 Tree-shaking 生效。如果你不确定当初应用的 babel-loader 会不会导致这个问题,最简略的方法就是在配置中将 @babel/preset-env 的 modules 属性设置为 false,确保不会转换 ES Modules,也就确保了 Tree-shaking 的前提。

sideEffects

Webpack 4 中新增了一个 sideEffects 个性,它容许咱们通过配置标识咱们的代码是否有副作用,从而提供更大的压缩空间。

::: tip
模块的副作用指的就是模块执行的时候除了导出成员,是否还做了其余的事件。
:::

这个个性个别只有咱们去开发一个 npm 模块时才会用到。Tree-shaking 只能移除没有用到的代码成员,而想要残缺移除没有用到的模块,那就须要开启 sideEffects 个性了。

咱们关上 Webpack 的配置文件,在 optimization 中开启 sideEffects 个性,具体配置如下:

// ./webpack.config.js

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {filename: 'bundle.js'},
  optimization: {sideEffects: true}
}

::: tip
留神这个个性在 production 模式下同样会主动开启。
:::

那此时 Webpack 在打包某个模块之前,会先查看这个模块所属的 package.json 中的 sideEffects 标识,以此来判断这个模块是否有副作用,如果没有副作用的话,这些没用到的模块就不再被打包。换句话说,即使这些没有用到的模块中存在一些副作用代码,咱们也能够通过 package.json 中的 sideEffects 去强制申明没有副作用。

那咱们关上我的项目 package.json 增加一个 sideEffects 字段,把它设置为 false,具体代码如下:

{
  "version": "0.1.0",
  "license": "MIT",
  "scripts": {"build": "webpack"},

  "devDependencies": {
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  },
  "sideEffects": false
}

这样就示意咱们这个我的项目中的所有代码都没有副作用,让 Webpack 放心大胆地去“干”。此时那些没有用到的模块就彻底不会被打包进来了。那这就是 sideEffects 的作用。

目前很多第三方的库或者框架都曾经应用了 sideEffects 标识,所以咱们再也不必放心为了一个小性能引入一个很大体积的库了。例如,某个 UI 组件库中只有一两个组件会用到,那只有它反对 sideEffects,你就能够放心大胆的间接用了。

应用 sideEffects 这个性能的前提是确定你的代码没有副作用,或者副作用代码没有全局影响,否则打包时就会误删掉你那些有意义的副作用代码。咱们在 JS 中间接载入的 CSS 模块,也都属于副作用模块,所以说不是所有的副作用都应该被移除,有一些必要的副作用须要保留下来。最好的方法就是在 package.json 中的 sideEffects 字段中标识须要保留副作用的模块门路(能够应用通配符),具体配置如下:

{
  "version": "0.1.0",
  "license": "MIT",
  "scripts": {"build": "webpack"},

  "devDependencies": {
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  },

  "sideEffects": [
    "./src/extend.js",
    "*.css"
  ]
}

Code Splitting

通过 Webpack 实现前端我的项目整体模块化的劣势诚然显著,然而它也会存在一些弊病:它最终会将咱们所有的代码打包到一起。试想一下,如果咱们的利用非常复杂,模块十分多,那么这种 All in One 的形式就会导致打包的后果过大,甚至超过 4~5M。

在绝大多数的状况下,利用刚开始工作时,并不是所有的模块都是必须的。如果这些模块全副被打包到一起,即使利用只须要一两个模块工作,也必须先把 bundle.js 整体加载进来,而且前端利用个别都是运行在浏览器端,这也就意味着利用的响应速度会受到影响,也会节约大量的流量和带宽。

所以这种 All in One 的形式并不合理,更为正当的计划是把打包的后果依照肯定的规定拆散到多个 bundle 中,而后依据利用的运行须要按需加载。这样就能够升高启动老本,进步响应速度。

为了解决打包后果过大导致的问题,Webpack 设计了一种分包性能:Code Splitting(代码宰割)。

Code Splitting 通过把我的项目中的资源模块依照咱们设计的规定打包到不同的 bundle 中,从而升高利用的启动老本,进步响应速度。

Webpack 实现分包的形式次要有两种:

  • 依据业务不同配置多个打包入口,输入多个打包后果;
  • 联合 ES Modules 的动静导入(Dynamic Imports)个性,按需加载模块。
多入口打包

多入口打包个别实用于传统的多页应用程序,最常见的划分规定就是一个页面对应一个打包入口,对于不同页面间专用的局部,再提取到公共的后果中。

// ./webpack.config.js

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

module.exports = {
  entry: {
    index: './src/index.js',
    album: './src/album.js'
  },

  output: {filename: '[name].bundle.js' // [name] 是入口名称
  },

  // ... 其余配置
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index'] // 指定应用 index.bundle.js
    }),

    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album'] // 指定应用 album.bundle.js
    })
  ]
}

个别 entry 属性中只会配置一个打包入口,如果咱们须要配置多个入口,能够把 entry 定义成一个对象。

::: tip
留神:这里 entry 是定义为对象而不是数组,如果是数组的话就是把多个文件打包到一起,还是一个入口。
:::

在这个对象中一个属性就是一个入口,属性名称就是这个入口的名称,值就是这个入口对应的文件门路。那咱们这里配置的就是 index 和 album 页面所对应的 JS 文件门路。

一旦咱们的入口配置为多入口模式,那输入文件名也须要批改,因为两个入口就有两个打包后果,不能都叫 bundle.js。咱们能够在这里应用 [name] 这种占位符来输入动静的文件名,[name] 最终会被替换为入口的名称。每个页面只应用它对应的那个输入后果指定所应用的 bundle,咱们能够通过 HtmlWebpackPlugin 的 chunks 属性来设置

提取公共模块

多入口打包自身非常容易了解和应用,然而它也存在一个小问题,就是不同的入口中肯定会存在一些公共应用的模块,如果依照目前这种多入口打包的形式,就会呈现多个打包后果中有雷同的模块的状况。

所以咱们还须要把这些公共的模块提取到一个独自的 bundle 中。Webpack 中实现公共模块提取非常简单,咱们只须要在优化配置中开启 splitChunks 性能就能够了,具体配置如下:

// ./webpack.config.js

module.exports = {
  entry: {
    index: './src/index.js',
    album: './src/album.js'
  },

  output: {filename: '[name].bundle.js' // [name] 是入口名称
  },

  optimization: {
    splitChunks: {
      // 主动提取所有公共模块到独自 bundle
      chunks: 'all'
    }
  }

  // ... 其余配置
}

这里在 optimization 属性中增加 splitChunks 属性,那这个属性的值是一个对象,这个对象须要配置一个 chunks 属性,咱们这里将它设置为 all,示意所有公共模块都能够被提取。除此之外,splitChunks 还反对很多高级的用法,能够实现各种各样的分包策略,这些咱们能够在文档中找到对应的介绍。

动静导入

除了多入口打包的形式,Code Splitting 更常见的实现形式还是联合 ES Modules 的动静导入个性,从而实现按需加载。

按需加载是开发浏览器利用中一个十分常见的需要。个别咱们常说的按需加载指的是加载数据或者加载图片,然而咱们这里所说的按需加载,指的是在利用运行过程中,须要某个资源模块时,才去加载这个模块。这种形式极大地升高了利用启动时须要加载的资源体积,进步了利用的响应速度,同时也节俭了带宽和流量。

Webpack 中反对应用动静导入的形式实现模块的按需加载,而且所有动静导入的模块都会被主动提取到独自的 bundle 中,从而实现分包。

相比于多入口的形式,动静导入更为灵便,因为咱们能够通过代码中的逻辑去管制需不需要加载某个模块,或者什么时候加载某个模块。而且咱们分包的目标中,很重要的一点就是让模块实现按需加载,从而进步利用的响应速度。

// ./src/index.js
// import posts from './posts/posts'
// import album from './album/album'

const update = () => {
  const hash = window.location.hash || '#posts'
  const mainElement = document.querySelector('.main')
  mainElement.innerHTML = ''if (hash ==='#posts') {// mainElement.appendChild(posts())
    import('./posts/posts').then(({default: posts}) => {mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {// mainElement.appendChild(album())
    import('./album/album').then(({default: album}) => {mainElement.appendChild(album())
    })
  }
}

window.addEventListener('hashchange', update)

update()

为了动静导入模块,能够将 import 关键字作为函数调用。当以这种形式应用时,import 函数返回一个 Promise 对象。这就是 ES Modules 规范中的 Dynamic Imports。

这里咱们先移除 import 这种动态导入,而后在须要应用组件的中央通过 import 函数导入指定门路,那这个办法返回的是一个 Promise。在这个 Promise 的 then 办法中咱们可能拿到模块对象。因为咱们这里的 posts 和 album 模块是以默认成员导出,所以咱们须要解构模块对象中的 default,先拿到导出成员,而后再失常应用这个导出成员。

如果你应用的是 Vue.js 之类的 SPA 开发框架的话,那你我的项目中路由映射的组件就能够通过这种动静导入的形式实现按需加载,从而实现分包。

魔法正文

默认通过动静导入产生的 bundle 文件,它的 name 就是一个序号,这并没有什么不好,因为大多数时候,在生产环境中咱们基本不必关怀资源文件的名称。

然而如果你还是须要给这些 bundle 命名的话,就能够应用 Webpack 所特有的魔法正文去实现。具体形式如下:

// 魔法正文

import(/* webpackChunkName: 'posts' */'./posts/posts')
  .then(({default: posts}) => {mainElement.appendChild(posts())
  })

所谓魔法正文,就是在 import 函数的形式参数地位,增加一个行内正文,这个正文有一个特定的格局:webpackChunkName: ”,这样就能够给分包的 chunk 起名字了。

除此之外,魔法正文还有个非凡用处:如果你的 chunkName 雷同的话,那雷同的 chunkName 最终就会被打包到一起。

Mini CSS Extract Plugin

对于 CSS 文件的打包,个别咱们会应用 style-loader 进行解决,这种解决形式最终的打包后果就是 CSS 代码会内嵌到 JS 代码中。

mini-css-extract-plugin 是一个能够将 CSS 代码从打包后果中提取进去的插件,它的应用非常简单,同样也须要先通过 npm 装置一下这个插件。具体命令如下:

$ npm i mini-css-extract-plugin --save-dev

装置实现过后,咱们回到 Webpack 的配置文件。具体配置如下:

// ./webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  mode: 'none',
  entry: {main: './src/index.js'},
  output: {filename: '[name].bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将款式通过 style 标签注入
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [new MiniCssExtractPlugin()
  ]

}

咱们这里先导入这个插件模块,导入过后咱们就能够将这个插件增加到配置对象的 plugins 数组中了。这样 Mini CSS Extract Plugin 在工作时就会主动提取代码中的 CSS 了。

除此以外,Mini CSS Extract Plugin 还须要咱们应用 MiniCssExtractPlugin 中提供的 loader 去替换掉 style-loader,以此来捕捉到所有的款式。

这样的话,打包过后,款式就会寄存在独立的文件中,间接通过 link 标签引入页面。

不过这里须要留神的是,如果你的 CSS 体积不是很大的话,提取到单个文件中,成果可能事与愿违,因为独自的文件就须要独自申请一次。集体教训是如果 CSS 超过 200KB 才须要思考是否提取进去,作为独自的文件。

Optimize CSS Assets Webpack Plugin

应用了 Mini CSS Extract Plugin 过后,款式就被提取到独自的 CSS 文件中了。然而这里同样有一个小问题。咱们以生产模式运行打包,JavaScript 文件失常被压缩了,而款式文件并没有被压缩。

Webpack 官网举荐了一个 Optimize CSS Assets Webpack Plugin 插件。咱们能够应用这个插件来压缩咱们的款式文件。

咱们回到命令行,先来装置这个插件,具体命令如下:

$ npm i optimize-css-assets-webpack-plugin --save-dev

装置实现过后,咱们回到配置文件中,增加对应的配置。具体代码如下:

// ./webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {main: './src/index.js'},
  output: {filename: '[name].bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [new MiniCssExtractPlugin(),
    new OptimizeCssAssetsWebpackPlugin()]
}

能你会在这个插件的官网文档中发现,文档中的这个插件并不是配置在 plugins 数组中的,而是增加到了 optimization 对象中的 minimizer 属性中。具体如下:

// ./webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {main: './src/index.js'},
  output: {filename: '[name].bundle.js'
  },
  optimization: {
    minimizer: [new OptimizeCssAssetsWebpackPlugin()
    ]
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [new MiniCssExtractPlugin()
  ]
}

如果咱们配置到 plugins 属性中,那么这个插件在任何状况下都会工作。而配置到 minimizer 中,就只会在 minimize 个性开启时才工作。Webpack 倡议像这种压缩插件,应该咱们配置到 minimizer 中,便于 minimize 选项的对立管制。

输入文件名 Hash

个别咱们部署前端资源文件时,都会启用服务器动态资源缓存,这样对于用户浏览器而言,它就能够缓存住浏览器中的动态资源,后续就不须要再申请服务器失去这些动态资源文件了,这样咱们整体利用的响应速度就有一个大幅度的晋升。

不过开启动态资源缓存有一些问题,如果在缓存生效工夫中设置工夫过短的话,那成果就不会特地显著,如果设置过长,一旦利用产生了更新部署之后不能及时更新到客户端。

为了解决这个问题,咱们倡议生产模式下,给文件名增加 Hash 值,这样资源文件产生扭转,文件名称也会发生变化,对于客户端而言,全新的文件名就是全新的申请,那就没有缓存的问题。

webpack 中 filename 属性和绝大多数插件的 filename 属性一样,反对应用占位符的形式来为文件名设置 Hash,它们反对三种 Hash 成果各不相同。

module.exports = {
  mode: 'none',
  entry: {main: './src/index.js'},
  output: {filename: '[name]-[hash].bundle.js'
  },
}

最一般的就是 [hash] 我的项目级别的,只有我的项目中任意一个文件产生改变,那么这次打包过程中的 hash 值都会发生变化。

其次是[chunkhash],这个 hash 是 chunk 级别的,也就是在打包过程中,只有是同一路的打包,那 chunkhash 都是雷同的。

最初是[contenthash],它是文件级别的 hash,它是依据输入文件的内容生成的 hash,也就是不同的文件有不同的 hash 值。

::: tip
咱们能够在占位符中指定 hash 的长度[contenthash:8]
:::

Webpack 打包优化

webpack 打包优化方向:

  • 打包速度:优化打包速度,次要是晋升了咱们的开发效率,更快的打包构建过程。
  • 打包体积:优化打包体积,次要是晋升产品的应用体验,升高服务器资源老本,更快的页面加载,将让产品显得更加“丝滑”,同时也能够让打包更快。

webpack 打包速度优化

webpack 进行打包速度优化有七种罕用伎俩:

优化 loader 搜寻范畴

对于 loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,而后对 AST 持续进行转变 最初再生成新的代码,我的项目越大,转换代码越多,效率就越低。优化正则匹配、应用 include 和 exclude 指定须要解决的文件, 疏忽不须要解决的文件。

rules: [{ 
    // 优化正则匹配 
    test: /\.js$/, 
    // 指定须要解决的目录 
    include: path.resolve(__dirname, 'src') 
    // 实践上只有 include 就够了,然而某些状况须要排除文件的时候能够用这个,排除不须要解决文件 
    // exclude: []}]
多过程 / 多线程

受限于 node 是单线程运行的,所以 webpack 在打包的过程中也是单线程的,特地是在执行 loader 的时候,长时间编译的工作 很多,这样就会导致期待的状况。咱们能够应用一些办法将 loader 的同步执行转换为并行,这样就能充分利用系统资源来进步打包速度了。

{ 
    test: /\.js?$/,
    exclude: /node_modules/, 
    use: [ 
        { 
            loader: "thread-loader", 
            options: {workers: 3 // 过程 3 个} 
            },
            { loader: "babel-loader", 
            options: {presets: ["@babel/preset-env"], 
            plugins: ["@babel/plugin-transform-runtime"] 
            } 
        } 
    ] 
},
分包

在应用 webpack 进行打包时候,对于依赖的第三方库,比方 vue,vuex 等这些不会批改的依赖,咱们能够让它和咱们本人编写 的代码离开打包,这样做的益处是每次更改我本地代码的文件的时候,webpack 只须要打包我我的项目自身的文件代码,而不会再 去编译第三方库,那么第三方库在第一次打包的时候只打包一次,当前只有咱们不降级第三方包的时候,那么 webpack 就不会 对这些库去打包,这样能够疾速进步打包的速度。因而为了解决这个问题,DllPlugin 和 DllReferencePlugin 插件就产生了。这种 形式能够极大的缩小打包类库的次数,只有当类库更新版本才须要从新打包,并且也实现了将公共代码抽离成独自文件的优化 计划

// webpack.dll.conf.js 
const path = require('path')
const webpack = require('webpack')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
    mode: 'production',
    devtool: false,
    entry: {vue: ['vue', 'vue-router', 'iscroll', 'vuex'],
    },
    output: {path: path.join(__dirname, '../dist'),
        filename: 'lib/[name]_[hash:4].dll.js',
        library: '[name]_[hash:4]'
    },
    performance: {
        hints: false,
        maxAssetSize: 300000, // 单文件超过 300k,命令行告警 maxEntrypointSize: 300000, // 首次加载文件总和超过 300k,命令行告警 
    }, optimization: {
        minimizer: [
            new UglifyJsPlugin({parallel: true // 开启多线程并行})
        ]
    },
    plugins: [
        new webpack.DllPlugin({
            context: __dirname,
            path: path.join(__dirname, '../dist/lib', '[name]-manifest.json'),
            name: '[name]_[hash:4]'
        })
    ]
}

// webpack.prod.cong.js 
plugins: [
    new webpack.DllReferencePlugin({context: __dirname, manifest: require('../dist/lib/vue-manifest.json')
    }),
]
开启缓存

当设置 cache.type:“filesystem”时,webpack 会在外部以分层形式启用文件系统缓存和内存缓存,将处理结果结存放到内存中,下次打包间接应用缓存后果而不须要从新打包。

cache: {
    type: "filesystem"
    // cacheDirectory 默认门路是 node_modules/.cache/webpack 
    // cacheDirectory: path.resolve(__dirname, '.temp_cache') 
},
打包剖析工具

显示测量打包过程中各个插件和 loader 每一步所耗费的工夫, 而后让咱们能够有针对的剖析我的项目中耗时的模块对其进行解决。

npm install speed-measure-webpack-plugin -D

// webpack.prod.config.js
const SpeedMeatureWebpackPlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeatureWebpackPlugin();

var webpackConfig = merge(baseWebpackConfig,{})
--> 批改为上面格局
var webpackConfig = {...}

module.exports = webpackConfig
--> 批改为上面格局
module.exports = smp.wrap(merge(baseWebpackConfig, webpackConfig));
ignorePlugin

这是 webpack 内置插件, 它的作用是疏忽第三方包指定目录,让这些指定目录不要被打包进去,避免在 import 或 require 调用时,生成以下正则表达式匹配的模块。

  • requestRegExp 匹配(test)资源申请门路的正则表达式。
  • contextRegExp(可选)匹配(test)资源上下文(目录)的正则表达式。
new webpack.IgnorePlugin({resourceRegExp: /^\.\/test$/, contextRegExp: /test$/,})
优化文件门路
  • alias:省下搜寻文件的工夫,让 webpack 更快找到门路
  • mainFiles:解析目录时要应用的文件名
  • extensions:指定须要查看的扩展名,配置之后能够不必在 require 或是 import 的时候加文件扩展名, 会顺次尝试增加扩展名进行 匹配
resolve: {extensions: ['.js', '.vue'],
        mainFiles: ['index'],
        alias: {'@': resolve('src'), }
    }

webpack 打包体积优化

webpack 打包体积优化有 11 种罕用优化伎俩

构建体积剖析

npm run build 构建,会默认关上:http://127.0.0.1:8888/,能够看到各个包的体积, 剖析我的项目各模块的大小,能够按需优化。

npm install webpack-bundle-analyzer -D

const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

plugins:[new BundleAnalyzerPlugin() 
]
我的项目图片资源优化压缩解决

对打包后的图片进行压缩和优化,升高图片分辨率,压缩图片体积等

npm install image-webpack-loader -D

// webpack.base.conf.js

{test: /\.(gif|png|jpe?g|svg|webp)$/i,
    type: "asset/resource",
    parser: {dataUrlCondition: { maxSize: 8 * 1024} },
    generator: {filename: "images/[name].[hash:6][ext]" },
    use: [{
        loader: "image-webpack-loader",
        options: {mozjpeg: { progressive: true, quality: 65},
            optipng: {enabled: false},
            pngquant: {quality: [0.5, 0.65], speed: 4 },
            gifsicle: {interlaced: false},
            webp: {quality: 75}
        }
    }]
} 
删除无用的 css 款式

有时候一些我的项目中可能会存在一些 css 款式被迭代废除,须要将其删除,能够应用 purgecss-webpack-plugin 插件,该插件能够去 除未应用的 css。

npm install purgecss-webpack-plugin glod -D

// webpack.prod.conf.js
const PurgeCSSPlugin = require("purgecss-webpack-plugin");

const glob = require('glob')

const PATHS = {src: path.join(__dirname, 'src') }

// plugins 
new PurgeCSSPlugin({paths: glob.sync(`${PATHS.src}/**/*`, {nodir: true}), 
    safelist: ["body"] 
}),
代码压缩

对 js 文件进行压缩,从而减小 js 文件的体积,还能够压缩 html、css 代码。

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

optimization:{
        minimize: true, // 代码压缩 
        usedExports: true, // treeshaking
        minimizer: [ 
            new TerserPlugin({ 
                terserOptions: { 
                    ecma: undefined, 
                    parse: {},
                    compress: {}, 
                    mangle: true, // Note `mangle.properties` is `false` by default. 
                    module: false, // Deprecated 
                    output: null, 
                    format: null, 
                    toplevel: false,
                    nameCache: null, 
                    ie8: false,
                    keep_classnames: undefined,
                    keep_fnames: false, 
                    safari10: false
                } 
            })],
            splitChunks: { 
                cacheGroups: { 
                    commons: { 
                        name: "commons", 
                        chunks: "initial", 
                        minChunks: 2
                    } 
                } 
            } 
        }

应用 UglifyjsWebpackPlugin 插件对 js 进行压缩,CssMinimizerWebpackPlugin 对 css 进行压缩。

module.exports = {
    optimization: {
        minimize: true,
        minimizer: [new UglifyJsPlugin({sourceMap:true}),
            new CssMinimizerPlugin()]
    }
}
开启 Scope Hoisting

Scope Hoisting 又译作“作用域晋升”。只需在配置文件中增加一个新的插件,就能够让 webpack 打包进去的代码文件更小、运行 的更快, Scope Hoisting 会剖析出模块之间的依赖关系,尽可能的把打包进去的模块合并到一个函数中去,而后适当地重命名一 些变量以避免命名抵触。

new webpack.optimize.ModuleConcatenationPlugin();
提取公共代码

将我的项目中的公共模块提取进去,能够缩小代码的冗余度,进步代码的运行效率和页面的加载速度

new webpack.optimize.CommonsChunkPlugin(options)
代码拆散

代码拆散可能将工程代码拆散到各个文件中,而后按需加载或并行加载这些文件,也用于获取更小的 bundle,以及管制资源加 载优先级, 在配置文件中配置多入口,输入多个 chunk。

// 多入口配置 最终输入两个 chunk
 module.exports = { 
    entry: { 
        index: 'index.js', 
        login: 'login.js' 
    },
    output: {// 对于多入口配置须要指定 [name] 否则会呈现重名问题 
        filename: '[name].bundle.js', 
        path: path.resolve(__dirname, 'dist') 
    } 
};
Tree-shaking

treeshaking 是一个术语,通常用于形容移除 JavaScript 上下文中的未援用代码(dead-code)。它依赖于 ES2015 模块语法的 静 态构造 个性,例如 import 和 export。

在 ES Module 中,通过解构的形式获取办法,会默认触发 TreeShaking,代码会主动革除无用代码。前提是调用的库必须应用 ES Module 的标准。同一文件的 TreeShaking 必须配置 mode=production。肯定要留神应用解构来加载模块。

CDN 减速

CDN 的全称是 Content DeliveryNetwork,即内容散发网络。CDN 是构建在网络之上的内容散发网络,依附部署在各地的边缘 服务器,通过核心平台的负载平衡、内容散发、调度等功能模块,使用户就近获取所需内容,升高网络拥塞,进步用户拜访响 应速度和命中率。CDN 的关键技术次要有内容存储和散发技术。在我的项目中以 CDN 的形式加载资源,我的项目中不须要对资源进行 打包,大大减少打包后的文件体积。

按需加载

在开发我的项目的时候,我的项目中都会存在十几甚至更多的路由页面。如果咱们将这些页面全副打包进一个文件的话,尽管将多个请 求合并了,然而同样也加载了很多并不需要的代码,消耗了更长的工夫。那么为了页面能更快地出现给用户,咱们必定是心愿 页面能加载的文件体积越小越好,这时候咱们就能够应用按需加载,将每个路由页面独自打包为一个文件。以下是常见的按需 加载的场景

  • 路由组件按需加载
  • 按需加载需引入第三方组件
  • 对于一些插件,如果只是在个别组件中用的到,也能够不要在 main.js 外面引入,而是在组件中按需引入
生产环境敞开 sourceMap

sourceMap 实质上是一种映射关系,打包进去的 js 文件中的代码能够映射到代码文件的具体位置, 这种映射关系会帮忙咱们间接 找到在源代码中的谬误。但这样会使我的项目打包速度减慢,我的项目体积变大,能够在生产环境敞开 sourceMap

相干文章

  • [万字总结] 一文吃透 Webpack 外围原理
  • 【万字】透过剖析 webpack 面试题,构建 webpack5.x 常识体系
退出移动版