kangyana / daily-question

When your heart is set on something, you get closer to your goal with each passing day.
https://www.webpack.top
MIT License
3 stars 0 forks source link

【Q094】常见的配置项 #94

Open kangyana opened 1 year ago

kangyana commented 1 year ago

1. 模块热替换

启用 HMR

启用此功能需要更新 webpack-dev-server 的配置,和使用 webpack 内置的 HMR 插件。

webpack.config.js

module.exports = {
    devServer: {
        hot: true
    }
};

worker.js

if (module.hot) {
  module.hot.accept('./print.js', function() {
    console.log('模块更新');
  })
}

HMR 修改样式表

CSS 的模块热更新,借助于 style-loader

2. Tree shaking

设置 sideEffects

package.json 中:

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

sideEffects 可选值:

「副作用」的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。 举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。有副作用的不能 tree sharking!

注意,任何导入的文件都会受到 tree shaking 的影响。 这意味着,如果在项目中使用类似 css-loader 并导入 CSS 文件,则需要将其添加到 sideEffect 列表中,以免在生产模式中无意中将它删除。

还可以在 module.rules 配置选项 中设置 sideEffects

压缩输出

从 v4 开始,也可以通过 mode 配置选项轻松切换到压缩输出,只需设置为 production

// webpack.config.js

module.exports = {
  mode: "production"
};

tree sharking 条件

kangyana commented 1 year ago

3. devtool

生产环境,在使用 uglifyjs-webpack-plugin 时,你必须提供 sourceMap:true 选项来启用 source map 支持。 鼓励你在生产环境中启用 source map,因为它们对调试源码(debug)和运行基准测试(benchmark tests)很有帮助。

开发环境

生产环境

特定场景

以下选项对于开发环境和生产环境并不理想。 他们是一些特定场景下需要的,例如,针对一些第三方工具。

4. 指定环境变量

基本使用

NODE_ENV 属性:

这个变量并不是 pocess.env 直接就有的,而是通过设置得到的。 可以通过判断这个变量区分开发环境或生产环境。

我们可以使用 webpack 内置的 DefinePlugin 为所有的依赖定义这个变量:

// webpack.prod.js

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

module.exports = merge(common, {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
});

任何位于 /src 的本地代码都可以关联到 process.env.NODE_ENV 环境变量,所以以下检查也是有效的:

// src/worker.js

import { cube } from './math.js';

if (process.env.NODE_ENV !== 'production') {
  console.log('Looks like we are in development mode!');
}

如何在 webpack 配置文件里获取 NODE_ENV 的值

package.json 配置 corss-env

"scripts": {
    "build-prod":"cross-env NODE_ENV=production webpack"
}

通过 cross-env NODE_ENV=production,信息传递给了 webpack 的配置文件, src 文件下面不能访问。

kangyana commented 1 year ago

5. 代码分离

概述

把代码分离到不同的 bundle 中,然后可以 按需加载或并行加载 这些文件。 代码分离可以用于获取 更小的 bundle,以及 控制资源加载优先级,如果使用合理,会极大缩小加载时间。

有三种常用的代码分离方法:

动态导入(dynamic imports)

当涉及到动态代码拆分时,webpack 提供了两个类似的技术:

重点讲第一种。 worker.js

// 方式一
const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');

// 方式二
import(/* webpackChunkName: "print" */ './print').then(module => {
      var print = module.default;
      // ...
});

注意:当调用 ES6 模块的 import() 方法时,必须指向模块的 .default 值,因为它才是 promise 被处理后返回的实际的 module 对象。

webpack.config.js

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

bundle 分析

介绍几种常用分析工具:

6. 缓存

hash、chunkhash 和 contenthash 三者的区别

在 webpack 中有时需要使用 hash 来做静态资源实现增量更新方案之一,文件名的 hash 值可以有三种 hash 生成方式,每一种都有不同应用场景,那么三者有何区别呢?

hash 一般是结合CDN缓存来使用,通过 webpack 构建之后,生成对应文件名自动带上对应的 MD5 值。 如果文件内容发生改变的话,那么对应文件 hash 值也会改变,对应的 HTML 引用的 URL 地址也会改变, 触发 CDN 服务器从原服务器上拉取对应数据,进而更新本地缓存。 但是实际使用时,这三种 hash 计算还是有一定区别。

hash

hash 是跟整个项目的构建相关,构建生成的文件 hash 值都是一样的,所以 hash 计算是跟整个项目的构建相关。 同一次构建过程中生成的 hash 都是一样的,只要项目里有文件更改,整个项目构建的 hash 值都会更改。

如果出口是 hash,那么一旦针对项目中任何一个文件的修改,都会构建整个项目,重新获取 hash 值,缓存的目的将失效。

chunkhash

采用 hash 计算的话,每一次构建后生成的 hash 值都不一样,即使文件内容压根没有改变。 这样子是没办法实现缓存效果,我们需要另一种 hash 值计算方法,即 chunkhash

chunkhashhash 不一样,它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的 hash 值。 我们在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用 chunkhash 的方式生成 hash 值,那么只要我们不改动公共库的代码,就可以保证其 hash 值不会受影响。

由于采用 chunkhash,所以项目主入口文件 main.js 及其对应的依赖文件 main.css 由于被打包在同一个模块, 所以共用相同的 chunkhash,但是公共库由于是不同的模块,所以有单独的 chunkhash。 这样子就保证了在线上构建时只要文件内容没有更改就不会重复构建。

entry:{
    main: path.join(__dirname,'./main.js'),
    vendor: ['vue']
},
output:{
    path:path.join(__dirname,'./dist'),
    publicPath: '/dist/',
    filname: 'bundle.[chunkhash].js'
}

contenthash

contenthash 表示由文件内容产生的 hash 值,内容不同产生的 contenthash 值也不一样。 在项目中,通常做法是把项目中 css 都抽离出对应的 css 文件来加以引用。

在这里我用 mini-css-extract-plugin 替代了 extract-text-webpack-plugin

const miniCssExtractPlugin=require("mini-css-extract-plugin");

module.exports={
    module:{
        rules:[
            {
                test: /\.css$/,
                use:[
                    miniCssExtractPlugin.loader,
                    'css-loader'
                ]
            }
        ]
    },
    plugins:[
        new miniExtractPlugin({
            filename: 'main.[contenthash:7].css'
        })
    }
}

打包后即使 css 文件所处的模块里就算其他文件内容改变,只要 css 文件内容不变,那么就不会重复构建。

注意:

如果对 css 使用了 chunkhash 之后,它与依赖它的 chunk 共用 chunkhash, 测试后会发现,css 与 js 文件名的 chunkhash 值是一样的,如果我修改了 js 文件,js 的 hash 值会变化, css 的文件名的 hash 还是和变化后的 js 文件的 hash 值一样,如果我修改了 css 文件,也会导致重新构建,css 的 hash 值和 js 的 hash 值还是一样的,即使 js 文件没有被修改。 这样会导致缓存作用失效,所以 css 文件最好使用 contenthash

实现图片/字体的缓存

对于图片、字体等静态资源,生成对应的文件 hash 值是由对应的 file-loader 来计算的。

那么这些静态文件的 hash 值使用的是什么 hash 值呢?

其实就是 hash 属性值。 此 hash 非 webpack 每次项目构建的 hash,它是由 file-loader 根据文件内容计算出来的,不是 webpack 构建的 hash

kangyana commented 1 year ago

7. 多种 webpack 配置方式(configuration types)

导出为一个函数

 module.exports = function(env, argv) {
   return {
     mode: env.production ? 'production' : 'development',
     devtool: env.production ? 'source-maps' : 'eval',
     plugins: [
       new webpack.optimize.UglifyJsPlugin({
         compress: argv['optimize-minimize'] // 只有传入 -p 或 --optimize-minimize
       })
     ]
   };
};

当 webpack 配置为 导出为一个函数 时,可以向起传入一个 环境对象(environment)。 也可以通过 指定环境变量 中的 cross-env NODE_ENV=production webpack 配置。

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

注意:如果设置 env 变量,却没有赋值,默认将 --env.production 设置为 true。 还有其他可以使用的语法。有关详细信息,请查看 webpack CLI 文档。

导出一个 Promise

webpack 将运行由配置文件导出的函数,并且等待 Promise 返回。 便于需要异步地加载所需的配置变量。

module.exports = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({
        entry: './app.js',
        /* ... */
      })
    }, 5000)
  })
}

导出一个配置对象

导出数组,多个配置对象

8. webpack-dev-server 的刷新模式 inline 和 iframe 详解

iframe

inline

总结

9. 模式

设置 NODE_ENV 时,不会自动设置 mode

用法

只在配置中提供 mode 选项:

// webpack.config.js

module.exports = {
  mode: 'production'
};

或者从 CLI 参数中传递:

webpack --mode=production

生产环境 production

// webpack.production.config.js

module.exports = {
+  mode: 'production',
-  plugins: [
-    new UglifyJsPlugin(/* ... */),
-    new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
-    new webpack.optimize.ModuleConcatenationPlugin(),
-    new webpack.NoEmitOnErrorsPlugin()
-  ]
}

开发环境

// webpack.development.config.js

module.exports = {
+ mode: 'development'
- plugins: [
-   new webpack.NamedModulesPlugin(),
-   new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}
kangyana commented 1 year ago

10. output

output.library

对于用途广泛的 library,我们希望它能够兼容不同的环境,例如 CommonJS,AMD,Node.js 或者作为一个全局变量。 为了让你的 library 能够在各种用户环境(consumption)中可用,需要在 output 中添加 library 属性。

为了让 library 和其他环境兼容,还需要在配置文件中添加 libraryTarget属性。 这是可以控制 library 如何以不同方式暴露的选项,output.libraryTarget 的默认选项是 var

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'webpack-numbers.js',
    library: 'webpackNumbers'
    library: 'webpackNumbers',
    libraryTarget: 'umd'
  },

output.publicPath 和 devServer.publicPath 的区别

devServer.publicPath 的意义就是决定外部能以怎样的路径通过 devServer 来访问构建在内存中的文件, 这个字段未显式设定时,则会去沿用 output.publicPath 字段的显式值(如果 output.publicPath 有值的话,否则就用自己的 default 值)。 output.publicPath 的意义是用来为构建的文件生成满足特定需求的前缀,并将这个前缀提供给需要的 resolverplugin 或者其他的配置字段。

注意:HtmlWebpackPlugin 中的 filename 也会依赖于 public.publicPath

output.path

这个字段只在 production 配置下有效。 另外,它的 default 值是 path.resolve(__dirname, './dist')

11. webpack.optimize.ModuleConcatenationPlugin

过去 webpack 打包时的一个取舍是将 bundle 中各个模块单独 打包成闭包。 这些打包函数使你的 JavaScript 在浏览器中 处理的更慢。 相比之下,一些工具像 Closure CompilerRollupJS 可以提升(hoist)或者 预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度。 这个插件会在 webpack 中实现以上的预编译功能。

12. devServer

devServer 构建的文件是在内存里的,而非你电脑的磁盘上,但是如果内存中找不到想要的文件时,devServer 会根据文件的路径尝试去电脑的磁盘上找,如果这样还找不到才会 404。

开发时在内存和 contentBase 下真实的磁盘路径中存在着同样文件名的文件,那么 devServer 返回的是内存的那个。

devServer.historyApiFallback

 historyApiFallback: {
     rewrites: [
         { from: /.*/, to: path.posix.join(devConfig.assetsPublicPath, 'index.html') },
     ],
 },

当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html。通过传入以下启用:

historyApiFallback: true

通过传入一个对象,比如使用 rewrites 这个选项,此行为可进一步地控制:

historyApiFallback: {
  rewrites: [
    { from: /^\/$/, to: '/views/landing.html' },
    { from: /^\/subpage/, to: '/views/subpage.html' },
    { from: /./, to: '/views/404.html' }
  ]
}

devServer.publicPath

决定外部能够以什么样的路径访问到构建的文件。

output.publicPath 的关系,参考第10小节的 [output.publicPath 和 devServer.publicPath 的区别]

kangyana commented 1 year ago

13. 监听文件

webpack 可以监听文件变化,当它们修改后会重新编译。

watch

boolean = false

启用 Watch 模式。这意味着在初始构建之后,webpack 将继续监听任何已解析文件的更改。

// webpack.config.js

module.exports = {
  watch: true,
};

注意:webpack-dev-serverwebpack-dev-middleware 里 Watch 模式默认开启。

watchOptions

object

一组用来定制 watch 模式的选项:

// webpack.config.js

module.exports = {
  watchOptions: {
    aggregateTimeout: 200,
    poll: 1000,
  },
};

watchOptions.aggregateTimeout

number = 20

当第一个文件更改,会在重新构建前增加延迟。 这个选项允许 webpack 将这段时间内进行的任何其他更改都聚合到一次重新构建里。以毫秒为单位:

// webpack.config.js

module.exports = {
  watchOptions: {
    aggregateTimeout: 600,
  },
};

watchOptions.ignored

RegExp string [string]

对于某些系统,监听大量文件会导致大量的 CPU 或内存占用。 所以可以选择下面一种方法来排除文件/目录:

// webpack.config.js

const path = require('path');

module.exports = {
  watchOptions: {
    ignored: /node_modules/, // 正则模式
    ignored: '**/node_modules', // glob 模式
    ignored: ['**/files/**/*.js', '**/node_modules'], // 多 glob 匹配模式
    ignored: [path.posix.resolve(__dirname, './ignored-dir')], // 绝对路径
  },
};

当使用 glob 模式时,我们使用 glob-to-regexp 将其转为正则表达式。 因此,在使用 watchOptions.ignored 的 glob 模式之前,请确保自己熟悉它。

注意:如果你使用 require.context,webpack 会监听你的整个目录。 你应该忽略一些文件和目录,以便那些不需要监听的文件修改后不会触发重新构建。

watchOptions.poll

boolean = false number

通过传递 true 开启 polling,或者指定毫秒为单位进行轮询。

// webpack.config.js

const path = require('path');

module.exports = {
  watchOptions: {
    poll: 1000, // 每秒检查一次变动
  },
};

watchOptions.followSymlinks

boolean

根据软链接查找文件。这通常是不需要的,因为 webpack 已经使用 resolve.symlinks 解析了软链接。

// webpack.config.js

const path = require('path');

module.exports = {
  watchOptions: {
    followSymlinks: true,
  },
};

watchOptions.stdin

boolean

当 stdin 流结束时停止监听。

// webpack.config.js

const path = require('path');

module.exports = {
  watchOptions: {
    stdin: true,
  },
};
kangyana commented 1 year ago

14. SplitChunksPlugin

SplitChunksPlugin 是拆分模块的插件。

开箱即用的 SplitChunksPlugin 对于大部分用户来说非常友好。

默认情况下,它只会影响到按需加载的 chunks,因为修改 initial chunks 会影响到项目的 HTML 文件中的脚本标签。

webpack 将根据以下条件自动拆分 chunks:

optimization.splitChunks

下面这个配置对象代表 SplitChunksPlugin 的默认参数。

// webpack.config.js

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};