Code Splitting是webpack的一个重要特性,他允许你将代码打包生成多个bundle。对多页应用来说,它是必须的,因为必须要配置多个入口生成多个bundle;对于单页应用来说,如果只打包成一个bundle可能体积很大,导致无法利用浏览器并行下载的能力,且白屏时间长,也会导致下载很多可能用不到的代码,每次上线用户都得下载全部代码,Code Splitting能够将代码分割,实现按需加载或并行加载多个bundle,可利用并发下载能力,减少首次访问白屏时间,可以只上线必要的文件。


三种Code Splitting方式

webpack提供了三种方式来切割代码,分别是:

  1. 多entry方式
  2. 公共提取
  3. 动态加载

    本文将简单介绍多entry方式和公共提取方式,重点介绍的是动态加载。这几种方式可以根据需要组合起来使用。这里是官方文档,中文 英文

多entry方式

这种方式就是指定多个打包入口,从入口开始将所有依赖打包进一个bundle,每个入口打包成一个bundle。此方式特别适合多页应用,我们可以每个页面指定一个入口,从而每个页面生成一个js。此方式的核心配置代码如下:

const path = require('path');

module.exports = {
mode: 'development',
entry: {
page1: './src/page1.js',
page2: './src/page2.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};

上边的配置最终将生成两个bundle, 即page1.bundle.js和page2.bundle.js。

公共提取

这种方式将公共模块提取出来生成一个bundle,公共模块意味着有可能有很多地方使用,可能导致每个生成的bundle都包含公共模块打包生成的代码,造成浪费,将公共模块提取出来单独生成一个bundle可有效解决这个问题。这里贴一个官方文档给出的配置示例:

  const path = require('path');

  module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
// 关键
optimization: {
splitChunks: {
chunks: 'all'
}
}
};

这个示例中index.js和another-module.js中都import了loadsh,如果不配置optimization,将生成两个bundle, 两个bundle都包含loadsh的代码。配置optimization后,loadsh代码被单独提取到一个vendorsanotherindex.bundle.js。

动态加载

动态加载的含义就是讲代码打包成多个bundle, 需要用到哪个bundle时在加载他。这样做的好处是可以让用户下载需要用到的代码,避免无用代码下载。确定是操作体验可能变差,因为操作之后可能还有一个下载代码的过程。关于动态加载,后面详解。


实现一个简单的动态加载

动态加载就是要实现可以在代码里边去加载其他js,这个太简单了,新建script标签插入dom就可以了,如下:

function loadScript(url) {
const script = document.createElement('script');
script.src = url;
document.head.appendChild(script);
}

只需要在需要加载某个js时调用即可,例如需要点击按钮时加载js可能就如下边这样。

btn.onClick = function() {
console.log('1');
loadScript('http://abc.com/a.js');
}

看上去非常简单,事实上webpack也是这么做的,但是他的处理更加通用和精细。


webpack动态加载

webpak打包出来的代码怎么执行

现有一个文件test2.js, 其中代码为

console.log('1')

此文件通过webpack打包后输出如下,删除了部分代码,完整版可自己尝试编译一个,也可查看web-test(这个项目是基于react,express,webpack的用于web相关实验的项目,里边使用了code splitting方案来基于路由拆分代码,与code splitting相关的实验放在test-code-split分支)。

(function (modules) { // webpackBootstrap
// The module cache
var installedModules = {}; // The require function
function __webpack_require__(moduleId) { // Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
}; // Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Flag the module as loaded
module.l = true; // Return the exports of the module
return module.exports;
}
return __webpack_require__(__webpack_require__.s = "./test2.js");
})
({ "./test2.js":
(function (module, exports, __webpack_require__) { "use strict";
eval("\n\nconsole.log('1');\n\n//# sourceURL=webpack:///./test2.js?"); }) });

不知大家是不是跟大雄一样之前从未看过webpack编译产出的代码。其实看一下还是挺有趣的,原来我们的代码是放在eval中执行的。细看下这段代码,其实并不复杂。他是一个自执行函数,参数是一个对象,key是模块id(moduleId), value是函数,这个函数是里边是执行我们写的代码,在自执行函数体内是直接调用了一个__webpack_require__,参数就是入口moduleId, __webpack_require__方法里值执行给定模块id对应的函数,核心代码是modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

上面是没有import命令的情况,对于有import命令的情况,产出和上边类似,只是自执行函数的参数有变化。例如:

// 入口文件test2.js
import './b.js'
console.log('1')
// b.js
console.log('b')

这段代码产出的自执行函数里边的参数如下:

// 自执行函数里边的参数
{ "./b.js":
(function (module, exports, __webpack_require__) { "use strict";
eval("\n\nconsole.log('b');\n\n//# sourceURL=webpack:///./b.js?");
}), "./test2.js":
(function (module, exports, __webpack_require__) { "use strict";
eval("\n\n__webpack_require__(/*! ./b.js */ \"./b.js\");\n\nconsole.log('1');\n\n//# sourceURL=webpack:///./test2.js?");
})
}

./test2.js这个moduleId对应的函数的eval里边调用了__webpack_require__方法,为了看起来方便,将eval中的字符串拿出来,如下

__webpack_require__("./b.js");
console.log('1');

原来import命令在webpack中就是被转换成了__webpack_require__的调用。太奇妙了,但是话说为啥模块里边为啥要用eval来执行我们写的代码,大雄还是比较困惑的。

webpack动态code splitting方案

经过一番铺垫,终于到主题了,即webpack是如何实现动态加载的。前文大雄给了一个粗陋的动态加载的方法--loadScript, 说白了就是动态创建script标签。webpack中也是类似的,只是他做了一些细节处理。本文只介绍主流程,具体实现细节大家可以自己编译产出一份代码进行研究。

首先需要介绍在webpack中如何使用code splitting,非常简单,就像下边这样

import('lodash').then(_ => {
// Do something with lodash (a.k.a '_')...
});

我们使用了一个import()方法, 这个import方法经过webpack打包后类似于前文提到的loadScript, 大家可以参看下边的代码:

__webpack_require__.e = function requireEnsure(chunkId) {
var promises = []; // JSONP chunk loading for javascript var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 means "already installed". // a Promise means "currently loading".
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise); // start chunk loading
var script = document.createElement('script');
var onScriptComplete; script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId); onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};

是不是非常熟悉,代码中也调用了document.createElement('script')来创建script标签,最后插入到head里。这段代码所做的就是动态加载js,加载失败时reject,加载成功resolve,这里并不能看到resolve的情况,resolve是在拆分出去的代码里调用一个全局函数实现的。拆分出的js如下:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{

/***/ "./b.js":
/*!**************!*\
!*** ./b.js ***!
\**************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) { "use strict";
eval("\n\nconsole.log('b');\n\n//# sourceURL=webpack:///./b.js?"); /***/ }) }]);

在webpackJsonp方法里调用了对应的resolve,具体如下:

function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1]; // add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data); while(resolves.length) {
resolves.shift()();
} };

这里的挂到全局的webpackJsonp是个数组,其push方法被改为webpackJsonpCallback方法的数组。所以每次在执行webpackJsonp时实际是在调用webpackJsonpCallback方法。

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i])

总结起来,webpack的动态加载流程大致如下:

总结

本文对webpack打包出的代码的结构和执行过程作了简单分析,介绍了webpack中code splitting的几种方式,重点分析了一下动态加载的流程。分析的不一定完全正确,大家可以自己使用webpack打包产出代码进行研究,一定会有所收获。大雄看完至少大概知道了原来webpack编出来的代码是那样执行的、Promise原来可以那么灵活的使用。


大雄在学习web开发或在项目中遇到问题时经常需要做一些实验, 在react出了什么新的特性时也常常通过做实验来了解一下. 最开始常常直接在公司的项目做实验, 直接拉个test分支就开搞, 这样做有如下缺点:

  • 在公司的项目去做实验本身就是一件不好的事情
  • 公司的项目里边只有前端的部分, 想要做接口有关的实验不方便. 例如想测试跨域的响应头Access-Control-Allow-Origin就得再启一个web服务器
  • 实验过的东西零散, 过一段时间想查找却找不到了

基于以上原因, 特搭建了个基于react,webpack,express的用于web开发相关实验的项目web-test.欢迎使用。

webpack Code Splitting浅析的更多相关文章

  1. [Webpack 2] Maintain sane file sizes with webpack code splitting

    As a Single Page Application grows in size, the size of the payload can become a real problem for pe ...

  2. [转] react-router4 + webpack Code Splitting

    项目升级为react-router4后,就尝试着根据官方文档进行代码分割.https://reacttraining.com/react-router/web/guides/code-splittin ...

  3. react-router4 + webpack Code Splitting

    项目升级为react-router4后,就尝试着根据官方文档进行代码分割.https://reacttraining.com/react-router/web/guides/code-splittin ...

  4. webpack code splitting

    一.代码分割优化 const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin' ...

  5. webpack优化之code splitting

    作为当前风头正盛的打包工具,webpack风靡前端界.确实作为引领了一个时代的打包工具,很多方面都带来了颠覆性的改进,让我们更加的感受到自动化的快感.不过最为大家诟病的一点就是用起来太难了. 要想愉快 ...

  6. webpack 利用Code Splitting 分批打包、按需下载

    webpack中的解决方案——Code Splitting,简单来说就是按需加载(下载),如果是requireJS对应的AMD的方案中这本是在正常不过了.但是在webpack中All in one的思 ...

  7. webpack 和 code splitting

    Code Splitting指的是代码分割,那么什么是代码分割,webpack和code splitting又有什么样的联系呢? 使用npm run dev:"webpack-dev-ser ...

  8. webpack async load modules & dynamic code splitting

    webpack async load modules & dynamic code splitting webpack 按需/异步加载/Code Splitting webpack loade ...

  9. 借助Code Splitting 提升单页面应用性能

    近日的工作集中于一个单页面应用(Single-page application),在项目中尝试了闻名已久的Code splitting,收获极大,特此分享. Why we need code spli ...

随机推荐

  1. id为194024的进程当前未运行

    .NET MVC 编译运行时 提示  >> id为194024的进程当前未运行 << 清理解决方案,重新运行

  2. python3学习笔记4---引用http://python3-cookbook.readthedocs.io/zh_CN/latest/

    2018-03-01数据结构与算法(4) 1.16过滤序列元素 最简单的过滤序列元素的方法就是使用列表推导.比如: >>> mylist = [1, 4, -5, 10, -7, 2 ...

  3. jqery对于select级联操作

    问题:今天在做一个需求的时候,有一个级联操作也就是选中下拉框的一列就显示对对应的数据 处理:我在做级联的时候在option的列里面绑定click的事件发现这个事件行不通:查资料发现select触发的是 ...

  4. 和同门一起做的PHP网站

    用的是PHP语言开发,使用ThinkPHP框架,数据库是mysql  统计是用的站长统计,前端很多都是别人网 上扒下来的.css.js水平不高啊.服务器是香港恒创科技的,性价比还可以哦,而且还免备案. ...

  5. Gradle连接Maven仓库直接从仓库 更新JAR包

    一般情况下服务器编写好服务程序  会用Maven打成JAR包,放在Maven仓库里管理,我们在用的时候直接引用就可以, 那么如何在Gradle项目中使用本地的  或者远程的Maven仓库呢    当M ...

  6. 关于mysql中的DDL,DML,DQL和DCL

    SQL语言一共分为4大类:数据定义语言DDL,数据操纵语言DML,数据查询语言DQL,数据控制语言DCL 1.数据定义语言DDL(Data Definition Language) 对象: 数据库和表 ...

  7. jsp 条件查询、列表分页

    条件查询 dao //根据搜索条件筛选数据 public List<User> GetUserBySearch(String userName, String sex) throws SQ ...

  8. 使用XHProf分析PHP性能瓶颈(二)

    上一篇文章里,我们介绍了如何基于xhprof扩展来分析PHP性能,并记录到日志里,最后使用xhprof扩展自带的UI在web里展示出来.本篇文章将讲述2个知识点: 使用xhgui代替xhprof的默认 ...

  9. Windows安装TensorFlow

    1.下载安装Anaconda 官方地址:https://www.continuum.io/downloads/镜像地址:https://mirrors.tuna.tsinghua.edu.cn/ana ...

  10. Preload,Prefetch 和它们在 Chrome 之中的优先级

    前言 上周五到的时候,想起之前在手游平台上有处理dns-prefetch的优化,那这篇分享的就更仔细了.今日早读文章由@gy134340翻译并授权分享. 正文从这开始- 今天我们来深入研究一下 Chr ...