webpack简单版 手写

243 阅读8分钟

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.jssum.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.实现逻辑

  1. 解析webpack.config.js ,读取入口信息与出口信息
  2. 根据entry信息,进行依赖分析,与内容分析,从run()方法开始
  3. 依赖分析 (根据内容递归读取,所有模块与依赖的图谱)
  4. 内容分析(使用 babel/parser的parse方法,生成抽象语法树AST)
  5. 使用@babel/traverse的traverse方法遍历AST,配合ImportDeclaration钩子函数,得到依赖的dependencies的数组(主要保存如a.js 对应是相对路径../a.js)
  6. 使用@babel/core 的transformFromAst方法输出code代码字符串。
  7. 把code 整合到自执行匿名函数里面。
  8. 生成chunk (包括依赖管理,依赖图谱)
  9. 输出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;"
   }
})