由头
最近在做一个大型前端项目优化和重构,在初步梳理渲染链路的过程中,发现因为历史包袱产生了非常多了冗余依赖,这些依赖在首页很大程度上用不到,或者有大量的内容在首页的init状态下并不需要被加载进来。在这种情况下,进行合理的chunk拆分,配合良好的异步加载逻辑,就可以在减小资源体积这个层面上做出性能优化。
webpack的模块加载基本逻辑
webpack的模块从加载模式上可以分为同步和异步。
同步的模块例如入口文件, 或者使用多入口,splitChunk分包打出来的文件都是同步模块, 这样的模块需要业务逻辑代码想办法加载到HTML中来使其运行。 最典型的例子是入口文件,我们需要使用各种技巧(一般是构建插件),来将其注入到HTML文档中。
异步的模块则是在代码运行时动态加载进来的。最常见的就是使用 关键字导入的模块
import(/* webpackChunkName: "test" */'./test').then(result=>{
console.log(result.default);
})
当webpack打包到该依赖的时候,就会将该依赖自动打包到一个文件而不是原本所在的被引入文件产物中。这样的结果就是引用了这个模块的原模块文件大小会减小。自然地,如果可以活用移步模块,就可以在文件内容的颗粒度上对最终的产物进行控制, 减少依赖冗余。
JSONP
在阐述jsonp之前,先看一个html加载并运行脚本的简单例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>HTML 页面</div>
</body>
<script type="text/javascript">
var handleCallbackForLaster = (value) => {
console.log('param is',value)
}
</script>
<script type="text/javascript">
handleCallbackForLaster("I'm from another script")
</script>
</html>
毫无疑问,两个脚本将会依次执行,并且在控制台打印出 param is I'm from another script
那如果在脚本中, 通过操作DOM的方式添加一个新的script标签呢?结果也是一样的。
<script type="text/javascript">
var handleCallbackForLaster = (value) => {
console.log('param is',value)
}
const script = document.createElement('script');
script.innerHTML = `handleCallbackForLaster(\"I'm from new script\")`
document.body.appendChild(script)
</script>
如果在原本的脚本中,是通过设置src属性,并且添加到文档中,以此引入新的脚本以调用之前已经设置好的函数, 并且在函数参数中传递需要的数据,这种方式就是JSONP。
JSONP可以工作的基本原理
- HTML中,拥有src属性的标签,例如script, img等,他们发起的资源加载,是不受同源策略限制的。
- DOM编程接口中, 可以通过脚本操作DOM,在jsonp的例子中,也就是添加新的script标签到文档中。
- 新的script标签被添加到文档中, 会自动尝试加载脚本,加载完毕后执行。
一个更加实际的例子
客户端需要向服务端请求航班和票务相关的信息
客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>HTML 页面</div>
</body>
<script type="text/javascript">
var flightHandler = function(data){
alert(`你查询的航班结果是:航班号: ${data.code},票价:${data.price}元,余票:${data.tickets}张`)
}
// 提供jsonp的服务接口, 注意callback的param
var url = 'http://localhost:3000/jsonp/flightresult?code=CA1998&callback=flightHandler';
var script = document.createElement('script')
script.setAttribute('src',url);
// 将目标JS文件添加到文档中
document.getElementsByTagName('body')[0].appendChild(script)
</script>
</html>
服务端
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx) => {
if(ctx.request.path === '/jsonp/flightresult'){
const query = ctx.request.query;
const { code, callback } = query;
ctx.set("Content-Type", "application/javascript");
ctx.set('charset','utf-8')
// 目标JS文件的内容是一个函数调用, 函数名称从query参数中读取,在处理好数据后,通过将信息作为函数调用JSON格式的参数,通过客户端的js运行时传递给客户端
ctx.body = `${callback}(${JSON.stringify({ code :code,
price : 2000,
tickets : 100})})`
}
})
模拟webpack异步模块的加载
此处是最简单的demo实现,模拟的是webpacp编译后的加载基本原理,并非编译过程, 也非完善的webpack编译后产物。
webpack的异步模块,也是使用JSONP的原理加载的。通过这种方式,有效地分割依赖,实现异步按需加载资源。
我们可以模拟一个最简单的webpack异步模块加载, 假设我们有一个入口模块index.js和一个依赖模块title.js
// index.js
import(/* webpackChunkname: 'another' */'./title.js').then(res => {
console.log('titl value is',res.default)
}
// another.js
export default 'This is title'
在使用webpack编译后,我们会得到两个js文件。假设我们命名为index.js 和 title.js
// index.chunk.js
/**
* 保存所有的模块
* key : moduleId
* value : (module, exports, require) => void
*/
let modules = {};
/**
* 通过模块的Id引入模块函数
*/
function require(moduleId) {
let module = { exports: {} };
modules[moduleId].call(module.exports, module, module.exports, require);
return module.exports;
}
/**
* 已经ready的模块
*/
let installedChunks = {
main: 0,
title: 0,
};
/**
* 引入具体内容
*/
function requireValue(chunkId) {
let promises = [];
requestChunkByJSONP(chunkId, promises);
return Promise.all(promises);
}
function requestChunkByJSONP(chunkId, promises) {
let promise = new Promise((resolve, reject) => {
installedChunks[chunkId] = [resolve, reject];
});
promises.push(promise);
// 拼接好异步模块资源的url
let publicPath = "file:///Users/ludwig/Code/Erics-Blog";
var jsonpUrl = `${publicPath}/${chunkId}.js`;
// 使用jsonp请求目标chunk
let script = document.createElement("script");
script.setAttribute("src", jsonpUrl);
document.head.appendChild(script);
}
/**
* 给模块对象复制赋值
*/
function assignChunkValue(exports, chunkValues) {
for (let key in chunkValues) {
Object.defineProperty(exports, key, {
enumerable: true,
get: chunkValues[key],
});
}
}
/**
* 在主文件里定义好的JSONP回调函数
*/
function webpackJsonp(chunkId, asyncChunk) {
let resolves = [];
let chunkData = installedChunks[chunkId];
installedChunks[chunkId] = 0;
resolves.push(chunkData[0]);
for (let moduleId in asyncChunk) {
modules[moduleId] = asyncChunk[moduleId];
}
resolves.forEach((res) => res());
}
// step0: 业务代码, 引入需要的变量
requireValue("title")
.then(require.bind(require, "./title.js"))
.then((res) => {
console.log(res.default);
});
// title.js
// 异步模块调用webpackJsonp方法传递模块内容
webpackJsonp("title", {
"./title.js": (module, exports, require) => {
const __VALUE__ = "This is titlesssss";
assignChunkValue(exports, {
default: () => __VALUE__,
});
},
});
通过这样的JSONP引入方式, 就可以将异步chunk的信息添加进来
总结
JSONP是在web技术人员的工作实践中得到的解决跨域资源请求问题的方案。本质上是利用了html文档中的标签加载能力,和js脚本的执行顺序和函数调用顺序和传参,实现了数据的传递