Webpack4 进阶与实践

Entry 与 Output 的基础配置

经过 《Webpack4 基础入门与实践》 的基础学习,已经了解到应用程序经过 webpack 执行打包的入口 entry 及文件输出 output。它们的默认值分别是 './src'main.jsjavascript

但若是咱们的项目是一个可能会有多个入口文件的多页面的应用。css

entry: {
    home: './home.js',
    about: './about.js',
    contact: './contact.js'
  }
复制代码

那么此时 output 下的配置也要进行相应的更改。最简单的方式就是经过占位符去设置打包输出的文件名。html

output: {
  publicPath: 'http://cdn.com' // 或指定目录 '/assets/',
  filename: '[name].js' // name 占位符
}
// 打包输出 home.js、about.js、contact.js
复制代码

不少时候咱们可能会将打包生成的文件给后端或者将资源托管到 CDN 时。那么咱们在打包生成的 html 文件内去引用这些文件就须要一个指定的 URL 前缀。打包后的文件在 html 内的引用方式会以下所示:前端

<script src="http://cdn.com/about.js"></script>
复制代码

查阅文档,学习更多 entryoutput 的配置参数以及 Output Management 的知识。java

Source Map 的配置

当 webpack 打包源代码时,可能会很难追踪到 error(错误) 和 warning(警告) 在源代码中的原始位置。node

例如,若是将三个源文件(a.js, b.jsc.js)打包到一个 bundle(bundle.js)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会直接指向到 bundle.js。你可能须要准确地知道错误来自于哪一个源文件,因此这种提示这一般不会提供太多帮助。webpack

为了更容易地追踪 error 和 warning,JavaScript 提供了 source map 功能(一个映射关系),能够将编译后的代码映射回原始源代码。git

经过设置 webpack 的 devtool 属性就能够配置 Scource Map。github

// ...
mode: 'development', // 开发环境
devtool: 'source-map', // 默认为 none
复制代码

从新打包后,若是有报错或者警告信息那么就能够经过控制台提示定位到对应位置的代码。web

同时 dist 目录下会增长一个 bundle.js.map 文件。打包后的文件代码与源代码就是经过这个文件进行映射的。

查看文档,webpack 提供了十几种 source map 的配置格式。其中:

  1. inline-source-map

    效果与 source map 一致,区别在于这种方式不会生成 map 文件,而是将其映射关系的内容经过 dataUrl 的方式直接写入到打包后的 bundle.js 文件底部。

  2. inline-cheap-source-map

    上面提到的配置格式,在提示错误和警告信息的时候,会帮咱们精确到项目文件代码中的具体某行某列。

    而加了 cheap 以后,它只会精确到行,而不会精确到哪一列。同时,它会只关心咱们写的业务代码,再也不关心其余部分的代码,因此这种方式的构建过程就会变得比较快。

  3. inline-cheap-module-source-map

    若是咱们在 cheap 的基础上还想让 webpack 为咱们提供 loader、第三方模块等部分代码的错误信息,那么能够加上 module 关键字。

  4. eval

    效果与 source map 的方式是同样的,可是 dist 目录下不会生成 map 文件,同时 bundle.js 文件底部也没有 Base64 的 dataUrl 字段。可是有一个 eval() 方法,因此它是经过 eval 的 js 执行形式来生成 source map 的对应关系的。

    这种方式是执行效率最快,性能最好的方式,但缺点是针对于比较复杂的代码状况下,不能正确的显示行数。

最佳实践:

  • 开发环境 development: cheap-module-eval-source-map
  • 生成环境 production: nonecheap-module-source-map
  • 或者使用 SourceMapDevToolPlugin 进行更细粒度的配置。(切勿和 devtool 选项同时使用 )

模块热更新 Hot Module Replace

在咱们使用 webpack-dev-server 实现代码的热加载以后,每次源代码有改动,这个本地服务器就会自动帮咱们打包并刷新浏览器。这确实很方便,但其实有时候咱们其实却并不但愿它去刷新页面。

好比,咱们在页面上经过不少的点击交互操做,最终在页面显示出一个特定列表。而后为了修改列表项样式,咱们对源代码作了更改。若是此时 webpack-dev-server 帮咱们自动刷新浏览器页面了,那咱们就须要再从新进行一遍点击操做才能看到更改样式后的列表… 😟

此时咱们就可使用 webpack-dev-server 的模块热更新功能 (HMR)。

  1. 修改 devServer 配置项
devServer: {
  port: 8086,
  hot: true, // 开启 hmr 功能
  hotOnly: true // 可选,意思是即便 hrm 不生效,浏览器也不刷新
},
复制代码
  1. 引入 HotModuleReplacementPlugin 插件 (webpack 自带)
// 先引入 webpack
const webpack = require('webpack');
//...
plugins:[
	new HtmlWebpackPlugin({
		template: './src/index.html'
	}),
  // 使用插件
	new webpack.HotModuleReplacementPlugin()
]
复制代码
  1. 重启一下npm start 使修改后的配置文件生效

关于热模块替换的更多用法指南、及实现原理、API 用法能够翻阅如下文档:

指南概念API

Tree Shaking

开发过程当中咱们常常会须要 import 一些外部的公共方法来实现方法复用,但咱们大多数时候都是只须要这个公共方法模块里的几个方法,而不是所有。借助 Tree Shaking,咱们就能够将模块中没有用到的方法摇晃掉。

Tree Shaking 只支持 ES module 这种静态的 import 的模块引入方式,而不支持 common js 动态的 require 引入方式。

配置: 默认的开发模mode: 'development' 是没有 tree shaking 功能的,要想加上 tree shaking 首先在配置文件中加入 optimization 配置项。

{
  plugins: [
    //...
  ],
  optimization: {
    usedExports: true // 只将使用到的导出模块进行打包
  },
}
复制代码

可是这样会可能遗漏掉那些不导出任何内容的模块。实际上,只要 import 引入一个模块,Tree Shaking 就会检查这个模块导出了什么,代码引用了什么,若是没有导出或者没有引用,就会忽略这个模块引入。

好比@babel/poly-fill这种只是单纯地在 window 对象上绑定了一些全局变量而不导出内容的模块,或者是代码里引入的一些 CSS、SCSS 样式文件。

此时要在 package.json 中加入sideEffects配置,将这些须要特殊处理的模块放进一个数组里。

{
  "name": 'webpack-demo',
  "sideEffects": [
    "@babel/poly-fill",
    "*.css",
    "*.scss"
  ]
  // 若是业务逻辑里没有要特殊处理的模块就直接将 sideEffects 设为 false
  // "sideEffects:false"
}
复制代码

其实 Development 模式下,即便咱们配置了 tree shaking ,它也不会将你不用的代码从打包后的 main.js 中剔除掉,而只是在注释中提醒你一下。🌚

这是由于咱们在开发环境生成的代码通常都须要作一些调试,若是 tree shaking 把一些代码删除掉的话,sourceMap 代码对应的一些行数就会错误,因此开发环境下的 tree shaking 还会保留这些代码。可是若是咱们真正的要将项目打包上线,将 mode 改成 production,那么它就会生效了。但同时要注意这时咱们的 devtool 属性在生成环境通常都使用cheap-module-source-map而不是带 eval 的配置。

另外在生产环境下,咱们甚至都不用写上面的 optimization 配置,它会默认按这个配置去执行。可是 sideEffects 仍是要本身配置的。🤪

开发与生产模式的配置

由上可见,开发环境与线上生产环境的配置在不少地方是有区别的。为了方便起见,咱们也能够编写两份不一样的配置文件,来实现两种环境的切换。

// package.json 文件
"scripts": {
  // "start": "webpack-dev-server --open" 开发环境
  "dev": "webpack-dev-server --config webpack.dev.js",
  // "build": "webpack --mode development" 生产环境
  "build": "webpack --config webpack.prod.js"
}
复制代码

而后遵循不重复原则 (Don't repeat yourself - DRY),建立一个 webpack.common.js 文件来报存两种环境下的通用配置。

而后再安装使用cnpm i webpack-merge -D将这些配置合并在一块儿。此工具会引用 "common" 配置,所以咱们没必要再在环境特定的配置中编写重复代码。

// webpack.prod.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const prodConfig = {
    mode: 'production',
    plugins:[
        new HtmlWebpackPlugin({
            template: './src/index.html'
        }),
    ]
}
module.exports = merge(commonConfig, prodConfig)
复制代码

也能够将这些新加的配置文件统一放入一个 build 文件夹内,但同时要注意修改各个配置文件及 package.json 里 script 字段的文件路径。

代码分离 Code Splitting

webpack 默认会根据配置将咱们项目的代码都打包到 output 的文件中,但其实咱们的项目代码不全是业务代码,确定会引用一些第三方类库或者像 lodash 这样的工具函数库。若是都各类代码全都打包到一个 JS 文件输出,不只页面加载这个文件时不只会很慢,并且一旦业务代码有任何改动,下次访问就须要从新获取。

因此咱们须要将代码进行分离,而后将不一样的代码打包到多个文件输出,这样下次访问时由于浏览器的缓存机制,没有变更的代码文件便不用去从新获取。

代码分离是 webpack 中最引人注目的特性之一。此特性可以把代码分离到不一样的 bundle 中,而后能够按需加载或并行加载这些文件。代码分离能够用于获取更小的 bundle,以及控制资源加载优先级,若是使用合理,会极大影响加载时间。

入口起点

代码分离最简单的方法就是经过手动配置 webpack 的入口起点来实现。

// 在 src 下新建一个 lodash.js 并将 lodash 挂载到全局
import _ from 'lodash';
window._ = _;

// 配置入口起点
entry: {
    index: './src/index.js',
    lodash: './src/lodash.js' // 打包 lodash.js
}
复制代码

而后从新运行打包命令,会发现 dist 下多出一个单独打包 lodash 工具函数库代码的 lodash.js,且打包后的 index.html 里也能自动引入。

但这种方式存在一些隐患:

  • 若是入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中。
  • 这种方法不够灵活,而且不能动态地将核心应用程序逻辑中的代码拆分出来。

这两点中的第一点,对咱们的示例来讲毫无疑问是个严重问题,由于咱们若是在 ./src/index.js 中也引入 lodash,这样就形成在两个 bundle 中重复引用。咱们能够经过使用 SplitChunksPlugin 插件来移除重复模块。

去除重复

其实,本质上 Code Splitting 只是一个分割代码的概念,与 webpack 没有直接关系。但之因此说它是 webpack 的特性是由于 webpack4 里面直接捆绑了SplitChunks这样的插件,咱们不用再手动配置或是安装其余插件就能够很方便的实现代码分割。

optimization: {
	splitChunks: {
    chunks: 'all'
  }
}
复制代码

该插件能够将公共的依赖模块提取到已有的 entry chunk 中,或者提取到一个新生成的 chunk。好比经过设置配置文件的optimization.splitChunks选项,此插件将 lodash 这个沉重负担从主 bundle 中移除,而后分离到一个单独的 chunk 中,同时将项目中重复的依赖项删除掉。

另一些由社区提供,对于代码分离颇有帮助的 plugin 和 loader:

动态引入

当涉及到对动态引入的代码进行拆分时,webpack 推荐选择的方案是:使用符合 ECMAScript 提案import() 语法 来实现动态导入。👉🏻dynamic imports

// 动态引入 lodash 的 demo 
function getComponent() {
  // Lodash, now imported by this script
	return import('lodash').then(({ default: _ }) => {
		var element = document.createElement('div');
		element.innerHTML = _.join(['Hello', 'webpack'], '🎉');
		return element;
	}).catch(
    error => 'An error occurred while loading the component');
}

getComponent().then(component => {
	document.body.appendChild(component);
})
复制代码

因为 import() 会返回一个 promise,所以它能够和async一块儿使用。可是,须要使用像 Babel 这样的预处理器和 Syntax Dynamic Import Babel Plugin

async function getComponent() {
  var element = document.createElement('div');
  
  // Notice the default
  const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  return element;
}
复制代码

/*webpackChunkName: "lodash"*/ :import() 语法的魔法注释,为动态引入的模板设置打包后的文件名。

Lazy Loading 懒加载

懒加载或者按需加载,并非 webpack 里的概念,而是一种很好的优化网页或应用的方式。借助 EcmaScript 的 import() 实验性质语法的支持,从而实现先把代码在一些逻辑断点处分离开,而后在代码块中完成某些操做后,再引用另一些新的代码块。

经过懒加载,页面能够在执行的时候须要哪一个模块再去请求对应模块的源代码(由于某些代码块可能永远不会被加载),而不是一次性地把全部代码都加载到页面上,减少了整体体积,因此能够加快应用的初始加载速度。

// 利用懒加载,只有页面被点击后才会加载 lodash
document.addEventListener('click', () => {
  getComponent().then(component => {
    document.body.appendChild(component);
  })
})
复制代码

SplitChunksPlugin 配置

上面👆咱们经过设置 SplitChunksPlugin 的splitChunks.chunks配置就实现了去除重复依赖项以及同步与异步动态引入代码的打包分离。

这是该插件的默认配置:

optimization: {
	splitChunks: {
  	chunks: 'async',
    minSize: 30000,
    maxSize: 0,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
    	vendors: {
      	test: /[\\/]node_modules[\\/]/,
        priority: -10,
        filename: 'vendors.js'
      },
      default: {
      	minChunks: 2,
        priority: -20,
        reuseExistingChunk: true
      }
    }
  }
}
复制代码

chunks: async 只对异步引入的模块进行打包分离,initial 同步,all 二者均可。

minSize/maxSize: 模块的文件大小范围。

minChunks: 打包生成的 chunks 至少有几个引用了该模块,符合条件的模块才会被分离。

maxAsyncRequests: 同时加载的模块数,后续超出部分的模块不会被分离。

maxInitialRequests: 入口文件引用模块代码分离的上限数。

automaticNameDelimiter: 生成的文件名默认链接符。

name: 使下面设置的 filename 生效,从而能够为生成的文件重命名。

cacheGroups: 缓存组 打包同步引入的代码时必须配合这个配置项一块儿使用才能生效,它决定分离出来的代码到底要放到哪一个文件里面。vendors 为默认的分组名,test 为模块来源,priority 当前组的优先级,先放入优先级高的分组下的文件里。reuseExistingChunk 忽略已打包过的模块,直接复用。

想要更好的控制代码分离的流程,请查阅 SplitChunksPlugin

Prefetch 和 Preload

既然咱们经过将splitChunks.chunks字段配置成 all 以后,能够同时对同步与异步代码进行分割,将 loadash、jQuery 等代码单独分割打包生成一个文件。在第一次加载后,再次访问就可使用浏览器缓存来提升访问速度。

那为何 webpack 还要将其默认值设为async,只对异步代码进行分割打包呢?

这是由于按照咱们的上述配置,只经过将 jQuery、loadash 打包成单独的文件,加载后使用缓存只能提升第二次及之后再访问这些文件时的速度,而不能真正对页面访问性能作优化。webpack 所但愿达到的效果是第一次访问页面时,它的速度就是最快的。

document.addEventListener('click', () => {
	const element = document.createElement('div');
	element.innerHTML = 'Bingo!!!';
	document.body.appendChild(element);
})
复制代码

好比这样一段代码,在页面加载以后,咱们能够在 Chrome 控制台经过查看到 Sources 源文件里代码的执行状况以及 Coverage 的 Unused Bytes 覆盖率。 其中点击回调方法里的代码是须要点击以后才会被覆盖执行到的。

若是想提升页面核心代码的利用率,咱们能够将那些交互以后才用到的代码方法封装到另一个文件中,再在须要执行的时候将其加载进来。

// 将点击的回调方法封装进 click.js
function handleClick() { 
  const element = document.createElement('div');
	element.innerHTML = 'Bingo!!!';
	document.body.appendChild(element);
}
export default handleClick;
// index.js
document.addEventListener('click', () => {
	// 这里经过 default 来拿到导出的方法后重命名为 func
	import('./click.js').then(({default: func}) => {
		func();
	})
})
复制代码

因而可知,webpack 认为只有这种异步的组件才能真正的提高网页的打包性能。而同步的代码模块只能增长一个缓存,而对性能的提高是有限的。即咱们在作前端代码性能优化的时候,最重要的点其实不是缓存,而是 Code Coverage 代码覆盖率。即缓存带来的代码性能提高是很是有限的,而应该经过提升页面核心代码的覆盖和利用率,从而提高代码性能与页面加载速度。

一些网站的登陆模态框功能就是使用这种方式去实现的,可是若是咱们只在点击后才去加载登陆相关的代码,加载速度有可能会比较慢,影响用户体验。那么此时就须要用到 webpack 预取 prefetching 和预加载 preloading 模块) 的功能。从而既能提升首页核心代码的加载速度,同时也能够在页面展现完成后将登录功能的代码加载进来,保证用户点击登陆后的快速响应。

import(/* webpackPrefetch: true */ './click.js').then(({default: func}) => {
		func();
	})
复制代码

经过加入/* webpackPrefetch: true */后咱们就能够等待页面核心代码加载完成以后,浏览器带宽闲置时再去懒加载 prefetch 对应的文件。

至于 webpackPreload,与 webpackPrefetch 不一样的一点就在于它是和业务代码主线程一块儿去加载的。

这里也并不适用 webpackPreload,关于二者细节的区别请查看文档。另外不正确地使用 webpackPreload 也会有损性能。

打包分析

当咱们使用 webpack 对各类模块代码进行了分离打包以后,理所应当应该利用一些打包分析的工具来对输出的结果进行检查,分析是否合理。

使用 webpack 官方打包分析工具 生成一个打包分析的说明文件 stats.json,而后能够上传到 这里 上查看结果。

// package.json
"scripts": {
  // 能够在 build 字段中加入 --profile --json > stats.json 
  "build": "webpack --profile --json > stats.json --config webpack.prod.js",
  // 也能够专门添加一条生成分析文件的指令
  "analyse": "webpack --profile --json > stats.json"
},
复制代码

其余一些打包分析的可视化工具:

  • webpack-chart:webpack stats 可交互饼图。
  • webpack-visualizer:可视化并分析你的 bundle,检查哪些模块占用空间,哪些多是重复使用的。
  • webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展现为便捷的、交互式、可缩放的树状图形式。
  • webpack bundle optimize helper:此工具会分析你的 bundle,并为你提供可操做的改进措施建议,以减小 bundle 体积大小。

CSS 代码分离

上面咱们已经实现了对项目引用的一些 JS 代码进行分离打包,可是 CSS 代码依然仍是被打包进了 JS 文件里 (css-in-js) 。

要想对 CSS 文件也进行分离打包,可使用 MiniCssExtractPlugin

但目前这个插件还不支持 HMR,因此咱们只是在线上环境使用。

使用yarn add mini-css-extract-plugin -D完成安装后,配置 webpack 线上环境的文件。

// webpack.prod.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      // 可选参数
      filename: "[name].css",
      chunkFilename: "[name].chunk.css"
    })
  ],
  module: {
    rules: [
    	{
      	test: /\.(css|scss)$/,
        use: [
          // 与开发环境使用 style-loader 不一样,这里要使用 MiniCssExtractPlugin 的 loader
        	MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'sass-loader',
        ],
      }
    ]
  }
}
复制代码

若是想对打包后的 css 文件代码进压缩可使用官方推荐的 optimize-css-assets-webpack-plugin(Webpack5 已内置)

其余关于多入口的时候打包对应 CSS 的需求,配置相关的optimization.splitChunks.cacheGroups便可实现。

Caching 缓存

目前咱们每次打包后生成的文件都是按如下格式来配置的:

output:{
	// 使用占位符来命名即项目直接引用的 app.js
	filename:"[name].js",
	// 代码在 node-modules 下的模块打包生成的 chunks 会按照这个格式命名
 	chunkFilename: "[name].chunk.js"
}
复制代码

这样有一个问题,就是若是咱们更改了源代码,将从新打包生成的文件放到服务器以后。在浏览器端去请求时,会由于本地有缓存的同名文件,而不会去使用最新上传服务器的文件,致使最新代码没法生效。

开发环境下咱们不须要关心缓存的问题,由于 HMR 会为咱们解决这个问题,因此咱们只须要使用[contenthash]占位符修改一下生成环境的打包配置。

output:{
	filename:"[name].[contenthash].js", 
	// app.6df1cf350155facd60f5.js
	chunkFilename: "[name].[contenthash].js"
	// lodash.96223b700cd12e8a3a4d.js
}
复制代码

使用一串哈希值为文件内容添加上一个标识,这样只要咱们的源代码不发生改变,打包后的文件名就不会变。相应的,若是源代码发生变更,文件名的 hash 值也会发生改变。从而解决本地缓存的问题。

一些由于旧版本的 boilerplate(引导模板),特别是 runtime 和 manifest 引发的,不改动源代码,文件名却发生变化的问题,查看官网具体 最佳方案 解决。

Shim 预置依赖

在 webpack 打包过程当中,当咱们要利用 polyfill 来作代码方面的兼容扩展浏览器能力,或者是处理打包过程的兼容时,可使用 Shim 预置依赖的功能。

常见的一些方案与工具查看官方 shim 预置依赖文档。

相关文章
相关标签/搜索