webpack工作原理

输出

首先新建项目及安装依赖

npm init -y
npm install --save-dev webpack
npm install --save-dev webpack-cli
复制代码

新建文件src/index.jssrc/hello.js,index.js为默认入口文件:

const sayHello = require('./hello')
console.log(sayHello('nick'))
复制代码

hello.js

module.exports = function(name) {
  return 'hello' + name
}
复制代码

在命令行运行npx webpack --mode=development,打开编译文件dist/main.js

 */
/******/ (() => { // webpackBootstrap
/******/ 	var __webpack_modules__ = ({

/***/ "./src/hello.js":
/*!**********************!*\
  !*** ./src/hello.js ***!
  \**********************/
/***/ ((module) => {

eval("module.exports = function(name) {\r\n  return 'hello' + name\r\n}\n\n//# sourceURL=webpack://commonJS/./src/hello.js?");

/***/ }),

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {

eval("const sayHello = __webpack_require__(/*! ./hello */ \"./src/hello.js\")\r\nconsole.log(sayHello('nick'))\n\n//# sourceURL=webpack://commonJS/./src/index.js?");

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// The module cache
/******/ 	var __webpack_module_cache__ = {};
/******/ 	
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/ 		// Check if module is in cache
/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
/******/ 		if (cachedModule !== undefined) {
/******/ 			return cachedModule.exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = __webpack_module_cache__[moduleId] = {
/******/ 			// no module.id needed
/******/ 			// no module.loaded needed
/******/ 			exports: {}
/******/ 		};
/******/ 	
/******/ 		// Execute the module function
/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ 	
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/ 	
/************************************************************************/
/******/ 	
/******/ 	// startup
/******/ 	// Load entry module and return exports
/******/ 	// This entry module can't be inlined because the eval devtool is used.
/******/ 	var __webpack_exports__ = __webpack_require__("./src/index.js");
/******/ 	
/******/ })()
;
复制代码

这里实际上外面包裹着一个IIFE(立即调用函数表达式)

(() => {
	var __webpack_modules__ = ({...})

	var __webpack_module_cache__ = {};

	function __webpack_require__(moduleId){...}

	var __webpack_exports__ = __webpack_require__("./src/index.js");
})()
复制代码

核心代码就是这些

  • webpack的打包结果就是一个IIFE,被称为webpackBootstrap
  • webpack_modules是一个模块加载函数,里面定义了两个函数,也就是我们打包的文件:./src/hello.js:(() => {})./src/index.js:(() => {})
  • 定义了webpack_module_cache缓存对象
  • webpack_require函数接受一个入口文件的路径作为参数,首先判断该参数是否在缓存对象中,如果在就返回缓存对象中的值;如果不在就先将参数写入缓存对象,随后创建一个新的module对象,并执行webpack_modules函数,实际上就是调用的是webpack_modules中定义的./src/index.js:(() => {})
工作流程

  • entry-options启动首先webpack会读取项目中的配置文件wenpack.config.js,或者从shell语句中获取必要的参数,这是webapck从内部接受业务信息的方式。
  • run 实例化 compiler用上一步得到的参数初始化Compiler对象,加载所配置的插件,执行对象的run方法开始执行编译
  • entry确定入口:根据配置中entry找出所有的入口文件
  • make编译模块:从入口文件出发,调用所有配置的loader对模块进行翻译,再递归找出该模块依赖的模块,
  • build module完成模块编译:经过上面使用loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及他们之间的依赖关系。
  • seal输出资源,根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个chunk转化成一个单独的文件加入到输出列表。
  • emit输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到系统文件
抽象语法树

在计算机科学中,抽象语法树(Abtract Syntax Tree,AST)是源码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源码中的一种结构和

之所以说语法是抽象的,是因为这里的语法并不会表示出真实语法中出现的每一个细节。

webpack将文件转化为AST的目的就是方便开发者提取模块文件中的关键信息,这样一来,我们就可以知晓开发者到底写了什么东西,也就可以根据这些写出来的进行分析和扩展。

可以在https://esprima.org/demo/parse.html#这个网站上将代码解析成AST树。

var answer = 6 * 7;
复制代码
{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "answer"
          },
          "init": {
            "type": "BinaryExpression",
            "operator": "*",
            "left": {
              "type": "Literal",
              "value": 6,
              "raw": "6"
            },
            "right": {
              "type": "Literal",
              "value": 7,
              "raw": "7"
            }
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}
复制代码
compiler和compilation

compiler和compilation这两个对象是webpack核心原理中最重要的概念,他们是理解webpack工作原理、loader和插件工作的基础。

  • compiler对象:它的实例包含了完整的webpack配置,且全局只有一个compiler实例,因此它就像webpack的骨架或神经中枢。当插件被实例化时,就会收到一个compiler对象,通过这个对象可以访问webpack的内部环境。
  • compilation对象:当webapck以开发模式运行时,每当检测到文件变化时,一个新的compilation对象就会被创建。这个对象包含了当前的模块资源、编译生成资源、变化的文件等信息。也就是说,所有构建过程中产生的构建数据都会被存储在该对象上,它也掌控着构建过程中的每一个环节。该对象还提供了很多事件回调供插件做扩展。

webpack的构建过程是通过compiler控制流程,通过compilation进行代码解析的。在开发插件时,我们可以从compiler对象中得到所有与webpack主环境的内容,包括事件钩子

compiler对象和compilation对象都继承自tapable库,该库暴露了所有和事件相关的发布订阅的方法。webpack中基于事件流的tapable库不仅能保证插件的有序性,还能使整个系统扩展性更好。

手写一个简易的webpack
项目初始化
mkdir wpk
npm init -y
复制代码

创建文件src/index.jssrc/greeting.js为我们的即将打包的业务代码。

创建文件lib/compiler.js复测编译,构建module输出文件, lib/index.js 实例化Compiler类,将配置参数传入 ,lib/parser.js 负责解析功能。

创建配置文件wpk.config.js

const path = require("path");
global.filename = path.join(__dirname,'./src')
module.exports = {
  entry: path.join(__dirname, "./src/index.js"),
  output: {
    path: path.join(__dirname, "./dist"),
    filename: "bundle.js",
  },
};
复制代码

这里定义了入口和出口

同时我们的业务代码

src/index.js

import { greeting } from "./greeting.js";

document.write(greeting("天王盖地虎"));
复制代码

src/greeting.js

export function greeting(name) {
  return "口令:" + name;
}
复制代码

这个项目的所需要的依赖

package.json

"dependencies": {
    "@babel/preset-env": "^7.15.6",
    "babel-core": "^6.26.3",
    "babel-preset-env": "^1.7.0",
    "babel-traverse": "^6.26.0",
    "babylon": "^6.18.0"
  }
复制代码

以及.babelrc

{
    "presets": [
        "@babel/preset-env"
    ]
}
复制代码
解析

项目初始化完成,首先完成parse.js的编写。

const fs = require("fs");
const babylon = require("babylon");

module.exports = {

  getAST: (path) => {
    const source = fs.readFileSync(path, "utf-8");

    return babylon.parse(source,{
        sourceType:'module'
    })
  },
  
};
复制代码

使用babylon,将文件解析成AST树。

Babylon 是 Babel 中使用的 JavaScript 解析器。

Babylon 根据 Babel AST 的格式 生成 AST 。它基于 ESTree 规范,具有以下差别(现在可以使用 estree 插件来取消掉这些差别):

新建测试文件,src/test.js

const path = require("path");
const { getAST} = require('./parser');

let ast = getAST(path.join(__dirname,'../src/index.js'))
console.log(ast)
复制代码

在命令行即可看见生成的AST,有了生成的AST,可以使用babel-traverse解析出文件所有的依赖

getDependencies: (ast) => {
    const dependencies = [];
    traverse(ast, {
      ImportDeclaration: ({ node }) => {
        dependencies.push(node.source.value);
      },
    });
    return dependencies;
  },
复制代码

接下来将ES6代码转化为ES5

transform: (ast) => {
    const { code } = transformFromAst(ast, null, {
      presets: ["env"],
    });
    return code;
  },
复制代码

parser.js中主要就三个方法:

  • getAST: 将获取到的模块内容 解析成AST语法树
  • getDependencies:遍历AST,将用到的依赖收集起来
  • transform:把获得的ES6AST转化成ES5
编译

接下来开始编写compiler.js,创建Compiler 类,完成以下功能

  • 接收wpk.config.js配置参数,并初始化entryoutput
  • 开启编译run方法。处理构建模块、收集依赖、输出文件等。
  • buildModule方法。主要用于构建模块(被run方法调用)
  • emitFiles方法。输出文件(同样被run方法调用)
const path = require("path");
const fs = require("fs");

module.exports = class Compiler {
  constructor(options) {
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
    this.modules = [];
  }
  // 开启编译
  run() {}
  // 构建模块相关
  buildModule(filename, isEntry) {
    // filename: 文件名称
    // isEntry: 是否是入口文件
  }
  // 输出文件
  emitFiles() {}
};
复制代码

因为我们的入口文件是讲配置文件当做参数传入的,new Compiler(options).run();,在构造函数中就初始化entryoutputmodules

开始构建模块

 run() {
    const entryModule = this.buildModule(this.entry, true);
    console.log(entryModule)
    this.modules.push(entryModule);
    this.modules.map((_module) => {
      _module.dependencies.map((dependency) => {
        this.modules.push(this.buildModule(dependency));
      });
    });
    console.log(this.modules);
  }

  buildModule(filename, isEntry) {
    let ast;
    if (isEntry) {
      ast = getAST(filename);
    } else {
      // const absolutePath = path.join(process.cwd(), './src',filename);
      const absolutePath = path.join(global.filename, filename);
      ast = getAST(absolutePath);
    }

    return {
      filename, // 文件名称
      dependencies: getDependencies(ast), // 依赖列表
      transformCode: transform(ast), // 转化后的代码
    };
  }
复制代码

buildModule函数根据传入的文件名称,将文件解析成AST,返回构建的module,module实际上就是一个包含文件名、依赖列表和转化后的代码的对象。

run函数中,将配置文件中定义的入口文件路径传入buildModule,随后将入口文件构建的module存入modules,开始递归遍历入口文件中所有的依赖,并将其构建成module。

有了所有的modules列表,将这些列表输出成一个文件。遍历modules列表,将所有的module转化成一个以文件名为名字的匿名函数,随后传入一个IIFE,这个IIFE完全仿照webpack4的输出文件

emitFiles() {
    const outputPath = path.join(this.output.path, this.output.filename);
    let modules = "";
    this.modules.map((_module) => {
      modules += `'${_module.filename}' : function(require, module, exports) {${_module.transformCode}},`;
    });

    const bundle = `
      (function(modules) {
        function require(fileName) {
          const fn = modules[fileName];
          const module = { exports:{}};
          fn(require, module, module.exports)
          return module.exports
        }
        require('${this.entry}')
      })({${modules}})
    `;
    // console.log(bundle)
    fs.writeFileSync(outputPath, bundle, "utf-8");
  }
复制代码

webpack4输出的IIFE

(function(modules) {
  // 已经加载过的模块
  var installedModules = {};

  // 模块加载函数
  function __webpack_require__(moduleId) {
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.l = true;
    return module.exports;
  }
  __webpack_require__(0);
})([
/* 0 module */
(function(module, exports, __webpack_require__) {
  ...
}),
/* 1 module */
(function(module, exports, __webpack_require__) {
  ...
}),
/* n module */
(function(module, exports, __webpack_require__) {
  ...
})]);
复制代码
  • webpack 将所有模块(可以简单理解成文件)包裹于一个函数中,并传入默认参数,将所有模块放入一个数组中,取名为 modules,并通过数组的下标来作为 moduleId
  • modules 传入一个自执行函数中,自执行函数中包含一个 installedModules 已经加载过的模块和一个模块加载函数,最后加载入口模块并返回。
  • __webpack_require__ 模块加载,先判断 installedModules 是否已加载,加载过了就直接返回 exports 数据,没有加载过该模块就通过 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 执行模块并且将 module.exports 给返回。

这个时候打开dist/bundle.js既可以看到打包完成的文件,新建dist/index.html,既可以看到页面输出的内容。

目前简易的webpack已经编写完成,美中不足的是正宗的webpack运行命令都是webpack,我们这里也来改进一下下。在package.json中添加

"bin": {
    "wpk": "lib/index.js" //入口文件
  },
复制代码

在根目录下运行npm link将指定文件链接到全局,直接wpk既可以完成打包。如果想使用npm run build,也可以在package.json中的script中添加"build": "wpk"即可。

完整代码

猜你喜欢

转载自juejin.im/post/7035794647813193742