1. 案例分析
./src/sum.js
exports.default = function(a,b) {return a + b}
./src/main.js
var sum = require('sum.js').default
console.log(sum(1 , 2))
由于 es5 不支持require 和 exports 方法,需要人为实现
1.1实现 exports ,其实就是个对象
//1.先定义全局的exports
var exports = {} ; //等价于window.exports = {}
//2.通过eval 执行 sum.js 里面定义的方法
eval('exports.default = function(a,b) {return a + b}')
// 测试
var fun = exports.default
console.log(func(10,20)) // 输出 30
而 require要做的事情,就是把上面的对象通过方法包裹然后返回
1.2实现 require ,其实就是个函数
const fs = require("fs");
function require(fliePath){ //通过传入路径,把路径下的js解析执行eval 并返回
let code = fs.readFileSync(fliePath, "utf-8"); // fs 拿到具体的js 的内容
eval(code) //这里直接eval('exports.default = function(a,b) {return a + b}') 会保留在window里
return exports
}
由于 eval(code)中 exports 使用的是全局window 会污染全局。使用局部变量+IIFE自执行函数,优化代码
function require(fliePath){ //通过传入路径,把路径下的js解析执行eval 并返回
let exports = {} // 定义局部变量对象,并做返回
let code = fs.readFileSync(fliePath, "utf-8"); // fs 拿到具体的js 的内容
;(function(exports,code) {
eval(code) //这里执行的时候 获取的事当前的传入的局部变量exports
})(exports,code)
return exports
}
测试
// 测试
var fun = require('./src/sum.js').default
console.log(func(10,20)) // 输出 30
1.3 生成文件与依赖图谱
由于webpack运行需要从入口文件开始,所以需要提前生成文件名与具体代码的map映射关系集合,以便require执行。
需要提前生成下个的结构数据。
{
"./src/main.js": {
"deps": { "./sum.js": "./src/sum.js" },
"code": "....."
},
"./src/add.js": {
"deps": {},
"code": "......"
}
}
然后require 从 /src/main.js开始找打关系 执行对应的code并且执行过程中,如果里面也包含requrie 语句,则 递归执行require方法,根据filePath 在map中找到deps和code,继续递归执行对应的文件名对应的代码,如上面的 sum.js和sum.js对应的code。
通过@babel/parser @babel/traverse 等工具可以拿到上面的集合。
1.4 基于依赖图谱map 改造requrie
需要在require代码外层加入依赖图谱map集合,并传入require函数做参数,以便在require执行的时候可以找到对应的代码
大概的逻辑代码
const map = {
"./src/main.js": {
"deps": { "./sum.js": "./src/sum.js" },
"code": "....."
},
"./src/add.js": {
"deps": {},
"code": "......"
}
}
(function(map){ //map 是一个集合
function require(fliePath){ //通过传入路径,把路径下的js解析执行eval 并返回
let exports = {} // 定义局部变量对象,并做返回
;(function(exports,code) {
eval(code) //这里里面有可能包含require语句,则进入递归执行效果
})(exports,map[filePath].code) //通过map拿到具体的js 的内容
return exports
}
require('./src/main.js') //从入口文件开始
})(map) //通过IIFE执行
1.5 路径优化
我们在代码内容默认是使用相对路径引入,如 ./a.js ,但是我们建立的关系图谱是用项目的根路径做key,所以需要把 路径key 如 ./a.js 根据 dependencies的记录转化为./src/a.js
const map = {
"./src/main.js": {
"deps": { "./sum.js": "./src/sum.js" },
"code": "....."
},
"./src/add.js": {
"deps": {},
"code": "......"
}
}
(function(graph){
function require(module){ //默认只处理绝对地址
function PathRequire(relativePath){//PathRequire 是为了解决,在代码内容默认是使用相对路径引入,如 ./sum.js,
//但是我们建立的关系图谱是用项目的根路径做key,所以需要把 路径key 如 ./sum.js 根据 dependencies的记录转化为./src/sum.js
return require(graph[module].dependencies[relativePath])
}
const exports = {};//这里声明exports 是code里面的代码赋值的对象,我们只需提前声明好
(function(require,exports,code){
eval(code) //这里是真正执行的代码,由于是闭包,如果由于需要用到require,exports,所以只能通过参数方式传入
})(PathRequire,exports,graph[module].code)
return exports;
}
require('./src/index.js')
})(map)
2.源码分析
//src/a.js
console.log("aaa");
export const str = "444"
//src/index.js
import { str } from "./a.js";
console.log("hello webpack");
//webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "main.js",
},
mode: "development",
};
//打包结果
/******/ (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;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./src/a.js":
/*!******************!*\
!*** ./src/a.js ***!
\******************/
/*! exports provided: str */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"str\", function() { return str; });\nconsole.log(\"aaa\");\r\nconst str = \"444\"\n\n//# sourceURL=webpack:///./src/a.js?");
/***/ }),
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ \"./src/a.js\");\n\nconsole.log(\"hello webpack\");\n\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
/******/ });
//输出两个自执行的匿名函数,并且定义了
(function(module){})([function(){},function(){}]);
//匿名函数 定义内容
function(modules) { // webpackBootstrap
// modules就是一个数组,元素就是一个个函数体,就是我们声明的模块
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
...
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// __webpack_public_path__
__webpack_require__.p = "";
// Load entry module and return exports
return __webpack_require__(0);
}
整个函数里就声明了一个变量installedModules 和函数__webpack_require__,并在函数上添加了一个m,c,p属性,m属性保存的是传入的模块数组,c属性保存的是installedModules变量,P是一个空字符串。最后执行__webpack_require__函数,参数为零,并将其执行结果返回。
//__webpack_require__的实现
function __webpack_require__(moduleId) {
//moduleId就是调用是传入的0
// installedModules[0]是undefined,继续往下
if(installedModules[moduleId])
return installedModules[moduleId].exports;
// module就是{exports: {},id: 0,loaded: false}
var module = installedModules[moduleId] = {
exports: {},
id: moduleId,
loaded: false
};
// 下面接着分析这个
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 表明模块已经载入
module.loaded = true;
// 返回module.exports(注意modules[moduleId].call的时候module.exports会被修改)
return module.exports;
}
3.实现逻辑
- 解析webpack.config.js ,读取入口信息与出口信息
- 根据entry信息,进行依赖分析,与内容分析,从run()方法开始
- 依赖分析 (根据内容递归读取,所有模块与依赖的图谱)
- 内容分析(使用 babel/parser的parse方法,生成抽象语法树AST)
- 使用@babel/traverse的traverse方法遍历AST,配合ImportDeclaration钩子函数,得到依赖的dependencies的数组(主要保存如a.js 对应是相对路径../a.js)
- 使用@babel/core 的transformFromAst方法输出code代码字符串。
- 把code 整合到自执行匿名函数里面。
- 生成chunk (包括依赖管理,依赖图谱)
- 输出bundle文件 (同时补全生成需要的常用方法 module export s require)
4.安装依赖库
利用babel解析 成抽象语法树AST
npm i @babel/core @babel/parser @babel/preset-env @babel/traverse -D
5.代码实现
//package.json
{
"name": "mini-webpack",
"version": "1.0.0",
"description": "",
"main": "bundle.js",
"directories": {
"lib": "lib"
},
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/parser": "^7.15.3",
"@babel/preset-env": "^7.15.0",
"@babel/traverse": "^7.15.0"
},
"scripts": {
"build": "node test.js"
},
"keywords": [],
"author": "",
"license": "ISC"
}
//test.js
// 读取配置
const options = require("./webpack.config.js");
// 引入webpack
const webpack = require("./webpack.js");
// webpack接收配置 启动入口函数,执行打包
new webpack(options).run();
//webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "main1.js",
},
mode: "development",
};
//src/index.js
import { str } from "./a.js";
console.log(`hello ${str}`);
//src/a.js
import { str2 } from "./b.js";
export const str = `test ${str2}`;
//src/b.js
export const str2 = "22222";
//webpack.js
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");
//@babel/preset-env
module.exports = class webpack {
constructor(options) {
const { entry, output } = options;
this.entry = entry;
this.output = output;
this.modules = [];
}
run() {
// 开发编译,执行打包
const info = this.parse(this.entry);
this.modules.push(info);
//这里通过 循环的时候动态添加数组,实现广度遍历
for (let i = 0; i < this.modules.length; i++) {
const item = this.modules[i];
const dependencies = item.dependencies;
if (dependencies) {
for (let j in dependencies) {
this.modules.push(this.parse(dependencies[j]));
}
}
}
// 数组结构转对象结构
const obj = {};
this.modules.forEach((item) => {
obj[item.entryFile] = {
dependencies: item.dependencies,
code: item.code,
};
});
this.file(obj);
}
//这里传入的参数必须是有效的相对完整路径 如: ./src/a.js
parse(entryFile) {
// 分析入口模块的内容
const content = fs.readFileSync(entryFile, "utf-8");
// 处理依赖
const ast = parser.parse(content, {
sourceType: "module",
});
const dependencies = {};
traverse(ast, {
ImportDeclaration({ node }) {
//./src/index.js
const pathName =
"./" + path.join(path.dirname(entryFile), node.source.value); //./a.js ./b.js
dependencies[node.source.value] = pathName;
},
});
// console.log(dependencies);
// 处理内容
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
return {
entryFile,
dependencies,
code,
};
}
file(code) {
// 生成代码内容 webpack启动函数
const filePath = path.join(this.output.path, this.output.filename);
const newCode = JSON.stringify(code);
const bundle = `(function(graph){
function require(module){
function PathRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
const exports = {};
(function(require,exports,code){
eval(code)
})(PathRequire,exports,graph[module].code)
return exports;
}
require('${this.entry}')
})(${newCode})`;
// 生成main.js 位置是./dist目录
fs.writeFileSync(filePath, bundle, "utf-8");
}
};
自执行匿名函数为:
(function(graph){
function require(module){
function PathRequire(relativePath){//PathRequire 是为了解决,在代码内容默认是使用相对路径引入,如 ./a.js,
//但是我们建立的关系图谱是用项目的根路径做key,所以需要把 路径key 如 ./a.js 根据 dependencies的记录转化为./src/a.js
return require(graph[module].dependencies[relativePath])
}
const exports = {};//这里声明exports 是code里面的代码赋值的对象,我们只需提前声明好
(function(require,exports,code){
eval(code) //这里是真正执行的代码,由于是闭包,如果由于需要用到require,exports,所以只能通过参数方式传入
})(PathRequire,exports,graph[module].code)
return exports;
}
require('./src/index.js')
})
最终编译结果
(function (graph) {
function require(modulePath) {
function PathRequire(relativePath) {
return require(graph[modulePath].dependencies[relativePath])
}
const exports = {};
(function (require, exports, code) {
eval(code)
})(PathRequire, exports, graph[modulePath].code)
return exports;
}
require('./src/index.js')
})({
"./src/index.js": {
"dependencies": {
"./a.js": "./src/a.js"
},
"code": "\"use strict\";\n\nvar _a = require(\"./a.js\");\n\nconsole.log(\"hello \".concat(_a.str));"
},
"./src/a.js": {
"dependencies": {
"./b.js": "./src/b.js"
},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.str = void 0;\n\nvar _b = require(\"./b.js\");\n\nvar str = \"webpack5 \".concat(_b.str2);\nexports.str = str;"
},
"./src/b.js": {
"dependencies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.str2 = void 0;\nvar str2 = \"!!!!\";\nexports.str2 = str2;"
}
})