重读webpack5

根底补充

对于harmony modules
ES2015 modules 又叫做 harmony modules

对于副作用:
webpack的 side effect副作用,是指在import后执行特定行为的代码,而不是export一个或者多个,例如 pollyfill,全局css款式等

对于entry:
entry 对象是webpack开始 build bundle 的中央。

对于context:
context 是蕴含入口文件的目录的相对字符串,默认就是当前目录,然而倡议设置。

对于依赖图:
webpack是 dynamically bundle 依赖通过依赖图 dependency graph,防止打包没用的module。

对于Loader:
module loader可链式调用,链中的每个loader都将解决资源,调用程序是反的。

对于图片:
webpack5内置了解决图片、字体文件,不须要额定的loader来解决 。

{ test: /\.(png|svg|jpg|jpeg|gif)$/i, type: 'asset/resource',}

对于 csv、xml
csv-loader来加载csv文件数据, xml-loader来加载xml文件数据 。
能够应用 parser 而不是loader来解决toml, yamljs and json5格局的资源,如下

// webpack.config.jsconst yaml = require('yamljs');module: {    rules: [       {          test: /\.yaml$/i,          type: 'json',          parser: {            parse: yaml.parse,          }        }    ]}// 代码中应用import yaml from './data.yaml';console.log(yaml)

对于html-webpack-plugin:
html-webpack-plugin装置形式 npm i --save-dev html-webpack-plugin@next

对于manifest
webpack应用manifest来track module映射到bundle的关系,应用webpack-manifest-plugin

对于sourceMap
source maps 用做track js的error和warning,能够把编译后的代码指向源代码,定位异样的确切地位。

开发服务

每次改了代码,重写打包会很麻烦,有3种解决办法,
webpack的watch模式
webpack-dev-server
webpack-dev-middleware,webpack-dev-middleware是一个包装器,它会将webpack解决过的文件发送到服务器,webpack-dev-server外部应用。

// 1. 应用webpack-dev-server,装置好包之后,在webpack.config.js 中配置 devServer: {    contentBase: './dist', }// 2 应用 webpack-dev-middleware和express,装置之后,新建serverjsconst express = require('express');const webpack = require('webpack');const webpackDevMiddleware = require('webpack-dev-middleware');const app = express();const config = require('./webpack.config.js');const compiler = webpack(config);// Tell express to use the webpack-dev-middleware and use the webpack.config.js// configuration file as a base.app.use(    webpackDevMiddleware(compiler, {      publicPath: config.output.publicPath,    }));// Serve the files on port 3000.app.listen(3000, function () {  console.log('Example app listening on port 3000!\n');});

代码宰割 codes splitting

1 entry 宰割

这样没什么用,用的lodash还是会整个打到最初的bundle中,尽管能够通过 import chunk from 'lodash/chunk'这种写法优化

// index.jsimport _ from 'lodash'console.log(  _.chunk([1,2,3,4], 2))// index2.jsimport _ from 'lodash'console.log(_.join(['Another', 'module', 'loaded!'], ' '));// webpack.config.jsentry: {  index: './src/index.js',  index2: './src/index2.js',}

2 Prevent Duplication

入口的 dependOn 字段和 runtimeChunk: 'single' 的办法

// 入口配置entry: {    index: {      import: './src/index.js',      dependOn: 'shared',    },    index2: {      import: './src/index2.js',      dependOn: 'shared',    },    shared: 'lodash',}// webpack.config.js optimization: {    runtimeChunk: 'single',  }

SplitChunksPlugin容许咱们将独特的依赖提取到一个现有的入口文件块或一个全新的chunk中。

 module.exports = {    entry: {      index: './src/index.js',      another: './src/another-module.js',    },   optimization: {     splitChunks: {       chunks: 'all',     },   },  };

3 动静导入 dynamic import

应用 require.ensure 或者 import()

// index.js // 咱们须要 default 值的起因是,自从webpack 4以来,当导入CommonJS模块时,导入将不再解析为module.exports的值import('lodash').then(({default: _}) => {  console.log(      _.chunk([1, 2, 3, 4], 2)  )})

Prefetching/Preloading modules

  • prefetch 预获取: 未来可能须要一些资源来反对运行。
  • preload 预加载: 在以后运行期间须要的资源。
  • preload chunk和父 chunk并行加载,prefetch chunk在父块加载实现后开始加载。
  • preload chunk具备中等优先级并立刻下载。当浏览器闲暇时,才会下载 prefetch chunk。
  • 父块应该立刻申请preload chunk,prefetch chunk可能在未来的任何时候应用。
  • 浏览器反对不同。
  • 一个简略的预加载例子是,有一个总是依赖于一个大库的组件,这个库应该在一个独自的块中,例如 import(/* webpackPreload: true */ 'ChartingLibrary');
  • webpackPreload 不正确应用会对性能产生影响,审慎应用。

缓存

浏览器会缓存文件,这会让web加载更快,缩小不必要的流量。然而编译文件更新了就会造成麻烦。

Output Filename

contenthash 会依据文件内容计算一个字符串,文件内容变了就会扭转。
然而哪怕内容从新打包,hash也不肯定一样,webpack版本新的应该没这个问题,这是因为webpack在entry块中蕴含了某些样板文件,特地是 runtime 和 manifest。

output: {  filename: '[name].[contenthash].js',  path: path.resolve(__dirname, 'dist'),}

提取样板

SplitChunksPlugin 能够用于将module拆分为独自的bundle。webpack提供了一个优化个性,能够应用optimize.runtimecchunk 选项将运行时代码宰割成一个独自的chunk。将其设置为single,为所有块创立单个运行时bundle.

optimization: { runtimeChunk: 'single',}

会失去下边的后果

                          Asset       Size  Chunks             Chunk Namesruntime.cc17ae2a94ec771e9221.js   1.42 KiB       0  [emitted]  runtime   main.e81de2cf758ada72f306.js   69.5 KiB       1  [emitted]  main                index.html  275 bytes          [emitted]

提取第三方库,像react等,它们个别不会变动,应用 cacheGroups 如下

optimization: {     runtimeChunk: 'single',     splitChunks: {       cacheGroups: {         vendor: {           test: /[\\/]node_modules[\\/]/,           name: 'common-libs',           chunks: 'all',         }       }     } }

失去如下后果

  asset common-libs.e6d769d50acd25e3ae56.js 1.36 MiB [emitted] [immutable] (name: common-libs) (id hint: vendor)  asset runtime.2537ce2560d55e32a85c.js    15.9 KiB [emitted] [immutable] (name: runtime)  asset index.f9c0d8e7e437c9cf3a6e.js     1.81 KiB [emitted] [immutable] (name: index)  asset index.html 370 bytes [emitted]

Module ID

如果在index.js,减少一个援用新的文件的应用,从新打包,会发现,所有的hash都变了,然而专用库内容没变,hash还是变了,因为减少了新的文件会导致它们的moduleid发生变化,所以hash也变了。在配置中减少如下。

optimization: {  // 通知webpack在抉择 模块id 时应用哪种算法,默认false   moduleIds: 'deterministic'}

用webpack5测试,只有index.js的hash变了,runtime chunk 和 common-libs 都没变。

环境变量

webpack内置了环境变量的设置办法,

npx webpack --env NODE_ENV=local --env production --progress

然而module.exports必须是个办法

const path = require('path');module.exports = env => {  // Use env.<YOUR VARIABLE> here:  console.log('NODE_ENV: ', env.NODE_ENV); // 'local'  console.log('Production: ', env.production); // true  return {    entry: './src/index.js',    output: {      filename: 'bundle.js',      path: path.resolve(__dirname, 'dist'),    },  };};

依赖治理

如果你的require蕴含表达式则会创立一个上下文,因而在编译时不晓得确切的模块

// 源目录- modules/fn1.js         /fn2.js   // 源代码let name = 'fn1';const f = require("./modules/" + name + '.js')console.log(f);// webpack 这么解决Directory: ./modulesRegular expression: /^.*\.js$/

生成一个context module。它蕴含了对该目录中所有模块的援用,匹配正则表达式的申请可能须要这些模块。
context模块蕴含一个映射,它将申请转换为模块id。

// webpack打包后有这么一个文件var map = {    "./fn1.js": 430,    "./fn2.js": 698};

这意味着反对动静需要,但会导致所有匹配的模块都蕴含在bundle中。

热更新 HMR

开启

如果是应用 webpack-dev-middleware 那么须要应用 webpack-hot-middleware 来开启热更新

// 在devserver开启devServer: {  contentBase: './dist',  hot: true,}

应用 node api

const webpackDevServer = require('webpack-dev-server');const webpack = require('webpack');const config = require('./webpack.config.js');const options = {  contentBase: './dist',  hot: true,  host: 'localhost',};webpackDevServer.addDevServerEntrypoints(config, options);const compiler = webpack(config);const server = new webpackDevServer(compiler, options);server.listen(5000, 'localhost', () => {  console.log('dev server listening on port 5000');});

摇树 Tree Shaking

摇树是用来删没用的代码的,它依赖于ES2015模块语法的动态构造就是 import 和 export,是由rollup倒退而来。
webpack2之后,内置了对es6的反对以及未应用模块导出的检测。
webpack4扩大了这个性能,通过package.json的 sideEffects 字段,来表明 "纯文件" 能够平安删除。

源代码

// math.jsexport function square(x) {  return x * x;}export function cube(x) {  return x * x * x;}// index.jsimport {cube} from './math.js';console.log(cube(5))

webpack.config.js

 mode: 'development', optimization: {   usedExports: true, },

打包后的文件,没用的代码并没有删除

/*!*********************!*\  !*** ./src/math.js ***!  \*********************//***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {"use strict";/* harmony export */ __webpack_require__.d(__webpack_exports__, {/* harmony export */   "cube": () => /* binding */ cube/* harmony export */ });/* unused harmony export square */function square(x) {  return x * x;}function cube(x) {  return x * x * x;}/***/ })

下边是没配置 usedExports: true,这个配置的作用,就是让webpack来判断哪些模块没应用,此配置依赖于
optimization.providedExports(通知webpack哪些export是由模块提供的),默认就是true

/*!*********************!*\  !*** ./src/math.js ***!  \*********************//***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {"use strict";__webpack_require__.r(__webpack_exports__);/* harmony export */ __webpack_require__.d(__webpack_exports__, {/* harmony export */   "square": () => /* binding */ square,/* harmony export */   "cube": () => /* binding */ cube/* harmony export */ });function square(x) {  return x * x;}function cube(x) {  return x * x * x;}/***/ })

在100% ESM模块的世界中,辨认副作用是很简略的,然而当初没到哪一步,所以须要在package.json中的sideEffects来通知webpack,
标记文件在副作用树,下面提到的所有代码都没有副作用,所以咱们能够简略地将该属性标记为false,以告诉webpack它能够平安地修剪未应用的导出文件。

// 开启后,打包后的死代码,依然没删除{  "name": "your-project",  "sideEffects": false}// 如果有些文件的确有副作用,提供一个数组即可 {  "name": "your-project",  "sideEffects": [    "./src/some-side-effectful-file.js"  ]}

留神,如果应用css-loader等加载款式,须要把css放到副作用数组中,避免在生产模式的时候,被webpack无心中删除。
还能够在webpack.config.js中的module.rules中的一个rule中指定。

{  "name": "your-project",  "sideEffects": [    "./src/some-side-effectful-file.js",    "*.css"  ]}

side effects 和 usedExports(摇树)的区别

side effects能够间接抹去文件,例如
如果设置了 sideEffects: false, 而后在index.js引入一个 math.js然而不应用,打包后的bundle不会打包math.js
然而如果没设置,不应用的文件还是会在打包后的文件中,代码如下

/***/ 733:/*!*********************!*\  !*** ./src/math.js ***!  \*********************//***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {"use strict";/* unused harmony exports square, cube */function square(x) {  return x * x;}function cube(x) {  return x * x * x;}/***/ })

usedExports 是依赖于terser来检测,在申明中的副作用,不如side effects间接
也无奈间接跳过整个文件,React的高阶组件存在问题。

// 应用这个表明示意没有副作用,这会容许删掉这行代码,不剖析他的side effect var Button$1 = /*#__PURE__*/ withAppProvider()(Button);

因为文件的import申明不好判断能够在package.json 的 sideEffects 字段退出该文件
If no direct export from a module flagged with no-sideEffects is used,
the bundler can skip evaluating the module for side effects

// 应用这一行代码 import { Button } from "@shopify/polaris"// 下边是 @shopify/polaris 库里的文件 // index.jsimport './configure';export * from './types';export * from './components'; // components/index.jsexport { default as Breadcrumbs } from './Breadcrumbs';export { default as Button, buttonFrom, buttonsFrom, } from './Button';export { default as ButtonGroup } from './ButtonGroup';// package.json"sideEffects": [  "**/*.css",  "**/*.scss",  "./esnext/index.js",  "./esnext/configure.js"],

对于 import { Button } from "@shopify/polaris"这样代码,有以下4种状况

include it: include the module, evaluate it and continue analysing dependenciesskip over: don't include it, don't evaluate it but continue analysing dependenciesexclude it: don't include it, don't evaluate it and don't analyse dependencies

仔细分析代码通过的模块

index.js:     没应用间接export的代码, 然而用sideEffects标记了 -> include itconfigure.js: 没应用间接export的代码, 然而用sideEffects标记了 -> include ittypes/index.js: 没应用间接export的代码, 没sideEffects标记  -> exclude itcomponents/index.js: 没应用间接export的代码, 没sideEffects标记 , but reexported exports are used -> skip overcomponents/Breadcrumbs.js: 没应用间接export的代码, 没sideEffects标记 -> exclude it. This also excluded all dependencies like components/Breadcrumbs.css even if they are flagged with sideEffects.components/Button.js: 应用间接export的代码, 没sideEffects标记-> include itcomponents/Button.css: 没应用间接export的代码, 然而用sideEffects标记了 -> include it

这样造成,间接引入的文件,只有4个

index.js: pretty much emptyconfigure.jscomponents/Button.jscomponents/Button.css

函数调用变副作用树

通过应用/*#__PURE__*/正文,能够通知webpack函数调用是无副作用(纯)的。

它能够放在函数调用的后面,以标记它们为无副作用。传递给函数的参数没有被正文标记,可能须要独自标记。
当未应用变量申明中的初始值被认为是无副作用(pure)时,它将被标记为死代码,不会被执行,并被最小化者删除。
优化时启用此行为。innerGraph设置为true

/*#__PURE__*/ double(55);

缩小代码

mode 设置为 production 即可,
--optimize-minimize 也能够开启 TerserPlugin,
module-concatenation-plugin这个插件在tree shaking中应用。

Build Performance

  • 放弃 webpack、node、npm是最新版本
  • 应用 DllPlugin 打包
  • 在loader中缩小解析范畴
const path = require('path');module.exports = {  module: {    rules: [      {        test: /\.js$/,        include: path.resolve(__dirname, 'src'),        loader: 'babel-loader',      },    ],  },};Boot
  • 缩小文件体积,移除没用的代码,
  • 应用缓存 cache配置

Module Federation

  • github demo
  • 多个独立的build应该形成惟一的应用程序。这些独自的build之间不应该有依赖关系,因而能够独自开发和部署它们。这通常被称为 微前端,但并不仅限于此
  • module两种,本地构建的模块(local module)、运行时容器加载的近程模块(remote module)。
  • 加载近程模块,是一个异步的加载chunk的操作,就是应用import、require.ensure或者require([])
  • 容器通过容器入口创立,并暴露出获取指定模块的办法,异步加载模块(chunk loading)和异步解析模块(解析期间与其余模块穿插执行)
  • 解析程序,近程解析到本地,或者本地解析到近程,不会收到影响
  • 容器能够应用其余容器的模块,容器之间的依赖共享能够实现。
  • 容器能够标识模块为可重写,消费者提供重写办法,就是一个能够替换容器中可替换模块的模块
  • 当consumer提供一个模块时,容器的所有模块都将应用替换模块而不是本地模块,不提供替换模块,就应用本地模块
  • 容器用一种不需在被consumer重写时下载的形式来治理可重写模块,通常是通过将它们放入不同的chunk来实现的。
  • 替换模块的provider只提供异步加载办法,它容许容器按需加载替换模块。provider用在容器不须要的时候不加载的形式来治理替换模块,通常是通过将它们放入不同的chunk来实现的
  • name 用于标识容器中的可重写模块
  • 重写动作和容器裸露模块类似的形式相似,分为两步,异步加载和异步解析。
  • 当应用嵌套,给一个容器提供重写将会主动重写嵌套容器中具备雷同“名称”的模块。
  • 重写必须在加载容器的模块之前提供。在初始块中应用的重写,只能被不应用Promise的同步模块重写。一旦被解析,重写项将不再是可重写的。
  • 每个构建都可作为容器,并能够生产别的构建作为容器应用。每个构建都能够通过从其容器中加载其余公开的模块来应用。
  • shared module是既能够重写又能够重写嵌套容器的模块。它们通常在每个构建中指向同一个模块,例如同一个库
  • packageName选项容许设置一个包名来查找所需的版本。默认状况下,主动推断模块申请,当主动推断应该被禁用时,将requiredVersion设置为false

Building blocks

  • OverridablesPlugin 此插件让一个模块,可重写
  • ContainerPlugin 此插件应用指定的exposed modules创立一个额定的容器entry,它外部应用OverridablesPlugin,并向容器的consumer公开override API
  • ContainerReferencePlugin 插件add特定的援用到容器作为externals,并容许从这些容器导入近程模块。它还调用这些容器的override API来提供对它们的override。本地重写(通过__webpack_override__或override API,当build也是一个容器时)和指定重写将被提供给所有援用的容器
  • ModuleFederationPlugin 此插件组合了ContainerPlugin和ContainerReferencePlugin,Overrides and overridables 会被组合到一个指定的共享模块列表中

Module Federation须要实现的指标

  • 应该能够公开和应用webpack反对的任何模块类型
  • chunk加载应并行加载所有须要的货色(web:到服务器的单程往返)
  • 从consumer到容器的管制,重写模块是单向操作,兄弟容器不能笼罩彼此的模块。
  • 应该独立于环境,web, Node.js等都能够用
  • 共享的绝对和相对申请(不应用也应该被提供、依据config.context来解析、不默认应用requiredVersion)
  • 共享的模块申请(按需提供,将匹配构建中应用的所有equal模块申请,将提供所有匹配模块,

它将从package.json中提取requiredVersion在图中的这个地位,可provide和consume多重不同版本当你有nested node_modules)

  • 带有后缀/共享的模块申请将匹配所有带有此前缀的模块申请

应用场景

  1. 每个页面独自构建

单个spa的每个页面都是在独自的构建中通过容器构建公开的,应用程序外壳是一个援用所有页面的独自构建的近程模块,
这样的话每个页面都能够独自部署,当路由更新或增加新路由时,应用程序外壳就会被部署。
应用程序外壳将罕用库定义为共享模块,以防止在页面构建中重复使用它们

  1. 组件库作为容器

多利用共享一个公共的组件库,能够将其构建为公开每个组件的容器,每个利用生产组件库容器。
对组件库的更改能够独自部署而不须要重新部署所有应用程序。应用程序主动应用组件库的最新版本。

动静近程容器

容器接口反对get和init办法。init是一个异步兼容的办法,调用时只有一个参数:共享范畴对象。
此对象在近程容器中用作共享scope,并由host填充provided modules。
它能够在运行时动静的连贯 remote containers to a host container

(async () => {  // 初始化shared scope,应用以后build和所有近程的provided modules  await __webpack_init_sharing__('default');  const container = window.someContainer; // or get the container somewhere else  // Initialize the container, it may provide shared modules  await container.init(__webpack_share_scopes__.default);  const module = await container.get('./module');})();

容器尝试提供共享模块,但如果共享模块曾经被应用,则正告和提供的共享模块将被疏忽。容器依然能够应用它作为fallback。
通过这种形式,你能够动静加载一个A/B测试,它提供了一个共享模块的不同版本。