Webpack原理(2) — 加载原理

by Teobler on 19 / 02 / 2020

views

webpack的核心目的和功能就是打包JavaScript代码,在时间的推进过程中,其逐渐演化成了一个生态体系,成为前端打包代码和处理开发时候必不可少的一个工具。

本文首发于我的个人博客: https://teobler.com, 转载请注明出处, 文章中提到的所有源代码来自于https://github.com/thelarkinn/webpack-workshop-2018

NPM scripts

以下代码位于feature/01-fem-first-script分支

Install & Run

首先将代码clone到你本地,然后运行yarn install。既然文章讲的是webpack,那么问题来了,当你运行这个命令的时候,发生了什么?

在你运行yarn install这个命令的时候,首先会在你的node_modules目录下添加一个.bin文件夹,里面是一些二进制可执行文件(包括webpack它本身),这些文件可以被node_modules里面你下载的所有packages运行,里面的可执行文件都在npm这个作用域下才可执行。

比如当你直接运行webpack的时候,是会抛错的:

$ webpack
zsh: command not found: webpack

但是此时如果你在package.json文件中加入下面的代码:

"script" {
	"webpack": "webpack"
}

然后再运行npm run webpack此时将会以默认设置运行node_modules中的webpack(注:这里需要webpack4及以上版本),在这个script标签中NPM允许你在其作用域中运行任何合法的script,甚至是bash的script。

这时其实我们的代码库里面是没有任何webpack的配置的,其默认回去寻找项目中的src目录下的index文件,但是这时webpack会抛出一个warning,推荐你设置环境变量以使用不同环境下的默认设置

Compose Scripts

NPM script有一个强大的功能是能够将已有的命令合并起来,并且还可以提供额外的参数,比如我们可以加一个新的script去运行之前的webpack并在新的命令里传入参数,避免重复新增和修改之前的script :

"script" {
	"webpack": "webpack",
	"dev": "npm run webpack -- --mode development"
	// 这里的 -- 代表将后面的参数传入前面的命令中
}

以下代码位于feature/03-fem-debug-script分支

Debugging

我不知道各位读者是怎么debug一个Node程序的,至少对于我来说在这之前我完全依赖于console.log,其实node早已经为我们提供了一个方便的方式,只需要在package.json文件中加入一个script:

"script": {
	"debug": "node --inspect --inspect-brk /path/to/file/you/want/to/debug"
}

运行这段script,然后打开你的chrome,在地址栏输入chrome://inspect或者在新版本的chrome打开控制台,在左上角有一个Node的logo,点击即可打开Node专用的devtool。

如果此时我们将文件路径换成./node_modules/webpack/bin/webpack.js的话,我们就可以debug webpack了。你可以再devtool里看到webpcck是怎样被加载的,每一步是如何进行的等等。这个步骤不单单对了解webpack在做什么有用,如果你在编写自己的plugin或者loader的话,你可以用这种方式去做debug。这就是传说中的"debug driven development"。

First Module

src目录下加入一个新的文件foo.js,里面只写一个导出:

export default "foo";

然后在index.js里面将其import进来console出来:

import foo from "./foo";
 
console.log(foo);

然后直接运行npm run dev,这时你应该能看到webpack将两个文件打包成功,并且在项目目录下出现了一个新的dist文件夹,里面的main.js文件就是刚刚打包好的内容(这是webpack的默认配置)。在控制台运行node ./dist/main.js你就能看到foo被打印出来了。

如果每次修改都run一次script未免太麻烦,所以webpack还提供了watching mode,只需要在dev命令后加上--watch的flag,在每次修改文件保存后webpack就会自动重新打包最新的代码。

ES Module Syntax

在上面的例子中foo文件直接export default了,但是有一些情况我们的一个文件中可能包含多个部分:

// bar.js
export const firstPart = "first";
export const secondPart = "second";
 
// index.js
import foo from "./foo.js";
import { firstPart, secondPart } from "./bar.js";
 
console.log(foo, firstPart, secondPart);

CommonJS也是几乎相同的用法,但是不推荐在0202年的这个时间点使用CommonJS,毕竟其不支持tree-shaking等等功能(老旧系统当我没说)

Tree Shaking

在默认情况下webpack会在打包production代码的时候启用这个功能,这个功能能够舍弃那些我们没有用到的死代码,一个通用的例子是,你引入了lodash,但是只用到了里面的一个函数,那么只要你正确引入并使用(比如使用ES Module Syntax等等)了lodash,在最后打包的时候webpack就只会打包你使用过的函数而不是一整个lodash库。

Bundle Result

开始前请将代码切到feature/031-all-module-types分支并且在根目录下创建webpack.config.js文件,并在其中加上一下内容:

module.exports = {
    mode: "none"
};

之后直接运行npm run webpack然后你就能在dist目录下看到打包好的,没有经过任何处理的main.js文件,我们来看看webpack打包后的代码到底是什么样子的。

首先我们看到的是一个IFEE,这个IFEE就是webpack的运行时(runtime code),它接受了一个参数modules,这个modules在下面可以看到是一个数组,其实就是我们打包的各个文件的真实代码,只不过webpack帮我们做了一些处理,打包的时候可以在控制台看到一些index:

index-of-bundle-files

它们就对应代码里面注释里的一个个文件:

the-first-bundled-file

那么这段runtime code里面做了什么呢?

/******/ (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, {
/******/ 				configurable: false,
/******/ 				enumerable: true,
/******/ 				get: getter
/******/ 			});
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// 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 = 0);
/******/ })
/************************************************************************/
/******/ ([here, is, file, modules]);
 
// 篇幅所限,剩下的代码请到代码库中查看
  • 第3行设置了一个已经加载过的modules的cache变量
  • 第6行有一个require函数,它会检查传入的module是否存在于上面的cache中,如果存在的话就会直接return cache里面的exports字段,如果不存在就会创建一个module放入那个cache中(不过此时的exports字段是空的,并且没有被加载),之后将这个module传入另一个require函数中(实际上是直接call了这个module),执行完后将其标记为已加载,最后再把这个module的exports字段返回
  • 第37行与ESM的动态绑定有关,它是一个支持循环依赖的特性,本来应该在浏览器端实现的一个功能,之所以使用defineProperty是因为webpack将各个module的exports做了冻结处理,防止其被修改
  • 第48行的函数是为了与CommonJS交互,因为CommonJS没有default export,所以webpack使用了与TS相同的处理方式,在上面定义了一个常量标记这是一个CommonJS的module
  • 第53行的函数是为了与non-harmony的modules交互
  • 第69行就是webpack的执行入口,它将第一个module传入require方法执行了
  • 之后执行的就是真正的我们自己写的代码了,如果将打包后的代码和我们自己写的代码作对比,你能够很轻易的发现第78行到83行就是在做import,下面的函数就是在做console

所以这些代码就是我们在使用webpack build的时候所发生的一切了(当然,这仅仅是最简单的那部分)