webpack(二)

935 阅读9分钟

如何将构建时产生的bundle自动添加到html中

使用HtmlWebpackPlugin插件,会生成新的index.html文件,替换我们的原有文件。

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
 
 module.exports = {
   entry: {
     index: './src/index.js',
     print: './src/print.js',
   },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Output Management',
    }),
  ],
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

如何清理(webpack生成的文件放置在)/dist文件夹

webpack 将生成文件并放置在 /dist 文件夹中,但是它不会追踪哪些文件是实际在项目中用到的。

使用clean-webpack-plugin插件,在每次构建前清理/dist文件夹,这样只会生成用到的文件。

如何将编译后的代码映射回原始源代码

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

为了更容易地追踪 error 和 warning,JavaScript 提供了 source maps 功能,可以将编译后的代码映射回原始源代码。

const path = require('path');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const { CleanWebpackPlugin } = require('clean-webpack-plugin');
 
 module.exports = {
 // 注意:仅用于开发环境,source map 相当消耗资源
 // 避免在生产中使用 inline-*** 和 eval-***,因为它们会增加 bundle 体积大小,并降低整体性能
   mode: 'development',
   entry: {
     index: './src/index.js',
     print: './src/print.js',
   },
  devtool: 'inline-source-map',
   plugins: [
     new CleanWebpackPlugin(),
     new HtmlWebpackPlugin({
       title: 'Development',
     }),
   ],
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

如何在代码发生变化后自动编译代码

webpack 提供以下方式:

  • webpack --watch命令行(观察模式watch mode

缺点:需要手动刷新浏览器,才能看到修改后的实际效果

  • webpack serve --open命令行(webpack-dev-server

webpack-dev-server提供了一个简单的 web server,并具有 live reloading(实时重新加载) 功能

注意:webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。如果你的页面希望在其他不同路径中找到 bundle 文件,则可以通过 dev server 配置中的 publicPath 选项进行修改。

 const path = require('path');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const { CleanWebpackPlugin } = require('clean-webpack-plugin');
 
 module.exports = {
   mode: 'development',
   entry: {
     index: './src/index.js',
     print: './src/print.js',
   },
   devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist',//从什么位置查找文件serve 
  },
   plugins: [
     new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),
     new HtmlWebpackPlugin({
       title: 'Development',
     }),
   ],
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };
  • 使用 webpack-dev-middleware

webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server。

webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行更多自定义设置。

可结合ExpressJS(自定义设置server)等使用

需要手动刷新浏览器,才能看到修改后的实际效果,要结合使用webpack-hot-middleware依赖包,以在你的自定义服务器或应用程序上启用HMR

server.js

const 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);

// 告知 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件作为基础配置。
app.use(
  webpackDevMiddleware(compiler, {
    publicPath: config.output.publicPath,
  })
);

// 将文件 serve 到 port 3000。
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

代码分离的方法

  1. 入口起点

    缺点:

  • 手动配置较多
  • 如果入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中。
  • 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来
  1. 防止重复
  • 入口依赖 配置dependOnoption 选项,这样可以在多个 chunk 之间共享模块
const path = require('path');
 
 module.exports = {
   mode: 'development',
   entry: {
     index: {
       import: './src/index.js',
       dependOn: 'shared',
     },
     another: {
       import: './src/another-module.js',
       dependOn: 'shared',
     },
     shared: 'lodash',
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   //注意:要在一个 HTML 页面上使用多个入口时需设置optimization.runtimeChunk: 'single',将 runtime 代码拆分为一个单独的 chunk
  optimization: {
    runtimeChunk: 'single',
  },
 };
  • SplitChunksPlugin

SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。

const path = require('path');

  module.exports = {
    mode: 'development',
    entry: {
      index: './src/index.js',
      print: './src/print.js',
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
    
   optimization: {
     splitChunks: {
       chunks: 'all',
     },
   },
  };
  1. 动态导入
  • ECMAScript提案的import()语法 (推荐)
  • webpack特定的require.ensure

注意

import() 调用会在内部用到 promises。如果在旧版本浏览器中(例如,IE 11)使用 import(),记得使用一个 polyfill 库(例如 es6-promisepromise-polyfill),来实现 Promise。

由于 import() 会返回一个 promise,因此它可以和 async 函数一起使用。

预获取/预加载

  • prefetch(预获取):将来某些导航下可能需要的资源
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

这会生成<link rel="prefetch" href="login-modal-chunk.js">并追加到页面头部,指示着浏览器在闲置时间预取 login-modal-chunk.js文件。

  • preload(预加载):当前导航下可能需要资源

ChartComponent.js

//...
import(/* webpackPreload: true */ 'ChartingLibrary');

在页面中使用 ChartComponent 时,在请求 ChartComponent.js 的同时,还会通过 <link rel="preload"> 请求 charting-library-chunk。假定 page-chunk 体积很大,加载慢,页面此时就会显示 LoadingIndicator(加载进度条) ,等到 charting-library-chunk 请求完成,LoadingIndicator 组件才消失。启动仅需要很少的加载时间,因为只进行单次往返,而不是两次往返。尤其是在高延迟环境下。

注意:不正确的使用webpackPreload会有损性能

prefetch指令相比,preload指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。

bundle分析

  • analyse官方:您可以运行此命令行webpack --profile --json > stats.json生成所需的JSON文件
  • webpack-chart: webpack stats 可交互饼图。
  • webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
  • webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为一个便捷的、交互式、可缩放的树状图形式。
  • webpack bundle optimize helper:这个工具会分析你的 bundle,并提供可操作的改进措施,以减少 bundle 的大小。
  • bundle-stats:生成一个 bundle 报告(bundle 大小、资源、模块),并比较不同构建之间的结果。

缓存

确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。

  const path = require('path');
  const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      // 对于 CleanWebpackPlugin 的 v2 versions 以下版本,使用 new CleanWebpackPlugin(['dist/*'])
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        title: 'Caching',
      }),
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
    },
    optimization: {
    //解决vendor bundle 会随着自身的 module.id 的变化,而发生变化
     moduleIds: 'deterministic',
      runtimeChunk: 'single',//提取引导模板,将 runtime 代码拆分为一个单独的 chunk
      splitChunks: {
    	//利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资源,同时还能保证 client 代码和 server 代码版本一致
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all',
          },
        },
      },
    },
  };

构建性能提升

  • loader通过使用include字段,仅将loader应用在实际需要将其转换的模块

  • 提高解析速度

    • 减少resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles中条目数量,因为他们会增加文件系统调用的次数。
    • 如果你不使用symlinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false
    • 如果你使用自定义resolve plugin规则,并且没有指定context上下文,可以设置 resolve.cacheWithContext: false
  • 使用DllPlugin为更改不频繁的代码生成单独的编译结果

  • 尽量保持chunk体积小

    • 使用数量更少/体积更小的library
    • 在多页面应用程序中使用SplitChunksPlugin
    • 在多页面应用程序中使用SplitChunksPlugin,并开启async模式。
    • 移除未引用代码。
    • 只编译你当前正在开发的那些代码。
  • 不要使用太多的worker thread-loader可以将非常消耗资源的loader分流给一个worker pool(worker池)

注意:不要使用太多的worker,因为Node.jsruntimeloader都有启动开销。最小化workermain process(主进程)之间的模块传输。进程间通讯(IPC, inter process communication)是非常消耗资源的。

  • 持久化缓存

  • ProgressPlugin从 webpack 中删除,可以缩短构建时间。

  • 最小化项目中的 preset/plugin 数量.

  • 在单独的进程中使用fork-ts-checker-webpack-plugin进行类型检查。

  • 配置loader跳过类型检查。

  • 使用ts-loader时,设置happyPackMode: true / transpileOnly: true.

  • node-sass中有个来自 Node.js 线程池的阻塞线程的 bug。 当使用thread-loader时,需要设置workerParallelJobs: 2

如何独立配置开发和生成环境并合并公共部分

在开发环境中,我们需要:强大的source map和一个有着live reloading(实时重新加载)hot module replacement(热模块替换)能力的localhost server

在生产环境中,目标则转移至其他方面,关注点在于压缩bundle、更轻量的source map、资源优化等,通过这些优化方式改善加载时间。

使用webpack-merge工具,此工具会引用“common”配置,避免再在环境特定(environment-specific)的配置中编写重复代码

资源模块

资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。

在 webpack 5 之前,通常使用:

  • raw-loader将文件导入为字符串
  • url-loader将文件作为 data URI 内联到 bundle 中
  • file-loader将文件发送到输出目录

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

  • asset/resource发送一个单独的文件并导出 URL。之前通过使用 file-loader实现。
  • asset/inline导出一个资源的 data URI。之前通过使用url-loader实现。
  • asset/source导出资源的源代码。之前通过使用raw-loader实现。
  • asset在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用url-loader,并且配置资源体积限制实现。
module.exports = {
    entry: './src/index.js',
    output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
    // 自定义输出文件名 1.assetModuleFilename 来修改此模板字符串
   	assetModuleFilename: 'images/[hash][ext][query]'
  },
  module: {
   rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        // 需从 asset loader 中排除来自新 URL 处理的 asset
        dependency: { not: ['url'] }, 
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
            }
          },
        ],
       // 想停止当前 asset 模块的处理,并再次启动处理,避免asset 重复
       type: 'javascript/auto'
      },
      // Resource资源配置
      {
       test: /\.png/,
       type: 'asset/resource'
     },
     // 自定义输出文件名 2.将某些资源发送到指定目录,仅适用于 asset 和 asset/resource 模块类型
     {
       test: /\.html/,
       type: 'asset/resource',
       generator: {
         filename: 'static/[hash][ext][query]'
       }
     },
     {
       test: /\.svg/,
       type: 'asset/inline',
       // 自定义 data URI 生成器,所有 .svg 文件都将通过 mini-svg-data-uri 包进行编码
       generator: {
         dataUrl: content => {
           content = content.toString();
           return svgToMiniDataURI(content);
         }
     },
     {
       test: /\.txt/,
       type: 'asset/source',
     },
     {
        test: /\.doc/,
        // 通用资源类型,webpack 将按照默认条件,自动地在 resource 和 inline 之间进行选择:小于 8kb 的文件,将会视为 inline 模块类型,否则会被视为 resource 模块类型
        type: 'asset',
      	parser: {
         dataUrlCondition: {
           maxSize: 4 * 1024 // 4kb
         }
       }
      }
   ]
  },
}
  • resource资源
import mainImage from './images/main.png';

img.src = mainImage; // '/dist/151cfcfa1bd74779aadb.png'
  • inline资源
import metroMap from './images/metro.svg';

block.style.background = `url(${metroMap})`; // url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo...vc3ZnPgo=)
  • source资源 src/example.txt
Hello world

src/index.js

import exampleText from './example.txt';

block.textContent = exampleText; // 'Hello world'
  • url资源 当使用new URL('./path/to/asset', import.meta.url),webpack也会创建资源模块。

src/index.js

const logo = new URL('./logo.svg', import.meta.url);

根据你配置中 target 的不同,webpack 会将上述代码编译成不同结果:

// target: web
new URL(__webpack_public_path__ + 'logo.svg', document.baseURI || self.location.href);

// target: webworker
new URL(__webpack_public_path__ + 'logo.svg', self.location);

// target: node, node-webkit, nwjs, electron-main, electron-renderer, electron-preload, async-node
new URL(__webpack_public_path__ + 'logo.svg', require('url').pathToFileUrl(__filename));