我是靠谱客的博主 包容大神,最近开发中收集的这篇文章主要介绍JavaScript打包之Webpack打包:从开发到生产Webpack:打包所有资源,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

打包:从开发到生产

  • 解决ES Module的浏览器兼容问题,编译ES6+到ES5
  • 解决模块请求频繁的问题,模块打包成一个文件
  • 资源都需要模块化,管理所有资源的依赖关系,便于业务代码与相关资源的统一维护

打包工具

作用:使用模块化为开发环境提供便利,使用打包为生产环境提供效率。

Webpack:打包所有资源

webpack的执行是node环境,因此相对路径必须要用./开头,不能省略。

配置文件

webpack会运行配置文件,拿到配置文件导出的配置对象,有多种导出配置对象的方式

  • 直接导出配置对象module.exports = {};
  • 导出一个函数module.exports = (env, argv) => { return configration };,函数内部返回一个配置对象,或者返回一个Promise(用于异步加载配置对象)
  • webpack支持导出多个配置对象,以数组的形式提供module.exports = [// 这里是多个配置对象],每个配置对象都提供一次打包。

配置对象的基本字段

  • entry:配置入口文件,如果是相对路径,则前面的./不能省略,省略./的一般都认为是内置模块或第三方模块
  • output:配置输出文件的信息
    • filename:打包输出的文件名
    • path:打包输出的路径,必须是绝对路径,通常使用path.join或path.resolve。注意,output.path和publicPath完全是两个概念,publicPath是指定资源的发布路径(即网络引用该资源的URL路径),而output.path是指定打包后在本机上的路径。
    • publicPath:标识HTML文件加载外部资源时的访问路径,外部资源(如图片)被导入时的变量值为publicPath + 生成的资源文件名的拼接值。通常如果需要用到CDN服务,则会更改为CDN路径。
      • 另外,像file-loader等拷贝资源的loader,也可以使用publicPath选项来指定资源被引用的URL路径部分,换句话说,publicPath影响的是资源被导入时的路径变量。file-loader等还可以使用outputPath来指定图片等打包输出的路径,也就是实际打包后图片在本地的存储路径。
  • mode:指定打包的工作模式,可以是‘production’、‘development’和‘none’,默认为’production’。

关于mode

  • development
    • 会将 process.env.NODE_ENV 的值设为 development。
    • 启用 NamedChunksPlugin 和 NamedModulesPlugin。
  • production
    • 会将 process.env.NODE_ENV 的值设为 production。
    • 启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin.

NamedModulesPlugin
在使用模块热替换时很有用。有了NamedModulesPlugin,我们能够看到被替换模块的相对路径,否则只能看到ID。
HMR描述
NamedChunksPlugin
和NamedModulesPlugin类似。不仅模块能看到名字,chunk也能。当应用在浏览器中运行起来时,可以在window.webpackJsonp属性中查看它们。
NamedModulesPlugin和NamedChunksPlugin的好处
使用NamedModulesPlugin和NamedChunksPlugin的一个额外好处是,当添加和删除依赖时,打包不再需要使用模块的顺序id。因为这些id和名字会在最终的输出产物中使用,修改它们会导致文件哈希值的变化,即使这些文件使用的模块本身并没有改变。使用以上两个插件会帮助你处理浏览器的缓存问题。

打包JavaScript模块后的代码浅析

  • 打包后的代码是一个IIFE,其中参数为一个数组,数组元素都是函数,这些函数内部是模块的代码,这里暂时将这些函数称为模块函数。
  • IIFE内部主要是定义了function __webpack_require__(moduleId)这个函数,以及围绕这个函数挂载的其他辅助属性和方法,其中moduleId对应数组的索引,最后返回return __webpack_require__(__webpack_require__.s = 0),即用该函数加载入口文件。
    • function __webpack_require__(moduleId)函数用于执行数组中moduleId对应的函数,返回module.exports对象。
    • 模块函数执行的时候,又分为两种情况
      • 如果有导入,则再调用function __webpack_require__(moduleId)去执行依赖的模块函数,然后使用变量记录依赖的导出成员,接着执行导出成员的代码。
      • 如果有导出,则将导出的成员挂载到module.exports对象上。

资源加载器loader

常用loader分类

编译转换类

将资源模块转换为JavaScript代码,如css-loader

文件操作类

将资源模块拷贝到输出目录,并导出文件的访问路径,如file-loader

代码检查类

资源模块进行代码风格校验,统一项目的代码风格,如eslint-loader

二进制资源文件加载器

webpack5中已经内置了静态文件资源的支持,不再需要file-loader、url-loader等的配置了,只需要添加如下配置

{
  test: /静态文件的正则匹配表达式/,
  type: 'assets'
}

file-loader

二进制文件如图片、字体等静态资源通过file-loade加载。实际上是将这些资源一起打包移动到了dist目录下,同时将导入资源的路径更新为publicPath + 文件名。

  • output:{ publicPath: 'dist/' }用publicPath字段来标识HTML文件加载外部资源时的访问路径,外部资源(如图片)被导入时的变量值为publicPath + 生成的资源文件名的拼接值。通常如果需要用到CDN服务,则会更改为CDN路径。
  • 有的时候图片等打包后在html里面img后src为“[object Module]”,此时需要设置file-loader的options.esModule: false
  • 像file-loader等拷贝资源的loader,也可以使用publicPath选项来指定资源被引用的URL路径部分,换句话说,publicPath影响的是资源被导入时的路径变量。file-loader等还可以使用outputPath来指定图片等打包输出的路径,也就是实际打包后图片在本地的存储路径。
  • 如果只配置了outputPath选项,则资源的引用路径会使用outputPath选项。
// index.js
import img from './images/xxx.jpg'
// 先通过'./images/xxx.jpg'拿到图片,然后将img更新为新生成的文件名(一般都是一些MD5之类的哈希名),最后将publicPath + img做一个拼接。因此,publicPath的'dist/'最后的斜线不能省略

url-loader

  • 使用Data URLs的方式(base64编码)来表示二进制文件。如果文件体积较大,则仍然使用文件URL类型来引用文件(这里还是会使用file-loader来加载资源,因此file-loader仍然要安装),因为base64编码实际上比原文件体积要大1/3左右。

    • 配置url-loader的limit

      // webpack.config.js
      {
          test: /.jpg$/,
          use: {
            loader: 'url-loader',
            options: {
              // 超过10KB的文件则不使用base64编码作为data:urls存在
              limit: 10 * 1024 // 10KB,因为limit单位是Byte
            }
          }
        }
      
  • 浏览器可以直接解析Data URLs为二进制文件,如可以直接在地址栏输入Data URLs。

编译ES6+:babel-loader

webpack默认可以处理ES Module,但ES6+的其他语法无法编译。

babel-loader编译ES6+

关于babel的使用可以参考这篇博客
安装以下npm包

  • babel-loader:加载并编译js文件,但编译依托于下面的包
  • @babel/core:babel的核心模块
  • @babel/preset-env:babel的ES6+插件预设
  • core-js@3:JavaScript标准库,一般用来对浏览器环境进行polyfill,要安装在dependencies中。由@babel/preset-env将polyfill的代码注入到编译后的代码中。

关于@babel/preset-env

@babel/preset-env可以根据对browserslist的配置,在编译时自动根据我们对目标运行环境的最低版本要求,采用智能化方式编译。

  • 如果我们设置的最低版本的环境,已经原生实现了要编译的ES特性,则会直接采用ES标准写法;
  • 如果最低版本环境,还不支持要编译的特性,则会自动注入对应的polyfill,这时候需要用到core-js@3

关于browerslist的集成

  • 官方推荐使用.browserslistrc文件作为browserslist的配置文件
  • 或者在package.json中配置browserslist字段
  • 或者在preset-env的options的target option,单独为babel配置browserslist,其实这种方式值得推荐,因为并非所有的工具都和babel具有一样的browserslist需求

关于配置browserslist

  • 具体配置方法参考官网

  • browserlist字符串,可以通过npx browserlist string来检测

    // 配置preset-env的目标运行环境target示例
     {
      "target": {
        "iOs": "8",
        "Android": "4"
      }
    }
    

配置babel-loader

{
  test: /.js$/,
  // 一定要记得排除掉这些文件夹里的js文件,以及其他不参与babel转换的文件
  exclude: /(node_modules|bower_components)/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: [
        [
          '@babel/preset-env', 
          {
            useBuiltIns: "usage",
            corejs: 3,
            // modules用来将ES Module编译为其他模块规范
            // 在使用tree-shaking的时候,这里要设为false表示不编译
            modules: false
          }
        ]
      ]
    }
  }
},

加载HTML模块:html-loader

  • 使用html-loader加载。
  • HTML文件同样可以作为模块被加载,但html-loader在将HTML文件作为模块导出时是将HTML内容作为字符串导出。
  • 默认html-loader只会处理<image>中的src属性,将属性值作为模块参与打包。
  • 如果想要其他标签的其他属性也作为模块触发webpack的打包,则需要进一步配置html-loader。如下文,将<a href>的href指定的资源也会参与打包。
// footHtml是一段html字符串
import footHtml from './foot.html'
// html-loader的配置
{
  test: /.html$/,
  use: {
    loader: 'html-loader',
    options: {
      // 默认attrs: ['img:src']
      attrs: ['img:src', 'a:href']
    }
  }
}

哪些资源会被打包

  • webpack支持各种模块规范,包括ES Module、CommonJS、AMD、CMD、UMD等,但尽量不要在一个项目中使用多种规范。
  • 注意:使用CommonJS规范导入ES Module时,ES Module默认导出的成员需要使用require().default来接收
const defaultMem = require('./lib/main.js').default;
  • 除了JavaScript模块规范外,某些形式的语法同样会被认定为资源模块,会参与webpack的打包。
    • CSS语法@import导入其他CSS文件
    • CSS语法url()导入图片等资源
    • html语法src属性或其他属性(需要针对性配置),如<image src>等

资源参与打包的形式

  • JavaScript类型资源直接遵循JavaScript模块规范
  • CSS类型资源,由于没有指定的导入导出规范,所以一般情况下只需要导入即可。如果开启了CSS Module,则可以导入CSS资源时,指定导入值,作为选择器的命名空间。
  • HTML资源,导出的是HTML文件内容字符串
  • 文件资源,要么作为data:url直接作为代码,要么拷贝到输出目录,导出的是该资源的访问路径。

webpack核心工作原理

  • 入口文件开始,分析依赖关系,得到依赖树
  • 递归依赖树,按依赖关系找到资源文件,对应的loader来加载资源,统一打包到bundle文件中。

Loader工作原理

  • loader模块需要导出一个函数,即对资源的处理过程。参数为资源文件的内容,返回值为处理结果
    • 如果loader处理资源后交给webpack默认的JavaScript加载器,则返回值必须是有效的JavaScript代码(字符串类型)或者Buffer。因为在打包结果中,loader的返回值会被放在一个JavaScript函数中。

    • 经过loader处理的资源,会被放在一个模块函数中执行。如果loader的返回值(作为JavaScript代码片段)不对外导出成员,则外部拿到的成员为webpack所默认定义的导出成员。对外导出可以使用CommonJSmodule.exports = ...或者ES Moduleexport default的语法,这样外部才能使用该资源对外导出的成员。

      // index.js中引入about.md文件, about为about.md经过loader处理后导出的成员
      import about from './about.md'
      

      如果loader处理后没有使用导出语法,则about变量则成为了webpack内部默认的导出成员。如果loader处理后使用了导出语法,则about变量拿到的就是loader处理后导出的成员。
      如果不需要导出成员,则loader处理后的代码也就不需要使用导出语法了。

    • 如果是loader数组,则一个loader处理资源后,交给下一个loader继续处理,则对返回值没有要求,具体情况看下一个loader要求的输入类型。但最后一个loader处理后必须是JavaScript代码。

  • loader处理是类似管道的过程,从指定的loaders数组最后一个loader开始,依次执行loader,最后交给webpack打包。

插件机制

  • 增强webpack打包过程中的自动化能力,在打包过程生命周期内调用,是一种Hook机制(可以达到构建项目能力)。
  • 绝大多数插件都导出一个类,在使用插件时实例化即可。

常见插件

自动清除目录clean-webpack-plugin

  • 在配置文件中导入const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  • 在配置文件中的plugins字段中配置该插件plugins: [new CleanWebpackPlugin()],默认清理打包输出目录。

自动生成加载打包结果的HTML文件:html-webpack-plugin

  • 导入const HtmlWebpackPlugin = require('html-webpack-plugin');
  • 配置:
plugins: [
  // HtmlWebpackPlugin支持很多对HTML的配置选项
  // 动态数据可以在模板中使用lodash模板语法输出
  new HtmlWebpackPlugin({
    template: './index.html',
    title: '首页'
  })
]

模板文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Webpack Learn</title>
</head>
<body>
  <p>webpack learn</p>
  <h1><%= htmlWebpackPlugin.options.title %></h1>
</body>
</html>
  • 同时输出多个HTML文件
plugins: [
  // 生成index.html文件的HtmlWebpackPlugin实例
  // 默认的filename是index.html
  new HtmlWebpackPlugin({
    template: './index.html',
    title: '首页'
  }),
  // 生成about.html文件的HtmlWebpackPlugin实例
  new HtmlWebpackPlugin({
    filename: 'about.html',
    template: './index.html',
    title: '关于'
  }),
]

拷贝静态资源文件:copy-webpack-plugin

针对不参与打包的静态资源文件,在生产环境的构建过程中需要参与,使用copy-webpack-plugin拷贝到打包输出目录下。

plugins: [
  new CopyWebpackPlugin({
    patterns: [{
      from: 'public'
    }]
  })
]

插件工作原理

  • 插件机制是在webpack打包编译的过程中某个时刻调用函数来做一些工作。
  • 插件处理函数(handler)注册到编译过程中的不同事件点上运行的生命周期钩子函数上。 当执行每个钩子时, 插件能够完全访问到编译(compilation)的当前状态,然后做一些处理工作。
  • 插件通常是一个函数或者拥有apply方法的对象,一般将插件暴露为一个类,类内部拥有一个apply方法来调用函数。使用插件时实例化即可。
    • compiler: apply(compiler) {} ,打包过程的所有配置信息
// 自定义移除webpack打包结果的注释的插件
class MyPlugin {
  apply(compiler) {
    // 插件处理函数注册到emit事件
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // 打包过程中的上下文compilation,包含了此次打包过程中的所有资源
      for(const name in compilation.assets) {
        // name是资源文件名
        // console.log(name)
        // assets[name]是描述资源的对象,其中的source()方法获取资源内容,size方法获取资源的大小
        // console.log(compilation.assets[name].source())
        if(name.endsWith('.js')) {
          const contents = compilation.assets[name].source();
          // 替换注释为空字符串
          const withoutCommments = contents.replace(//**+*//g, '');
          // 重新替换资源对象
          compilation.assets[name] = {
            source: () => withoutCommments,
            size: () => withoutCommments.length
          }
        }
      }
    })
  }
}

webpack增强开发体验

自动编译

watch工作模式

监视文件变化,自动重新打包
使用npx webpack --watch命令

自动刷新浏览器

  • 可以使用browser-sync启动服务器,监控webpack打包输出目录的文件变化。但这种方式一方面使用了两个工具,不太方便,另一方面webpack要写入文件到磁盘,browser-sync要从磁盘读文件,多了两步磁盘读写操作,降低了速度。

webpack-dev-server

  • 特点

    • 集成自动编译
    • 集成自动刷新浏览器
    • 打包构建结果在内存中,而不输出到硬盘
  • 安装npm install webpack-dev-server -D,会在node_modules/.bin目录下有webpack-dev-server的CLI工具

  • 使用npx webpack-dev-server命令,会自动使用webpack打包项目,同时监听源代码的变化,便于重新打包。

  • 使用npx webpack-dev-server --open命令,自动打开浏览器。

服务器资源的路径配置

  • 默认将打包构建结果的文件作为开发服务器的资源文件,只要参与打包构建的文件都可以被访问到。如果没有参与打包的静态资源也需要作为开发服务器的资源被访问,需要在webpack的配置中添加devServer字段做如下配置:
devServer: {
    // contentBase告诉服务器serve的文件路径,推荐使用绝对路径
    // 多个路径放在一个数组里
    contentBase: [ path.join(__dirname, 'public') ]
  },

服务器的代理API 配置

  • 代理后端API服务
    • 开发阶段由于运行在开发服务器localhost下,如果使用XHR请求后端接口地址,就会出现两种情况:
    1. 使用相对路径,则请求是针对localhost而言的,显然localhost没有API服务。
    2. 使用绝对路径,就会造成跨域问题。
    • 针对这种情况,就需要使用代理服务,将后端API的请求由开发服务器代理转发,这意味着在开发时,使用真实的API请求地址(这种请求地址一般只需要路径部分,而不需要域名部分),开发服务器将该API请求进行转发到运行该API服务的真实服务器上。
  • dev-server 使用了非常强大的 http-proxy-middleware 包

配置代理

devServer: {
  proxy: {
    // 这里的/api就是一个请求路径部分
    '/api': {
      // 所以请求表面上是http://localhost:8080/api/users -> 实际上会被代理转发给https://api.github.com/api/users
      // 目标主机
      target: 'https://api.github.com',
      // 有的时候需要重写目标路径中的某些部分,其中'^/api'是用正则的形式来匹配路径,替换为''
      // 之后就成为http://localhost:8080/api/users -> https://api.github.com/users
      pathRewrite: {
        '^/api': ''
      },
      // 将XHR请求的Origin字段(或者host字段?)重写为目标主机的域名,而不是localhost
      // 如果不重写,那代理直接转发请求,则请求的Origin字段就是http://localhost:8080
      // 如果后端服务器是虚拟主机,那无法认定请求的是哪个域名;
      // 如果是跨域,则localhost无法进入白名单;
      changeOrigin: true,
      // 可能需要配置secure来将HTTP请求转发运行在 HTTPS 上,且使用了无效证书的后端服务器
      secure: false
    }
  }
}

HMR热更新

自动刷新的特点会导致页面状态的丢失,有些时候在开发时需要保存页面状态,从而查看源代码更新后的效果,这时候就需要使用HMR了。
HMR模块热更新:将更新后的模块替换原来的模块,而不需要去刷新浏览器。

  • 目前已经集成在webpack-dev-server中,可以使用webpack-dev-server --hot来开启HMR功能。
  • 在打包时,将热更新功能关闭,打包就会移除热更新替换逻辑的代码(热更新的替换代码必须包裹在if(module.hot) {}才能生效),这种情况适用于生产环境的打包。

开启热更新配置

  • 在devServer中设定hot: truehotOnly: true

  • 配置插件HotModuleReplacementPlugin

    const webpack = require('webpack');
    devServer: {
      hot: true
    }
    plugins: [
      new webpack.HotModuleReplacementPlugin()
    ]
    

热更新规则

  • CSS文件的热更新可以直接显示效果,因为style-loader内部已经内置了CSS文件的热更新替换逻辑,样式文件的热更新替换逻辑很简单,直接替换即可。
  • JavaScript文件的热更新需要手动处理模块热更新的逻辑,因为JavaScript的替换逻辑是复杂的,无法统一替换逻辑。(像诸如react等框架的模块文件要遵守一定规律,可以统一替换逻辑
  • 图片模块的热更新也需要手动处理热更新逻辑,但图片的导入都是路径,因此热更新逻辑就是替换图片的src属性为新的路径(仍旧是导入的那个变量)即可

热更新原理及过程
如图所示

  • 首次打包:1 -> 2 -> A -> B
  • 热更新:1 -> 2 -> 3 -> 4 -> 5
    热更新原理过程图
  • Bundle Server:webpack打包后的结果,通过Bundle Server提供给浏览器bundle.js
  • HMR Server:热更新Server,将热更新的结果提供给浏览器
  • HMR Rumtime:打包结果中包含了热更新的运行逻辑,将其注入到浏览器中,一旦有热更新Server提供的数据,则解析该数据,获取到最新的打包结果。

HMR APIs

在导入模块的文件中使用module.hot.accept()方法来指定模块更新时的逻辑。

  • accept(path, callback):path来指定导入的模块路径,callback指定热更新后的逻辑。
// if(module.hot)用来判断是否开启了HMR(包括devServer.hot = true和引入插件,貌似插件不引入也可以)
// 因为module.hot是webpack.HotModuleReplacementPlugin插件提供的
// 万一没有提供,则可以保证不报错
// 同时可以让生产环境的打包移除热更新替换逻辑的代码
// 如果没有if(module.hot)的判断,打包是不会移除热更新的替换逻辑代码的
if(module.hot) {
  module.hot.accept('./module1.js', () => {})
}

取消自动回退liveReload

  • 在HMR不支持或者HMR替换逻辑代码出问题时,就会自动回退为liveReload自动刷新。这一点对于替换逻辑出问题时就会显得不够友好。
  • 配置取消自动回退liveReload:devServer的hot: true改为hotOnly: true

开发调试

SourceMap

.map文件标准(JSON格式文件)

{
  // 当前文件使用的sourceMap标准的版本
  "version":
  // 源文件组成的数组
  "soruces":
  // 源代码中的变量名
  "names:"
  // 记录转换前后的映射关系
  "mappings"
}

在转换后文件的末尾通过以下语法引入sourceMap文件,就可以通过浏览器的Source面板得到源代码

//# sourceMappingURL=<url>

webpack配置SourceMap

  devtool: 'source-map'

devtool字段有如下的值,分别对应着不同的SourceMap生成方式
devtool与SourceMap

  • eval模式:
    • eval是JavaScript的一个函数,用来执行传入的JavaScript代码。
      eval('console.log("hello world"); //# sourceURL=<url>')
      
    • eval()使用上述//# sourceURL=<url>语法,可以将'console.log("hello world");这段代码指定为属于<url>指定的JS文件。
    • devtool: eval就是将模块代码放入eval()函数中执行,并附上从属的模块地址。但这种模式仅仅只能指明代码所属模块,并不能指明行列信息。
    • devtool: eval-source-map,会生成SourceMap文件,可以指明行列信息。
    • devtool: cheap-eval-source-map,只会指明行信息,但指向的是经过loader处理后的代码
    • devtool: cheap-module-eval-source-map,只会指明行信息,但指向的是最初的源代码
    • devtool: inline-source-map,将SourceMap文件作为data:url放在编译后的代码中。
    • devtool: hidden-source-map,隐藏了SourceMap文件,因此在开发工具中看不到SourceMap的效果(代码中没有引入)。
    • devtool: nosources-source-map,虽然有错误信息,但开发工具中看不到源代码,可以在生产环境中保护源代码。

上文中粗体的关键字表示了SourceMap的不同配置,在其他模式下都是一样的效果。

选择合适的SourceMap配置方式

  • 开发环境:cheap-module-eval-source-map
  • 生产将环境:none或者nosources-source-map

为不同环境创建不同的配置

创建不同配置的方式

配置文件根据环境不同,导出不同的配置对象

一般适用于中小型项目

module.exports = (env, argv) => {
  // env:运行CLI时传入的环境变量
  // argv: 运行CLI时传入的所有参数
  const config = {
  // 这里是开发环境下的配置
  }
  if(env === 'production') {
    config.mode = 'production';
    config.devtool = false;
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin({
        patterns: [{
          from: 'public'
        }]
      })
    ]
  }
}

一个环境对应一个配置文件

一般适用于大型项目

  • 公共配置文件webpack.common.js
  • 开发环境配置文件webpack.dev.js
  • 生产环境配置文件webpack.prod.js

这里需要用到webpack-merge包来进行对象的合并。webpack-merge并不是浅复制(将同名属性覆盖),而是深度复制(同名属性如果是引用类型,则进一步进行深复制,从而把不同对象的各种深层次属性都合并)

// webpack.prod.js
const common = require('./webpack.common');
const { merge } = require('webpack-merge');

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
      patterns: [{
        from: 'public'
      }]
    })
  ]
})

生产环境的打包优化

开箱即用的mode: production模式提供了生产环境下的预设。

DefinePlugin插件

创建一个在编译时可以配置的全局常量。这可能会对开发模式和发布模式的构建允许不同的行为非常有用。如果在开发构建中,而不在发布构建中执行日志记录,则可以使用全局常量来决定是否记录日志。

  • 为代码注入全局常量,在production模式下自动启用。
  • 注入process.env.NODE_ENV全局常量,很多第三方模块会根据NODE_ENV来决定其行为。

规则
每个传进 DefinePlugin 的键值都是一个标志符或者多个用 . 连接起来的标志符。
使用键作为常量名。

  • 如果这个值是一个字符串,它会被当作一个代码片段来使用。
  • 如果这个值不是字符串,它会被转化为字符串(包括函数)。
  • 如果这个值是一个对象,它所有的 key 会被同样的方式定义。
  • 如果在一个 key 前面加了 typeof,它会被定义为 typeof 调用。

因为这个插件直接执行文本替换,给定的值必须包含字符串本身内的实际引号。通常,有两种方式来达到这个效果,使用 ‘“production”’, 或者使用 JSON.stringify(‘production’)

Tree-shaking

将未引用代码从打包代码中去除。

首先需要说明的是,webpack的一个打包规则:模块导出的成员,只有在导入模块中使用了该成员,该成员才会在打包结果中导出。如果没有使用该成员,则模块就不会有导出(这样的结果会被tree-shaking功能给删除掉这个模块)。

分为两个阶段

  • 标记未引用代码:未引用代码即未导入的模块成员和sideEffects的代码
    • optimization.usedExports: true,表示只在代码中导出那些外部导入的模块成员
    • package.json中添加"sideEffects"字段来向webpack的编译器提供提示,说明哪些模块是纯ES6模块,可以安全地在打包中删除sideEffects的代码。
  • 删除未引用代码optimization.minimize: true,实际上开启了uglify插件将未引用代码删除掉。

tree-shaking的实现前提
由webpack打包的模块必须是ES Module。因此,在与babel-loader一起使用的时候,不能让babel-loader去转换ES Module语法。
babel-loader在webpack >= 2时标识了支持ESM 和 dynamic import,因此在@babel/preset-env的配置中,不配置modules字段即可,或者配置为**modules: false**

开启Tree-shaking功能的配置

配置optimization字段来开启webpack的优化功能

optimization: {
  // 打包结果中不再导出那些未导入的模块成员
  usedExports: true,
  // 将所有源代码中参与打包的JS模块打包后,合并在一个模块函数中
  // 因此打包结果中这些模块不再是多个模块函数。
  // 而成为一个模块函数,又称为作用域提升(因为放在一个函数里了嘛)
  concatenateModules: true
  // 删除未引用代码
  minimize: true
}

配置sideEffects

sideEffects:指的是模块除了导出成员外的其他行为。
sideEffects一般用于npm包标记是否具有副作用。

  • 在package.json中配置sideEffects字段
  • 在webpack的配置文件中配置optimization.sideEffects: true,webpack会在打包时,根据package.json中的sideEffects字段判断哪些模块不包含副作用,就会删除哪些模块中的副作用代码。
// package.json
// 表示所有模块的代码都不包含副作用,可以安全删除那些副作用代码
{ 
  "sideEffects": false 
}
// webpack的配置文件
// 打包时删除副作用代码
optimization: {
  sideEffects: true
} 

如果要保留一部分副作用模块,则需要标记sideEffects为数组,如css模块、Polyfill等。数组方式支持相关文件的相对路径、绝对路径和 glob 模式,可以用通配符*

// package.json
{
  "sideEffects": ['.src/**/*.css', '.node_modules/**']
}

以上关于tree-shaking的配置在生产模式下自动开启

Tree-shaking的局限性

webpack的tree-shaking仅仅是从模块顶层作用域来判断导入和导出有哪些变量被使用了,将未使用的导出成员去除,这也是利用了ES6模块的静态语法,可以做静态分析。也就是说,只有在模块顶层作用域内声明的函数或变量才能被webpack的tree-shaking分析到。

因为webpack是先加载资源,再根据静态分析的结果删除无用代码。因此,只要资源被导入后使用了,该资源就会被打包。在删除未使用成员和副作用代码时,如果未使用成员或副作用代码使用了导入的模块,则该模块会被webpack打包到结果中,但实际上该模块未被使用,这样就造成了打包多余的模块

tree-shaking在做静态分析时,是无法标记副作用代码和函数内部代码对于模块的使用,因此如果使用了模块,则该模块会被打包

因此,tree-shaking需要增加一项功能,就是对未使用成员或副作用代码使用的其他模块也一并从打包结果中删除。

tree-shaking做不到作用域的深度遍历来分析导入导出的成员,从而决定最终打包的是哪些代码。

例如下面这样的情况

// index.js
import { func2 } from '../common/util';

var a = func2(222);

alert(a);
// commom/util
import lodash from 'lodash-es';

// func1函数有副作用,因为引用了外部环境的lodash变量
var func1 = function(v) {
  alert('111');
  // 这里的lodash变量是无法分析到的,虽然func1函数不会被打包,但lodash库会被打包
  // 实际上,lodash库根本没有用到
  return lodash.isArray(v);
}

var func2 = function(v) {
  return v;
};

export {
  func1,
  func2
}

tree-shaking就会无能为力,webpack的tree-shaking会将lodash也一并打包,虽然lodash实际上根本没用上。
之所以会产生这样的情况,原因在于webpack的tree-shaking并不是深度的作用域遍历。

webpack-deep-scope-plugin:深度遍历作用域的tree-shaking插件

// webpack.config.js
const WebpackDeepScopeAnalysisPlugin = require('webpack-deep-scope-plugin').default;

module.exports = {
  // 不知道为什么,如果在development下配置webpack的tree-shaking,
  // 深度作用域就不会生效。
  // 应该是webpack的tree-shaking和webpack-deep-scope-plugin不能共存?
  mode: 'production',
  plugins: [
    ...,
    new WebpackDeepScopeAnalysisPlugin()
  ]
}

代码分割

  • 打包到一起的代码按照一定规则分割为几个包,然后按需加载。

  • 分包使用的是SplitChunksPlugin插件,直接在optimization字段中配置。

SplitChunksPlugin

基于以下的规则才会分包(默认配置):

  • chunks: 'async',默认只对动态导入做分包。
  • 被共享的模块或者是来自node_modules中的模块
  • 且模块的大小超过30KB(在压缩前)
  • 当按需加载包时,最大并行请求数不超过6
  • 当首屏加载包时,最大并行请求数不超过4

后面两个是因为HTTP/1.1要求一个域最多维持6个HTTP永久连接。
SplitChunksPlugin的默认配置是为了达到web的最佳性能。

多页面多入口打包

注意,在new HtmlWebpackPlugin插件中要用chunks属性指定要注入的打包文件。
注意chunks数组内的成员与entry的属性名对应。

{
  entry: {
    index: './src/index.js',
    about: './scr/pages/about.js'
  }
  
}
new HtmlWebpackPlugin({
  chunks: ['index']
})
new HtmlWebpackPlugin({
  chunks: ['about']
})

提取公共模块为单个包

{
  optimization: {
    splitChunks: {
      // 将所有的公共模块分割称为单独的包
      chunks: all
    }
  }
}

动态导入

  • 按需加载,首屏优化
  • 所有动态导入的模块都会被自动分包,webpack内部使用promise来创建<script>,异步加载被分包的模块

语法

// import(path).then
// import返回一个Promise
import('./pages/about/about.js')
  .then((module) => {})

魔法注释

给动态导入的模块分包时命名
语法形式:在导入模块的路径前加入/* webpackChunkName: '<chunk-name>' */

import(/* webpackChunkName: 'about' */'./pages/about/about.js')
  .then((module) => {})

prefetch和preload

预加载资源:在页面加载好之后,网络空闲时,将未来需要加载的文件提前使用空闲的网络下载到本地,这样可以加快页面与用户交互的响应。
同样使用import动态导入和魔法注释,只是魔法注释是为了预加载,而不是命名分包。

prefetch方式
这种方法会在页面的<head>中加入<link rel="prefetch" href="login-modal-chunk.js">标签,让浏览器在网络空闲时下载链接指向的文件,不管下载是否完成,都不会在当前页面去执行该JavaScript文件。

import(/* webpackPrefetch: true */ 'LoginModal')

preload方式
与prefetch使用相同,但加载时机不同,有以下的不同点

  • preload标识的文件与父文件并行加载,而prefetch则是父文件加载好之后且网络空闲才加载。
  • preload标识的文件拥有中等优先权,是立即加载的,且要在当前页面去执行。
  • preload标识的文件由父文件立刻发出请求去加载,prefetch则在未来的某个时刻加载。
  • 浏览器的兼容性不同。

CSS代码的生产优化

CSS代码的提取

提取插件mini-css-extract-plugin

配置方式

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module: {
  rules: [
    {
      test: /.css$/,
      // 注意,这里原来的style-laoder不再需要了,因为CSS提取出来通过<link>引入
      use: [ MiniCssExtractPlugin.loader, 'css-loader']      
    },
  ]
}
plugins: [
  new MiniCssExtractPlugin()
]

CSS文件的压缩

生产模式下,webpack会自动压缩代码,但仅限于javascript文件,其他资源文件需要额外插件支持。

CSS文件使用optimize-css-assets-webpack-plugin插件来压缩。

配置方式

  • 在plugins中配置,这样不管什么环境,都可以压缩CSS代码。
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
plugins: [
  new OptimizeCssAssetsWebpackPlugin()
]
  • 也可以在optimization中配置,webpack建议像这些压缩插件在optimization.minimizer中配置,方便集中维护。
  • 但只有在生产模式下,minimizer才会开启,压缩才能成功
  • 另外,如果使用minimizer开启压缩,那么自定义的minimizer会覆盖webpack的默认minimizer配置,所以还需要添加JavaScript文件的压缩插件terser-webpack-plugin
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
optimization: {
  minimizer: [
    new OptimizeCssAssetsWebpackPlugin(),
    new TerserWebpackPlugin()
  ]
}

输出文件的Hash

  • 在生产模式下,为输出的文件名使用Hash。
  • 这样如果客户端的缓存时间很长,也不会担心如果静态资源更新后,客户端拿不到新的资源。
  • 原因在于,更新后的文件名变化了,从HTML文件中引入的那些静态资源名变化了,客户端需要重新请求这些变化了的资源。

配置方式

webpack和绝大多数插件都支持为filename属性使用占位符的方式。
Hash有三种配置值

  • [hash]:这种hash值指的是本次webpack打包的hash值。由于每次webpack打包的hash值都不同,因此即使文件内容不变,hash值也都会变化。
  • [chunkhash]:这种hash值指的是同一路打包的hash值,也就是说,hash值与chunk相关。这里的chunk指的就是webpack打包出来的JavaScript代码片段(代码包)。如果是从代码包中提取出来的CSS文件,则同属于一个chunkhash。当使用动态导入分包时,如果更改了动态导入模块的内容,打包时该模块的chunkhash就会变化,从而也会引起导入该模块的JS文件(因为导入该模块的路径变了)也随之chunkhash会变化。
  • [contenthash]:这种hash值指的是模块内容映射的hash值。只有模块内容更新了,该hash值才会更新。[contenthash] 是最适合解决客户端缓存的hash文件名方式。

默认的hash值为20个字符,可以在占位符中使用[contenthash:12]这样的方式来指定生成的hash值为多少个字符。

最后

以上就是包容大神为你收集整理的JavaScript打包之Webpack打包:从开发到生产Webpack:打包所有资源的全部内容,希望文章能够帮你解决JavaScript打包之Webpack打包:从开发到生产Webpack:打包所有资源所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(56)

评论列表共有 0 条评论

立即
投稿
返回
顶部