webpack的核心目的和功能就是打包JavaScript代码,在时间的推进过程中,其逐渐演化成了一个生态体系,成为前端打包代码和处理开发时候必不可少的一个工具。
本文首发于我的个人博客: teobler.com, 转载请注明出处, 文章中提到的所有源代码来自于github.com/thelarkinn/…
NPM scripts
以下代码位于
feature/01-fem-first-script分支
Install & Run
首先将代码clone到你本地,然后运行yarn install。既然文章讲的是webpack,那么问题来了,当你运行这个命令的时候,发生了什么?
在你运行yarn install这个命令的时候,首先会在你的node_modules目录下添加一个.bin文件夹,里面是一些二进制可执行文件(包括webpack它本身),这些文件可以被node_modules里面你下载的所有packages运行,里面的可执行文件都在npm这个作用域下才可执行。
比如当你直接运行webpack的时候,是会抛错的:
1 |
$ webpack zsh: command not found: webpack |
但是此时如果你在package.json文件中加入下面的代码:
1 |
"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 :
1 |
"script" { "webpack": "webpack", "dev": "npm run webpack -- --mode development" // 这里的 -- 代表将后面的参数传入前面的命令中 } |
以下代码位于
feature/03-fem-debug-script分支
Debugging
我不知道各位读者是怎么debug一个Node程序的,至少对于我来说在这之前我完全依赖于console.log,其实node早已经为我们提供了一个方便的方式,只需要在package.json文件中加入一个script:
1 |
"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,里面只写一个导出:
1 |
export default "foo"; |
然后在index.js里面将其import进来console出来:
1 |
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了,但是有一些情况我们的一个文件中可能包含多个部分:
1 |
// 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文件,并在其中加上一下内容:
1 |
module.exports = { mode: "none" }; |
之后直接运行npm run webpack然后你就能在dist目录下看到打包好的,没有经过任何处理的main.js文件,我们来看看webpack打包后的代码到底是什么样子的。
首先我们看到的是一个IFEE,这个IFEE就是webpack的运行时(runtime code),它接受了一个参数modules,这个modules在下面可以看到是一个数组,其实就是我们打包的各个文件的真实代码,只不过webpack帮我们做了一些处理,打包的时候可以在控制台看到一些index:

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

那么这段runtime code里面做了什么呢?
1 |
/******/ (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的时候所发生的一切了(当然,这仅仅是最简单的那部分)