Webpack打包文件

TIP

本节主要分析一下webpack的打包文件,包含同步模块加载和异步模块加载

一 配置环境

1 安装文件

npm init -y

npm i webpack webpack-cli html-webpack-plugin clean-webpack-plugin -D

mkdir webpack.config.js

2 编写文件

// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
	mode: "development",
	devtool: "source-map",
	entry: "./src/index.js",
	output: {
		path: path.resolve(__dirname, "dist"),
		filename: "[name].js",
		chunkFilename:'[name].main.js',
		//publicPath:'/'
	},
	module: {},
	plugins: [
		new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ["**/*"] }),
		new HtmlWebpackPlugin({
			template: "./src/index.html",
			filename: "index.html",
		}),
	],
	devServer: {},
};

// src\title.js
module.exports = {
    name:'title_name',
    age:'title_age'
}

// src\index.js
let title = require('./title')
console.log(title);

// src\index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>webpack</title>
  </head>
  <body></body>
</html>

// package.json
"scripts": {
    "build": "webpack"
}

3 添加删除注释插件

vscode添加插件 remove-js-comments

4 打包文件

// dist/main.js
;(() => {
	var __webpack_modules__ = {
		"./src/title.js": (module) => {
			module.exports = {
				name: "title_name",
				age: "title_age",
			}
		},
	}
	var __webpack_module_cache__ = {}
	function __webpack_require__(moduleId) {
		if (__webpack_module_cache__[moduleId]) {
			return __webpack_module_cache__[moduleId].exports
		}
		var module = (__webpack_module_cache__[moduleId] = {
			exports: {},
		})
		__webpack_modules__[moduleId](module, module.exports, __webpack_require__)
		return module.exports
	}
	;(() => {
		let title = __webpack_require__("./src/title.js")
		console.log(title)
	})()
})()

二 同步模块加载

  • webpack内部自己实现了一套CommonJS规范,所以天然能识别require、exports、module.exports
  • webpack在CommonJS与ES6 modules模块的加载有四种情况,如下:

1 CommonJS 加载 CommonJS

  • 代码编写
// src/index.js - CommonJS导入
let title = require('./title')
console.log(title );

// src/title.js  - CommonJS导出
module.exports = {
    name:'title_name',
    age:'title_age'
}
  • 加载原理
(() => {
	// 1. 定义模块对象
	var modules = {
		"./src/title.js": (module) => {
			module.exports = {
				name: "title_name",
				age: "title_age",
			}
		},
	}
	// 2. 定义缓存对象
	var cache = {}
	// 3. 定义require方法
	function require(moduleId) {
		// 3.1 先查看缓存是否已经存在,如果已经存在就返回
		if (cache[moduleId]) {
			return cache[moduleId].exports
		}
		// 3.2 缓存没有就定义空对象,两个变量指向同一个引用对象
		// var module = {exports:{}};
		// cache[moduleId]= module;
		var module = cache[moduleId] = {
			exports: {},
		};
		// 3.3 执行模块对象的函数,然后给module.exports赋值
		modules[moduleId].call(module.exports, module, module.exports, require);
		// 3.4 返回结果
		return module.exports
	}
	// 4. 自执行函数定义模块执行入口
	(() => {
		let title = require("./src/title.js")
		console.log(title)
	})()
})()

2 CommonJS 加载 ES6 modules

  • 如果原来是ES6 modules会统一变成CommonJS
  • export default会变成exports.default
  • export xx 保持不变,还是为 exports.xx
  • 代码编写
// src/index.js - CommonJS导入
let title = require('./title')
console.log(title );

// src/title.js  - ES6 modules导出
export default 'title_name';
export const age = 'title_age';
  • 加载原理
(() => {
	// 1. 定义模块对象
	var modules = {
		"./src/title.js": (module, exports, require) => {
			// 如果是es6的模块,就调用r方法加入es6的模块标识
			require.r(exports);
			// 将es6模块中的default默认导出加入一个default的key,value全部变为函数
			require.d(exports, {
				default: () => DEFAULT_EXPORT,
				age: () => age,
			})
			const DEFAULT_EXPORT = "title_name"
			const age = "title_age"
		},
	}
	// 2. 定义缓存对象
	var cache = {}
	// 3. 定义require方法
	function require(moduleId) {
		// 3.1 先查看缓存是否已经存在,如果已经存在就返回
		if (cache[moduleId]) {
			return cache[moduleId].exports
		}
		// 3.2 缓存没有就定义空对象,两个变量指向同一个引用对象
		// var module = {exports:{}};
		// cache[moduleId]= module;
		var module = cache[moduleId] = {
			exports: {},
		};
		// 3.3 执行模块对象的函数,然后给module.exports赋值
		modules[moduleId].call(module.exports, module, module.exports, require);
		// 3.4 返回结果
		return module.exports
	}
	// 浅拷贝,将需要加载的模块执行完成之后全部放到module.exports对象上
	require.d = (exports, definition) => {
		for (var key in definition) {
			// exports[key]=definition[key]();
			Object.defineProperty(exports, key, {
				enumerable: true,
				get: definition[key],
			})
		}
	}
	// 给module.exports对象上加入ES6的模块标识
	require.r = (exports) => {
		 //console.log(Object.prototype.toString.call(exports));//[object Module]
    	//exports.__esModule=true 
		Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
		Object.defineProperty(exports, "__esModule", { value: true })
	}

	// 4. 自执行函数定义模块执行入口
	(() => {
		let title = require("./src/title.js")
		console.log(title)
		console.log(title.age)
	})()
})()

3 ES6 modules 加载 ES6 modules

  • 代码编写
// src/index.js - ES6 modules导入
import name, { age } from "./title";
console.log(name);
console.log(age);

// src/title.js  - ES6 modules导出
export default 'title_name';
export const age = 'title_age';
  • 加载原理
(() => {
	// 1. 定义模块对象
	var modules = {
		"./src/index.js": (module, exports, require) => {
			require.r(exports)
			var title = require("./src/title.js")
			console.log(title.default)
			console.log(title.age)
		},
		"./src/title.js": (module, exports, require) => {
			// 如果是es6的模块,就调用r方法加入es6的模块标识
			require.r(exports);
			// 将es6模块中的default默认导出加入一个default的key,value全部变为函数
			require.d(exports, {
				default: () => DEFAULT_EXPORT,
				age: () => age,
			})
			const DEFAULT_EXPORT = "title_name"
			const age = "title_age"
		},
	}
	// 2. 定义缓存对象
	var cache = {}
	// 3. 定义require方法
	function require(moduleId) {
		// 3.1 先查看缓存是否已经存在,如果已经存在就返回
		if (cache[moduleId]) {
			return cache[moduleId].exports
		}
		// 3.2 缓存没有就定义空对象,两个变量指向同一个引用对象
		// var module = {exports:{}};
		// cache[moduleId]= module;
		var module = cache[moduleId] = {
			exports: {},
		};
		// 3.3 执行模块对象的函数,然后给module.exports赋值
		modules[moduleId].call(module.exports, module, module.exports, require);
		// 3.4 返回结果
		return module.exports
	}
	// 浅拷贝,将需要加载的模块执行完成之后全部放到module.exports对象上
	require.d = (exports, definition) => {
		for (var key in definition) {
			// exports[key]=definition[key]();
			Object.defineProperty(exports, key, {
				enumerable: true,
				get: definition[key],
			})
		}
	}
	// 给module.exports对象上加入ES6的模块标识
	require.r = (exports) => {
		 //console.log(Object.prototype.toString.call(exports));//[object Module]
    	//exports.__esModule=true 
		Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
		Object.defineProperty(exports, "__esModule", { value: true })
	}

	// 4. 自执行函数定义模块执行入口
	(() => {
		require("./src/index.js")
	})()
})()

4 ES6 modules 加载 CommonJS

  • 代码编写
// src/index.js - ES6 modules导入
import name, { age } from "./title";
console.log(name);
console.log(age);

// src/title.js  - CommonJS导出
module.exports = {
    name:'title_name',
    age:'title_age'
}
  • 加载原理
(() => {
	// 1. 定义模块对象
	var modules = {
		"./src/index.js": (module, exports, require) => {
			require.r(exports)
			var title = require("./src/title.js")
			// 这一步还不知道是ES6调用还是CommonJS调用,需要封装一个函数
			var title_default = require.n(title)
			console.log(title_default())
			console.log(title.age)
		},
		"./src/title.js": (module, exports, require) => {
			module.exports = {
				name:'title_name',
				age:'title_age'
			}
		},
	}
	// 2. 定义缓存对象
	var cache = {}
	// 3. 定义require方法
	function require(moduleId) {
		// 3.1 先查看缓存是否已经存在,如果已经存在就返回
		if (cache[moduleId]) {
			return cache[moduleId].exports
		}
		// 3.2 缓存没有就定义空对象,两个变量指向同一个引用对象
		// var module = {exports:{}};
		// cache[moduleId]= module;
		var module = cache[moduleId] = {
			exports: {},
		};
		// 3.3 执行模块对象的函数,然后给module.exports赋值
		modules[moduleId].call(module.exports, module, module.exports, require);
		// 3.4 返回结果
		return module.exports
	}
	// 判断是ES6的模块还是CommonJS模块,然后返回对应的函数
	require.n = (exports)=>{
		var getter = exports.__esModule ? () => exports.default : () => exports;
		return getter;
	}
	// 浅拷贝,将需要加载的模块执行完成之后全部放到module.exports对象上
	require.d = (exports, definition) => {
		for (var key in definition) {
			// exports[key]=definition[key]();
			Object.defineProperty(exports, key, {
				enumerable: true,
				get: definition[key],
			})
		}
	}
	// 给module.exports对象上加入ES6的模块标识
	require.r = (exports) => {
		 //console.log(Object.prototype.toString.call(exports));//[object Module]
    	//exports.__esModule=true 
		Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
		Object.defineProperty(exports, "__esModule", { value: true })
	}

	// 4. 自执行函数定义模块执行入口
	(() => {
		require("./src/index.js")
	})()
})()

三 异步加载

  • module 就是src源码里面编写的每一个文件模块

  • chunk 就是webpack在打包过程中分包的依据

  • bundle 就是webpack打包之后最终输出的文件

  • 代码编写

// webpack.config.js
module.exports = {
	output: {
		path: path.resolve(__dirname, "dist"),
		filename: "[name].js",
		chunkFilename:'[name].main.js',
	}
};

// src/index.js
// 如果不加webpackChunkName,打包生成的文件就是src-hello.main.js,加了就是hello.main.js
import(/* webpackChunkName: "hello" */ './hello').then(result => {
	console.log(result.default);
});

// src/hello.js
export default 'hello';
  • 原理分析
// main.js
(() => {
	// 存放着所有的模块定义,包括懒加载,或者说异步加载过来的模块定义
	var modules = {}
	// 定义缓存对象
	var cache = {}
	// 定义require方法
	function require(moduleId) {
		// a 先查看缓存是否已经存在,如果已经存在就返回
		if (cache[moduleId]) {
			return cache[moduleId].exports
		}
		// b 缓存没有就定义空对象,两个变量指向同一个引用对象
		// var module = {exports:{}};
		// cache[moduleId]= module;
		var module = cache[moduleId] = {
			exports: {},
		};
		// c 执行模块对象的函数,然后给module.exports赋值
		modules[moduleId].call(module.exports, module, module.exports, require);
		// d 返回结果
		return module.exports
	}
	// 浅拷贝,将需要加载的模块执行完成之后全部放到module.exports对象上
	require.d = (exports, definition) => {
		for (var key in definition) {
			// exports[key]=definition[key]();
			Object.defineProperty(exports, key, {
				enumerable: true,
				get: definition[key],
			})
		}
	}
	// 给module.exports对象上加入ES6的模块标识
	require.r = (exports) => {
		 //console.log(Object.prototype.toString.call(exports));//[object Module]
    	//exports.__esModule=true 
		Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
		Object.defineProperty(exports, "__esModule", { value: true })
	}

	// 2.创建promise,发起jsonp请求
	require.e = chunkId => {
		let promises = [];
		require.f.j(chunkId, promises);
		return Promise.all(promises);
	}
	//已经安装的代码块 main代码块的名字:0表示已经就绪
	let installedChunks = {
		main: 0,
		hello: 0
	}
	require.p = ''; // 配置文件中的publicPath 资源访问路径
	require.u = chunkId => { // 拼接请求代码块名称
		return chunkId + '.main.js';
	}
	require.f = {};
	// 3.通过jsonp异步加载chunkId,也就是hello这个代码块
	require.f.j = (chunkId, promises) => {
		let promise = new Promise((resolve, reject) => {
			installedChunks[chunkId] = [resolve, reject];
		});
		promises.push(promise);
		// 组装异步模块的url
		var url = require.p + require.u(chunkId); // /hello.main.js
		require.l(url);
	}
	// 4.通过JSONP请求这个新的url地址  http://127.0.0.1:8080/hello.main.js
	require.l = url => {
		let script = document.createElement('script');
		script.src = url;
		document.head.appendChild(script); // 一旦添加head里,浏览器会立刻发出请求
	}

	// 6.开始执行回调
	var webpackJsonpCallback = ([chunkIds, moreModules]) => {
		// 拿到resolve数组
		let resolves = chunkIds.map(chunkId => installedChunks[chunkId][0]);
		// 把异步加载回来的额外代码块合并到总的模块定义对象modules上
		for(let moduleId in moreModules) {
			modules[moduleId] = moreModules[moduleId];
		}
		// 将promises全部执行成功
		resolves.forEach(resolve => resolve());
	}

	// 0 定义全局空数组window["webpack5"],然后重新push方法
	var chunkLoadingGlobal = window["webpack5"] = [];
	chunkLoadingGlobal.push = webpackJsonpCallback;

	// 1.准备加载异步代码块hello
	require.e("hello").then(require.bind(require, "./src/hello.js")).then(result => {
		console.log(result.default);
	})
})()
// hello.main.js
// 5.执行window["webpack5"]上的push方法,传递参数[chunkIds,moreModules]
;(window["webpack5"] = window["webpack5"] || []).push([
	["hello"],
	{
		"./src/hello.js": (module, exports, require) => {
			"use strict"
			require.r(exports)
			require.d(exports, {
				default: () => DEFAULT_EXPORT,
			})
			const DEFAULT_EXPORT = "hello"
		},
	},
])