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结构的变化。

从上图可以清楚的看到,一开始页面只加载了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的原理。
webpack分片chunk加载原理的更多相关文章
- Vue(基础七)_webpack(webpack异步加载原理)
---恢复内容开始--- 一.前言 1.webpack异步加载原理’ 2.webpack.ensure原理 ...
- Webpack按需加载一切皆模块
前言 在学习 Webpack 之前,我们需要了解一个概念:模块. 何为模块? 如果你曾学过 Java , C# 之类的语言,一定会知道 Java 中的 import 或 C# 中的 using 吧? ...
- vue第七单元(vue的单文件组件形式-单文件组件的加载原理-vue-cli构建的开发环境以及生命周期)
第七单元(vue的单文件组件形式-单文件组件的加载原理-vue-cli构建的开发环境以及生命周期) #课程目标 掌握安装 vue-cli 命令行工具的方法,掌握使用命令行在本地搭建开发环境,使用命令行 ...
- iOS 下拉刷新-上拉加载原理
前言 讲下拉刷新及上拉加载之前先给大家解释UIScrollView的几个属性 contentSize是UIScrollView可以滚动的区域. contentOfinset 苹果官方文档的解释是&qu ...
- 老调重弹:JDBC系列之<驱动加载原理全面解析) ----转
最近在研究Mybatis框架,由于该框架基于JDBC,想要很好地理解和学习Mybatis,必须要对JDBC有较深入的了解.所以便把JDBC 这个东东翻出来,好好总结一番,作为自己的笔记,也是给读者 ...
- composer 实现自动加载原理
简介 一般在框架中都会用到composer工具,用它来管理依赖.其中composer有类的自动加载机制,可以加载composer下载的库中的所有的类文件.那么composer的自动加载机制是怎么实现的 ...
- 深入解析 composer 的自动加载原理 (转)
深入解析 composer 的自动加载原理 转自:https://segmentfault.com/a/1190000014948542 前言 PHP 自5.3的版本之后,已经重焕新生,命名空间.性状 ...
- 解析苹果的官方例子LazyTableImages实现图片懒加载原理
解析苹果的官方例子LazyTableImages实现图片懒加载原理 首先在官网下载源码: https://developer.apple.com/library/ios/navigation/#sec ...
- Spring Boot源码分析-配置文件加载原理
在Spring Boot源码分析-启动过程中我们进行了启动源码的分析,大致了解了整个Spring Boot的启动过程,具体细节这里不再赘述,感兴趣的同学可以自行阅读.今天让我们继续阅读源码,了解配置文 ...
随机推荐
- 洛谷 P4714 「数学」约数个数和 解题报告
P4714 「数学」约数个数和 题意(假):每个数向自己的约数连边,给出\(n,k(\le 10^{18})\),询问\(n\)的约数形成的图中以\(n\)为起点长为\(k\)的链有多少条(注意每个点 ...
- 微信小程序之:wepy(二)
一大堆实例:人家的博客园 代码规范: 1.尽量使用驼峰命名,避免使用$开头,框架内建属性都已$开头,可以使用this直接调用. 2.入口文件.页面.组件后缀都为.wpy. 3.使用ES6语法开发. 4 ...
- Guest Editors’ Introduction: Special Issue on Advances in Management of Softwarized Networks
文章名称:Guest Editors’ Introduction:Special Issue on Advances in Management of Softwarized Networks 发表时 ...
- python3 动态import
有些情况下,需要动态的替换引入的包 1.常用的import方法 import platform import os 2.__import__ 动态引用 loop_manager = __import_ ...
- SQL Server TVPs 批量插入数据
在SQL Server 中插入一条数据使用Insert语句,但是如果想要批量插入一堆数据的话,循环使用Insert不仅效率低,而且会导致SQL一系统性能问题.下面介绍SQL Server支持的两种批量 ...
- windows bat 批处理 执行 for 循环无法执行?
示例: cmd 命令行可以执行.但是 写成 bat 却不能执行, for /f "delims==" %a in ('dir /b /s F:\F\*.TXT')do copy ...
- 【译】使用 Flutter 实现跨平台移动端开发
作者: Mike Bluestein | 原文地址:[https://www.smashingmagazine.com/2018/06/google-flutter-mobile-developm ...
- 微信小程序中使用 <web-view> 内嵌 H5 时,登录问题的处理方法
在微信小程序的开发中,经常遇到需要使用 <web-view></web-view> 内嵌 H5 的需求.在这种需求中比较棘手的问题应该就是登录状态的判断了,小程序中的登录状态怎 ...
- 记我在github上参与的Star增长最快的十万级项目。。。
前言 GitHub作为程序员的圣地. 用了两三年,一直都觉得,他可以代码托管,项目管理,为项目建立静态主页,个人简历,找工作,面试加分. 然而>>>....昨天才认识到我还是太年轻, ...
- Luogu P4358 密钥破解 题解报告
题目传送门 [题目大意] 给定一个正整数N,可以被分解为两个不同的质数p和q,计算出r=(p-1)*(q-1). 然后给出了一个小于r且与r互质的整数e,已知e*d≡1(mod r),求d. 最后给定 ...