zoukankan      html  css  js  c++  java
  • webpack分片chunk加载原理

    首先,使用create-react-app快速创建一个demo

    npx create-react-app react-demo # npx命令需要npm5.2+ 
    cd react-demo
    npm start
    

    通过http://localhost:3000/端口可以访问页面,接下来修改主应用组件App.js

    import React, { Component } from 'react';
    import './App.css';
    class App extends Component {
      onButtonClick = () => {
        import(/* webpackChunkName: "alert" */ './Alert')
        .then((res) => {
          res.default()
        })
      }
      render() {
        return (
          <div className="App">
            <button onClick={this.onButtonClick}>点击按需加载</button>
          </div>
        );
      }
    }
    export default App;
    

    在App.js中,点击按钮会加载alert模块,通过import()方法按需引入模块webpack很早就支持了,什么?你还在用过时的require.ensure(),你一定是个假的前端。通过注释里的webpackChunkName可以指定待加载模块打包后的文件名。Alert模块代码很简单,如下:

    function showAlert() {
      alert('我们一起学喵叫,喵喵喵喵')
    }
    export default showAlert
    

    为了方便研究源码,我们需要修改npm build时,webpack的配置,为此可以执行npm eject“释放”该项目的配置,然后修改config/webpack.config.prod.js,找到插件配置plugins数组里的webpack.optimize.UglifyJsPlugin注释掉,再执行npm build,打包后的结果在build目录下。

    默认情况下,prd环境打出来的入口包是main.js,dev环境下是bundle.js,通过查看页面源码script标签引用的文件可以看出来。下面的动图演示了点击按钮时,页面html结构的变化。
    2018-09-30_16-43-48-1
    从上图可以清楚的看到,一开始页面只加载了bundle.js文件,当点击按钮时在head标签的末尾插入了一个script标签,以此引入了alert.chunk.js并执行了代码。本文即要分析此过程webpack是如何实现的。

    _this.onButtonClick = function() {
                  __webpack_require__
                    .e(/* import() */ 0)
                    .then(__webpack_require__.bind(null, 20))
                    .then(function(res) {
                      res.default()
                    })
                }
    

    我们从按钮点击事件开始,上面的代码即为打包后的点击按钮的事件处理函数,首先关注__webpack_require__.e(/* import() */ 0),其传入参数是分片代码的IdchunkId,值为0,返回结果是一个Promise

    __webpack_require__.e = function requireEnsure(chunkId) {
        // installedChunks是在外层代码中定义的一个对象,缓存了已加载chunk信息,键为chunkId
      var installedChunkData = installedChunks[chunkId]
        // installedChunkData为0表示此chunk已经加载过
      if (installedChunkData === 0) {
        return new Promise(function(resolve) {
          resolve()
        })
      }
        /* 如果此chunk正在加载中,则返回对应未fullfilled的Promise,
        此时installedChunkData是一个数组,数组的元素从后续的代码中
        可以看出为[resolve, reject, Promise]。
        */
      if (installedChunkData) {
        return installedChunkData[2]
      } 
        /* 如果需要加载的chunk还未加载,则构造对应的Promsie并缓存在
        installedChunks对象中,从这里可以看出正在加载的chunk的缓存数据结构是一个数组
        */
      var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject]
      })
      installedChunkData[2] = promise
      
        /*开始加载chunk*/
        // 构造script标签
      var head = document.getElementsByTagName("head")[0]
      var script = document.createElement("script")
      script.type = "text/javascript"
      script.charset = "utf-8"
      script.async = true
      script.timeout = 120000
        // 如果设置了内容安全策略,则添加响应属性值,默认情况下是不启用的参考https://webpack.docschina.org/guides/csp/)
      if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc)
      }
        // 根据chunkId设置src,__webpack_require__.p是配置的公共路径
      script.src =
        __webpack_require__.p +
        "static/js/" +
        ({ "0": "alert" }[chunkId] || chunkId) +
        "." +
        { "0": "620d2495" }[chunkId] +
        ".chunk.js"
      var timeout = setTimeout(onScriptComplete, 120000)
      script.onerror = script.onload = onScriptComplete
      function onScriptComplete() {
        // avoid mem leaks in IE.
        script.onerror = script.onload = null
        clearTimeout(timeout)
        // 取出缓存中对应的chunk加载状态
        var chunk = installedChunks[chunkId]
        // 如果加载失败
        if (chunk !== 0) {
            // chunk加载超时
          if (chunk) {
            chunk[1](new Error("Loading chunk " + chunkId + " failed."))
          }
          // 将此chunk的加载状态重置为未加载状态
          installedChunks[chunkId] = undefined
        }
      }
        /* 将script标签插入到head中就会立即加载该脚本,
        脚本加载成功或者失败会执行上面的onScriptComplete方法
        */
      head.appendChild(script)
        // 返回待fullfilled的Promise
      return promise
    }
    

    补充说明

    • installedChunks在本例中的初始值为
      var installedChunks = {
        1: 0
      }
    

    该对象用户缓存已经加载和正在加载的chunk,在入口文件(把入口文件也当做一个chunk)中初始化,初始化后包含了入口chunk的状态,此例中入口chunk的Id为1,webpack分配chunkId是0开始计数递增的,实际上入口chunk的Id一定是最大的,从上面的代码中值0表示当前的入口chunk已经加载了。

    • chunk在此过程中有三种状态,在installedChunks分别对应三种值:未加载(undefined)->加载中([resolve, reject, Promise])->已加载(0)

    以上代码是chunk对应的js资源加载的方式,那么新加载的chunk是如何执行的呢?installedChunks中对应正在加载chunk状态该如何变化呢?下面我们假设需要加载的chunk加载成功了,此时alert.xxx.chunk.js对应的代码就会执行。alert.js打包后的代码如下:

    webpackJsonp([0], {
      20: function(module, __webpack_exports__, __webpack_require__) {
        "use strict"
        Object.defineProperty(__webpack_exports__, "__esModule", { value: true })
        function showAlert() {
          alert("我们一起学喵叫,喵喵喵喵")
        }
        __webpack_exports__["default"] = showAlert
      }
    })
    

    这段代码就执行了一个webpackJsonp方法,传入了两个参数,第一个参数是一个数组,数组的元素由需要加载的chunkId组成,第二个参数是一个对象,对象的键是moduleId,此例子中,当前chunk依赖的唯一模块是自己本身的,如果当前代码还有其他未加载的模块,也会出现在这里,注意如果在其他已加载的chunk中已加载的模块,这里就不会重新加载了。
    下面我们只需要关注webpackJsonp方法是如何实现的,这个方法并不是在当前chunk中实现的,而是在入口chunk文件中实现的。从这里也可以看出webpack是通过jsonp的方式异步加载chunk的

    var parentJsonpFunction = window["webpackJsonp"]
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
      // add "moreModules" to the modules object,
      // then flag all "chunkIds" as loaded and fire callback
      var moduleId,
        chunkId,
        i = 0,
        resolves = [];
        // 遍历需要执行的chunk
      for (; i < chunkIds.length; i++) {
        chunkId = chunkIds[i]
        // 如果该chunk正在加载中状态
        if (installedChunks[chunkId]) {
            // 暂存该chunk对应Promise的resolve方法
          resolves.push(installedChunks[chunkId][0])
        }
            // 将该chunk的状态置为加载完成
        installedChunks[chunkId] = 0
      }
       // 遍历这些chunk依赖的模块并缓存模块到modules对象中,这个对象是在入口文件的最外层方法当做参数传入的
      for (moduleId in moreModules) {
        if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
          modules[moduleId] = moreModules[moduleId]
        }
      }
      if (parentJsonpFunction)
        parentJsonpFunction(chunkIds, moreModules, executeModules)
        // 将加载的chunk对应的Promise fullfill掉
      while (resolves.length) {
        resolves.shift()()
      }
    }
    

    还记得之前点击按钮的代码吗,当加载的chunk对应的Promise变为fullfilled状态,就会执行__webpack_require__.bind(null, 20)加载该chunk中对应主模块。__webpack_require__是用来加载模块的,它的实现非常简单:

    function __webpack_require__(moduleId) {
      // 检查模块缓存对象中是否已有该模块,有的话直接返回
      if (installedModules[moduleId]) {
        return installedModules[moduleId].exports
      } 
      // 模块缓存对象中没有该模块就创建一个新模块并添加到缓存中
      var module = (installedModules[moduleId] = {
        i: moduleId, // 模块Id
        l: false, // 是否已加载
        exports: {} // 模块的导出
      }) 
      // 执行模块对应的代码(模块的代码是在webpackJsonp方法中缓存到modules对象中的)
      modules[moduleId].call(
        module.exports,
        module,
        module.exports,
        __webpack_require__
      ) 
        // 将模块标志改为已加载
      module.l = true 
        // 返回模块的导出对象
      return module.exports
    }
    

    上述代码中modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)执行了下面的模块代码,执行上下文为module.exports对象,传入了参数module、 module.exports、 __webpack_require____webpack_require__是用来加载其他模块的,本例中并没有。

    function (module, __webpack_exports__, __webpack_require__) {
      "use strict"
      Object.defineProperty(__webpack_exports__, "__esModule", { value: true })
      function showAlert() {
        alert("我们一起学喵叫,喵喵喵喵")
      }
      // __webpack_exports__就是传入的module.exports对象
      __webpack_exports__["default"] = showAlert
    }
    

    返回按钮点击执行的事件处理程序,当__webpack_require__.bind(null, 20)执行后返回导出的模块,再执行res.default()就相当于执行了showAlert方法。

    _this.onButtonClick = function() {
                  __webpack_require__
                    .e(/* import() */ 0)
                    .then(__webpack_require__.bind(null, 20))
                    .then(function(res) {
                      res.default()
                    })
                }
    

    通过这个简单的例子,基本了解了webpack加载chunk和module的原理。

  • 相关阅读:
    Android开发--去掉标题栏
    Android开发app如何设定应用图标下的应用名称为汉字以及自定义图标
    mysql的sql其他 SQL中inner join、outer join和cross join的区别
    中文乱码问题 -js页面传值乱码
    liunx Centos Xshell 简单命令汇总
    html 属性及相关应用-实例
    时间格式转换
    三元表达式
    Grid++Report生成简单的条形码、Excel导出、图表控件 等
    jmp指令的简单应用
  • 原文地址:https://www.cnblogs.com/star91/p/webpack-fen-pianchunk-jia-zai-yuan-li.html
Copyright © 2011-2022 走看看