Webpack原理(2) — 加载原理

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

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

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

NPM scripts

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.png

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

the-first-bundled-file.png

那么这段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的时候所发生的一切了(当然,这仅仅是最简单的那部分)

阅读 322

推荐阅读
想回山的猿
用户专栏

静静的写写东西,在这一年做好输出

1 人关注
8 篇文章
专栏主页