webpack详解&HMR

870 阅读9分钟

参考资料:

  1. webpack官网资料
  2. 深入浅出webpack
  3. webpack简介
  4. vue热更新原理及源码

一、 核心概念

  • webpack 把⼀切静态资源视为模块,所以⼜叫做静态模块打包器。通过⼊⼝⽂件递归构建依赖图,借助不同的 loader 处理相应的⽂件源码,最终输出⽬标环境可执⾏的代码。

  • 通常我们使⽤其构建项⽬时,维护的是⼀份配置⽂件,如果把整个 webpack 功能视为⼀个复杂的函数,那么这份配置就是函数的参数,我们通过修改参数来控制输出的结果。

  • 在开发环境中,为了提升开发效率和体验,我们希望源码的修改能实时且⽆刷新地反馈在浏览器中,这种技术就是 HRM(Hot Module Replacement)。

  • 借助 webpack loader,我们可以差异化处理不同的⽂件类型。准确地说,loader 站在⽂件类型的维度上处理不同的任务,将各种语法的源码转换为统⼀的资源例如 less/sass => css,ts/jsx => js,ES6=> ES5。因此它只作⽤于静态资源。

  • webpack plugin 则以 webpack 打包的整个过程为维度,监听某些节点来执⾏定义的事件,能够处理 loader 不能完成的任务。例如:资源优化、模块拆分、环境变量定义等等。

二、 webpack 从零到一

1、安装webpack

yarn init // ⼀路回⻋,当然仓库的信息也可根据需求填写

yarn add webpack webpack-cli -D

2、webpack.confifig.js 配置

  • context:配置基础目录
  • Entry:配置模块的入口;
  • Output:配置如何输出最终需要的代码;
  • Module:配置处理模块的规则;
  • Resolve:配置寻找模块的规则;
  • Plugins:配置扩展插件;
  • DevServer:配置DevServer开发服务器;
  • target:配置构建目标;
  • Optimization:配置优化内容,拆包等;
  • devtools:配置sourcemap;
  • watch和watchOptions:配置监听文件;
  • externals:配置排除依赖,打包排除文件;
const path = require('path');

module.exports = {
  // entry 表示 入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  // 类型可以是 string | object | array   
  entry: './app/entry', // 只有1个入口,入口只有1个文件
  entry: ['./app/entry1', './app/entry2'], // 只有1个入口,入口有2个文件
  entry: { // 有2个入口
    a: './app/entry-a',
    b: ['./app/entry-b1', './app/entry-b2']
  },

  // 如何输出结果:在 Webpack 经过一系列处理后,如何输出最终想要的代码。
  output: {
    // 输出文件存放的目录,必须是 string 类型的绝对路径。
    path: path.resolve(__dirname, 'dist'),

    // 输出文件的名称
    filename: 'bundle.js', // 完整的名称
    filename: '[name].js', // 当配置了多个 entry 时,通过名称模版为不同的 entry 生成不同的文件名称
    filename: '[chunkhash].js', // 根据文件内容 hash 值生成文件名称,用于浏览器长时间缓存文件

    // 发布到线上的所有资源的 URL 前缀,string 类型
    publicPath: '/assets/', // 放到指定目录下
    publicPath: '', // 放到根目录下
    publicPath: 'https://cdn.example.com/', // 放到 CDN 上去

    // 导出库的名称,string 类型
    // 不填它时,默认输出格式是匿名的立即执行函数
    library: 'MyLibrary',

    // 导出库的类型,枚举类型,默认是 var
    // 可以是 umd | umd2 | commonjs2 | commonjs | amd | this | var | assign | window | global | jsonp ,
    libraryTarget: 'umd', //浏览器和node端都可以运行

    // 是否包含有用的文件路径信息到生成的代码里去,boolean 类型
    pathinfo: true, 

    // 附加 Chunk 的文件名称
    chunkFilename: '[id].js',
    chunkFilename: '[chunkhash].js',

    // JSONP 异步加载资源时的回调函数名称,需要和服务端搭配使用
    jsonpFunction: 'myWebpackJsonp',

    // 生成的 Source Map 文件名称
    sourceMapFilename: '[file].map',

    // 浏览器开发者工具里显示的源码模块名称
    devtoolModuleFilenameTemplate: 'webpack:///[resource-path]',

    // 异步加载跨域的资源时使用的方式
    crossOriginLoading: 'use-credentials',
    crossOriginLoading: 'anonymous',
    crossOriginLoading: false,
  },

  // 配置模块相关
  module: {
    rules: [ // 配置 Loader
      {  
        test: /.jsx?$/, // 正则匹配命中要使用 Loader 的文件
        include: [ // 只会命中这里面的文件
          path.resolve(__dirname, 'app')
        ],
        exclude: [ // 忽略这里面的文件
          path.resolve(__dirname, 'app/demo-files')
        ],
        use: [ // 使用那些 Loader,有先后次序,从后往前执行
          'style-loader', // 直接使用 Loader 的名称
          {
            loader: 'css-loader',      
            options: { // 给 html-loader 传一些参数
            }
          }
        ]
      },
    ],
    noParse: [ // 不用解析和处理的模块
      /special-library.js$/  // 用正则匹配
    ],
  },

  // 配置插件
  plugins: [
  ],

  // 配置寻找模块的规则
  resolve: { 
    modules: [ // 寻找模块的根目录,array 类型,默认以 node_modules 为根目录
      'node_modules',
      path.resolve(__dirname, 'app')
    ],
    extensions: ['.js', '.json', '.jsx', '.css'], // 模块的后缀名
    alias: { // 模块别名配置,用于映射模块
       // 把 'module' 映射 'new-module',同样的 'module/path/file' 也会被映射成 'new-module/path/file'
      'module': 'new-module',
      // 使用结尾符号 $ 后,把 'only-module' 映射成 'new-module',
      // 但是不像上面的,'module/path/file' 不会被映射成 'new-module/path/file'
      'only-module$': 'new-module', 
    },
    alias: [ // alias 还支持使用数组来更详细的配置
      {
        name: 'module', // 老的模块
        alias: 'new-module', // 新的模块
        // 是否是只映射模块,如果是 true 只有 'module' 会被映射,如果是 false 'module/inner/path' 也会被映射
        onlyModule: true, 
      }
    ],
    symlinks: true, // 是否跟随文件软链接去搜寻模块的路径
    descriptionFiles: ['package.json'], // 模块的描述文件
    mainFields: ['main'], // 模块的描述文件里的描述入口的文件的字段名称
    enforceExtension: false, // 是否强制导入语句必须要写明文件后缀
  },

  // 输出文件性能检查配置
  performance: { 
    hints: 'warning', // 有性能问题时输出警告
    hints: 'error', // 有性能问题时输出错误
    hints: false, // 关闭性能检查
    maxAssetSize: 200000, // 最大文件大小 (单位 bytes)
    maxEntrypointSize: 400000, // 最大入口文件大小 (单位 bytes)
    assetFilter: function(assetFilename) { // 过滤要检查的文件
      return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
    }
  },

  devtool: 'source-map', // 配置 source-map 类型,默认值为false

  context: __dirname, // Webpack 使用的根目录,string 类型必须是绝对路径

  // 配置输出代码的运行环境
  target: 'web', // 浏览器,默认
  target: 'webworker', // WebWorker
  target: 'node', // Node.js,使用 `require` 语句加载 Chunk 代码
  target: 'async-node', // Node.js,异步加载 Chunk 代码
  target: 'node-webkit', // nw.js
  target: 'electron-main', // electron, 主线程
  target: 'electron-renderer', // electron, 渲染线程

  externals: { // 使用来自 JavaScript 运行环境提供的全局变量
    jquery: 'jQuery'
  },

  stats: { // 控制台输出日志控制
    assets: true,
    colors: true,
    errors: true,
    errorDetails: true,
    hash: true,
  },

  //DevServer 相关的配置
  devServer: { 
    proxy: { // 代理到后端服务接口
      '/api': 'http://localhost:3000'
    },
    contentBase: path.join(__dirname, 'public'), // 配置 DevServer HTTP 服务器的文件根目录
    compress: true, // 是否开启 gzip 压缩
    historyApiFallback: true, // 是否开发 HTML5 History API 网页
    hot: true, // 是否开启模块热替换功能
    //另一选型only,true时热更新页面失败刷新页面,only热更新失败不刷新页面
    https: false, // 是否开启 HTTPS 模式
    open: false, //第一次构建完成,是否启动浏览器
    host: 0.0.0.0, //支持任何地址访问DevServer的Http服务
    allowedHosts: ['baidu.com', 'sub.host.com'], //允许访问域名列表
  	disableHostCheck: false, //host检查关闭,可直接使用ip访问服务器
    inline: false, //关闭inline使用iframe方式
  },

  profile: true, // 是否捕捉 Webpack 构建的性能信息,用于分析什么原因导致构建性能不佳
  cache: false, // 是否启用缓存提升构建速度
  watch: true, // 是否开始
  watchOptions: { // 监听模式选项
    // 不监听的文件或文件夹,支持正则匹配。默认为空
    ignored: /node_modules/,
    // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
    // 默认为300ms 
    aggregateTimeout: 300,
    // 判断文件是否发生变化是不停的去询问系统指定文件有没有变化,默认每秒问 1000 次
    poll: 1000
  },
  //优化配置,拆包处理 
  optimization: {
    splitChunks: {
      cacheGroups:{
        vendor:{
          filename: 'vendor.js',
          chunks: 'all', //包类型,async(异步时拆包),initial
          test: /[\/]node_modules[\/](react|react-dom)[\/]/   //正则表达式
        }
      }
    }
  }
}

3、output输出

module.exports = {
    output: {
        filename: '[name]_[chunkhash:8].js', //chunk属性:id\name\hash\chunkhash(内容的hash值)
        chunkFilename: '',   //chunk文件输出
        path: path.resolve(__dirname, 'dist_[hash]'), //输出路径
        crossOriginLoading: 'anonymous(默认,不带cookies)|use-credentials(带cookies)'
    //配置script标签的crossorigin属性
    }
};

三、webpack loaders

参考资料:

  1. babel配置文件解析

特点:⽆需导⼊,针对特定⽂件进⾏处理,输⼊⽂件内容并输出处理后的内容,交给下⼀个 loader 处理。

本质:具有返回值函数

⼏⼤原则按重要性排序:单⼀职责、可链式调⽤、模块化、⽆状态、尽量借助⼯具库(loader-utils、schema-utils 等)、标记依赖项、解决模块依赖关系、公共代码复⽤、避免绝对路径、peerdependencies。

1、babel-loader

用于将ES6转换为ES5,安装使用@babel/core、@babel/preset-env、babel-loader

module: {
  rules: [
    {
      test: /.js$/,
      include: [resolve('src'), resolve('test')],
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: { 
          presets: ['env'], //对应babel-preset-env
          plugins: [
            ['@babel/plugin-proposal-object-rest-spread'],
            //装饰器loader,需安装@babel/plugin-proposal-decorators
            ["@babel/plugin-proposal-decorators", { "legacy": true }],
          ],
          cacheDirectory: true 
          /* 默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果。之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程(recompilation process)。*/
        }
      }
    }
  ]
}
//package.json文件,babel-core和babel-loader是核心插件,babel-preset-env处理代码的预设
"devDependencies": {
    "babel-core": "^6.26.0",   // 核心包
    "babel-loader": "^7.1.2",   // 基础包
    "babel-preset-env": "^1.6.1",  // 根据配置转换成浏览器支持的 es5  
    "babel-plugin-transform-runtime": "^6.23.0",  
     //polyfill作用:es6新语法引入,promise、Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol
    "babel-preset-react": "^6.24.1", //react语法的转换
    "babel-plugin-import": "^1.6.3",  // import的转换 
    "babel-preset-stage-0": "^6.24.1", //babel-preset-stage-0 打包处于 strawman 阶段的语法)
}
  • 配置文件.babelrc

增加了.babelrc文件后,options项即可省略,在执行babel-loader的时候默认会去读.babelrc中的配置,webpack和babel.rc文件里的配置都会生效,比如transform-remove-console插件在任意一处配置,都会生效。在.babelrc配置文件中,主要是对预设(presets)和插件(plugins)进行配置。

2、thread-loader

Runs the following loaders in a worker pool.

把这个 loader 放置在其他 loader 之前, 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。

use: [
  {
    loader: "thread-loader",
    // 有同样配置的 loader 会共享一个 worker 池(worker pool)
    options: {
      // 产生的 worker 的数量,默认是 (cpu 核心数 - 1)
      // 或者,在 require('os').cpus() 是 undefined 时回退至 1
      workers: 2,

      // 一个 worker 进程中并行执行工作的数量
      // 默认为 20
      workerParallelJobs: 50,

      // 额外的 Node.js 参数
      workerNodeArgs: ['--max-old-space-size=1024'],

      // Allow to respawn a dead worker pool
      // respawning slows down the entire compilation
      // and should be set to false for development
      poolRespawn: false,

      // 闲置时定时删除 worker 进程
      // 默认为 500ms
      // 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
      poolTimeout: 2000,

      // 池(pool)分配给 worker 的工作数量
      // 默认为 200
      // 降低这个数值会降低总体的效率,但是会提升工作分布更均一
      poolParallelJobs: 50,

      // 池(pool)的名称
      // 可以修改名称来创建其余选项都一样的池(pool)
      name: "my-pool"
    }
  },
  // your expensive loader (e.g babel-loader)
]

3、url-loader

用于将文件转换为base64 URI的webpack加载程序。

module.exports = {
  module: {
    rules: [
      {
        test: /.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
              fallback: 'responsive-loader', //Default: 'file-loader'
              quality: 85,
              mimetype: 'image/png'
            }
          }
        ]
      }
    ]
  }
}

4、eslint-loader

module.exports = {
  // ...
  module: {
    rules: [
      {
        enforce: "pre",
        test: /.js$/,
        exclude: /node_modules/,
        loader: "eslint-loader",
        options: {
          eslintPath: path.join(__dirname, "reusable-eslint")
        }
      },
      {
        test: /.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      }
    ]
  }
  // ...
};

四、webpack plugins

参考资料:

  1. html-webpack-plugin

特点:需要导⼊并实例化,通过钩⼦可以涉及整个构建流程,因此贯穿整个构建范围。

本质:原型上具有 apply⽅法的具名构造函数或类。

再详细点,原型上的 apply ⽅法就是“通过 webpack 在不同阶段提供的事件钩⼦来操纵其内部实例特定的数据,最后调⽤ webpack 提供的回调”的函数。

1、CopyWebpackPlugin

将单个文件或整个目录复制到生成目录。

const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
  plugins: [
    new CopyPlugin([
      {
        from: 'src/**/*',
        to: 'dest/',
        ignore: ['*.js'],
      },
    ]),
  ],
};

2、HtmlWebpackPlugin

自动引入脚本和样式表,可使用模版选项配置模版。简化了HTML文件的创建,以便为你的webpack包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。

var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');

module.exports = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  plugins: [
    new HtmlWebpackPlugin(
  	filename: path.resolve(__dirname, '../dist/index.html'),
    template: 'index.html',
    inject: true, //true || 'head' || 'body' || false 
    /* Inject all assets into the given template or templateContent. When passing 'body' all javascript resources will be placed at the bottom of the body element. 'head' will place the scripts in the head element. Passing true will add it to the head/body depending on the scriptLoading option. Passing false will disable automatic injections. */
    chunksSortMode: 'dependency',
    minify: {
        collapseWhitespace: true,
        keepClosingSlash: true,
        removeComments: true,
        removeRedundantAttributes: true,
        removeScriptTypeAttributes: true,
        removeStyleLinkTypeAttributes: true,
        useShortDoctype: true
        }
  )]
};

3、MiniCssExtractPlugin

样式抽离,这个插件将CSS提取到单独的文件中。它会根据包含CSS的JS文件创建一个CSS文件。它支持按需加载CSS和SourceMaps。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
  ],
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          {
            //使用MiniCssExtractPlugin.loader代替style-loader,加载css需使用style-loader
            loader: MiniCssExtractPlugin.loader,
            options: {
              // you can specify a publicPath here
              // by default it use publicPath in webpackOptions.output
              publicPath: '../'
            }
          },
          ["css-loader"]
        ]
      }
    ]
  }
}

五、HMR

图片.png

1、使用

热更新可使用webpack-dev-server

//webpack.config.js开启热更新配置
{
  devServer: {
    port: 8000,
    hot: true
  }
}

//App.js配置热更新的回调,有更新回调,类似于ajax,不会刷新页面
if (module.hot) {
  module.hot.accept(App, () => {
    render(<App />, document.querySelector('#app'));
  });
}
//如果 App 组件是外部文件创建的,通常写作(与import导入的路径一致)
if (module.hot) {
  module.hot.accept('./App', () => {
    render(<App />, document.querySelector('#app'));
  });
}

2、热更新原理

  • 首次启动时:源代码 => 编译(compiler)=> bundle.js => 浏览器访问端口 => 服务器返回静态资源(js\css\html)=> 浏览器与服务器dev-server建立socket链接,首次收到hash值。
  • 代码更新时:源代码修改 => 增量编译(compiler)=> HMR(基于新内容生成[hash].update.js(on))=> 向浏览器推送消息(包括新的hash)=> 浏览器创建新的script标签下载[hash].update.js(on) => 调用页面更新的方法(module.hot.accept)。

3、源码分析

  • 开发环境基于 webpack-dev-server,在 /node_modules/webpack-dev-server/lib/Server.js (下⽂均将省略 /node_modules 这层⽂件夹)下找到下⾯的逻辑——本地服务器的消息类型:
createWebSocketServer() {
    this.webSocketServer = new /** @type {any} */ (this.getServerTransport())(this);
    ...
    if (this.options.hot === true || this.options.hot === "only") {
        this.sendMessage([client], "hot");
    }
    if (this.options.liveReload) {
        this.sendMessage([client], "liveReload");
    }
    if (this.options.client &&
    /** @type {ClientConfiguration} */
    (this.options.client).progress) {
        this.sendMessage([client], "progress", (this.options.client).progress);
    }
    if (this.options.client && (this.options.client).reconnect) {
        this.sendMessage([client], "reconnect", (this.options.client).reconnect);
    }
    ...
 }
 
...
sendMessage(clients, type, data, params) {
    for (const client of clients) {
        // client 是所有与服务端链接的客户端实例
        if (client.readyState === 1) {
            client.send(JSON.stringify({ type, data, params }));
        }
    }
}

图片.png

  • 热更新阶段 /webpack-dev-server/lib/Server.js :
...
setupHooks() {
    this.compiler.hooks.invalid.tap("webpack-dev-server", () => {
        if (this.webSocketServer) {
            this.sendMessage(this.webSocketServer.clients, "invalid");
        }
    });
    this.compiler.hooks.done.tap("webpack-dev-server",(stats) => {
        if (this.webSocketServer) { // 注意这⾥的 sendStats
            this.sendStats(this.webSocketServer.clients, this.getStats(stats));
        }
        this.stats = stats;
    });
}

接上⽂ sendStats (注释有删减、格式有调整):

sendStats(clients, stats, force) {
    ...
    this.currentHash = stats.hash;
    this.sendMessage(clients, "hash", stats.hash);
    ...
    if ((stats.errors).length > 0 || (stats.warnings).length > 0) {
        const hasErrors = (stats.errors).length > 0;
        if ((stats.warnings).length > 0) {
            let params;
            if (hasErrors) {
                params = { preventReloading: true };
            }
            this.sendMessage(clients, "warnings", stats.warnings, params);
        }
        if ((stats.errors).length > 0) {
            this.sendMessage(clients, "errors", stats.errors);
        }
    } else {
        this.sendMessage(clients, "ok");
    }
}

同时,静态资源也有变更:

图片.png

初步结论是,更新阶段,增加的⽂件下载与后⾯的 3 次消息(invalid, hash, ok)是关联的。⽽且毫⽆疑问,客户端必然有针对这些消息进⾏接收与处理的逻辑( webpack-dev-server/client/socket.js ):

var socket = function initSocket(url, handlers, reconnect) {
    ...
    client.onMessage(function (data) {
        var message = JSON.parse(data);
        if (handlers[message.type]) {
            handlers[message.type](message.data, message.params);
        }
    });
}

上⾯的逻辑表明,handler 对象实现了不同的消息类型对应的同名处理⽅法。 webpack-dev-server/client/index.js :

// 这⾏在最后,⽅便顺藤摸⽠所以前置了。
socket(socketURL, onSocketMessage, options.reconnect);
    var onSocketMessage = {
        hot: function hot() {
            if (parsedResourceQuery.hot === "false") {
                return;
            }
        options.hot = true;
        log.info("Hot Module Replacement enabled.");
    },
    
    liveReload: function liveReload() {
        if (parsedResourceQuery["live-reload"] === "false") {
            return;
        }
        options.liveReload = true;
        log.info("Live Reloading enabled.");
    },

    invalid: function invalid() {
        log.info("App updated. Recompiling...");
        if (options.overlay) {
            hide();
        }
        sendMessage("Invalid");
    },

    hash: function hash(_hash) {
        status.previousHash = status.currentHash;
        status.currentHash = _hash;
    },

    overlay: function overlay(value) {
        if (typeof document === "undefined") {
            return;
        }
        options.overlay = value;
    },
    ...
    
    ok: function ok() {
        sendMessage("Ok");
        if (options.overlay) {
            hide();
        }
        reloadApp(options, status); // 直到接到 "ok" 的信号,才会重载 App
    },
    ...
}

重载 App 的逻辑 webpack-dev-server/client/utils/reloadApp.js :

import hotEmitter from "webpack/hot/emitter.js";
...

function reloadApp(_ref, status) {
    var hot = _ref.hot,
    liveReload = _ref.liveReload;
    
    if (status.isUnloading) {
        return;
    }

    var currentHash = status.currentHash, previousHash = status.previousHash;
    var isInitial = currentHash.indexOf(previousHash) >= 0;
    
    if (isInitial) { // hash 值不变时不更新
        return;
    }
    ...

    var search = self.location.search.toLowerCase();
    var allowToHot = search.indexOf("webpack-dev-server-hot=false") === -1;
    ...

    if (hot && allowToHot) {
        // 同时满⾜⻚⾯地址没有 webpack-dev-server-hot=false 字样才热更新
        log.info("App hot update...");
        hotEmitter.emit("webpackHotUpdate", status.currentHash); // 客户端热更新事件
        if (typeof self !== "undefined" && self.window) { // 向服务端回执热更新
            self.postMessage("webpackHotUpdate".concat(status.currentHash), "*");
        }
    } else ...
}

根据 webpackHotUpdate => check => module.hot.check ,我们可以追踪到⼀份运⾏时⽂件webpack/lib/hmr/HotModuleReplacement.runtime.js (这⾥的运⾏时⽂件源码使⽤了占位标识,运⾏的时候会替换为 webpack 的全局变量,这⾥的代码也是要注⼊浏览器的), module.hot.check 的逻辑如下:

function hotCheck(applyOnUpdate) {
    return setStatus("check")
    .then($hmrDownloadManifest$) // 占位 1
    .then(function (update) {
        ...
        return setStatus("prepare").then(function () {
            ...
            return Promise.all(
                Object.keys($hmrDownloadUpdateHandlers$).reduce(function ( // 占位 2
                promises, key) {
                    $hmrDownloadUpdateHandlers$[key]( // 占位 2
                    update.c, update.r, update.m, promises,
                    currentUpdateApplyHandlers, updatedModules);

                    return promises;
                }, [])
        ...
}

六、异步组件加载

  1. webpack 将以import 函数为分割点,import('./Async') 返回 promise,等待组件加载完成后,展示真正的组件,Component: () => null为默认内容。
const Async = lazy(() => import(/* webpackChunkName: "Async" */ './Async'));

解析:异步组件从本质上,解决的还是SPA用户体验的问题。它为webpack提供了代码分割的依据,使得使用率高或者加载时间长的组件代码独立出去,同时通过低成本的过渡交互,保证了网站的体验。

  1. require.ensure 函数是 webpack 特有的作为代码分割点的依据,已被 import() 取代
require.ensure(
  dependencies: String[], // 依赖项
  callback: function(require), // 加载组件
  errorCallback: function(error), // 加载失败
  chunkName: String // 指定产出块名称
)
//和导入的区别,导入时打包时会导入包中;使用ensure会使用单独文件,不会打包进包中
require.ensure([], function () {
    const ensure = require('./requireEnsure');
    ensure.default();
}, () => null, 'require-ensure') //打包可见生成了output/require-ensure.js