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:
它们就对应代码里面注释里的一个个文件:
那么这段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的时候所发生的一切了(当然,这仅仅是最简单的那部分)