本系列已集结成书从 0 到 Webpack:学习 Modern Web 专案的建置方式,这是一本完整介绍 Webpack 的专书,如有学习 Webpack 相关的问题可以参考此书。
本文目标在於实作一个简易的打包工具。
本文的范例程序放在 peterhpchen/webpack-quest 中,每个程序码区块的第一行都会标注档案的位置,请搭配文章作参考。
webpack 是个拥有强大功能的工具,本文将尝试自己实作 webpack 的核心功能:打包,接着跟着我一起试试写个简易版的打包器吧。
本文 Ronen Amiel 的 Build Your Own Webpack 启发,因此实作方式会以 Ronen Amiel 的 minipack
方式做展示,与 webpack 的打包方式比起来, minipack
化繁为简,对於初学打包技巧的开发者会是比较好入门的方式。
我们使用下面这个例子做打包的示范:
// ./src/index.js
import message from "./message.js";
console.log(message);
// ./src/message.js
import { demoName } from "./demoName.js";
export default `hello ${demoName}`;
// ./src/demoName.js
export const demoName = "simple";
这个范例的模组相依图(Module Graph)如下图:
之後下面所有的展示都以上面的代码作为打包对象。
在自己动手前,先来观察 webpack 是如何打包的,现在执行建置後, webpack 会产出 ./dist/main.js
,内容如下:
// ./demos/simple/dist/main.js
(function (modules) {
// webpackBootstrap
// The require function
function __webpack_require__(moduleId) {
// Create a new module
var module = {
i: moduleId,
exports: {},
};
// Execute the module function
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// Return the exports of the module
return module.exports;
}
// define getter function for harmony exports
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
enumerable: true,
get: getter,
});
}
};
// define __esModule on exports
__webpack_require__.r = function (exports) {
Object.defineProperty(exports, "__esModule", {
value: true,
});
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
// Load entry module and return exports
return __webpack_require__(0);
})([
/* 0 */
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
var _message_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
console.log(_message_js__WEBPACK_IMPORTED_MODULE_0__["default"]);
},
/* 1 */
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
var _demoName_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
__webpack_exports__[
"default"
] = `hello ${_demoName_js__WEBPACK_IMPORTED_MODULE_0__["demoName"]}`;
},
/* 2 */
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "demoName", function () {
return demoName;
});
const demoName = "simple";
},
]);
上述的代码已经将不是非必要的代码删去,让我们集中精神在打包的部分,这段代码有下面几个看点:
webpack 的 bundle 产生了一个 IIFE ,传入模组当作其参数。
(function(modules) {...})([...]);
每个模组内容都被 function
包覆,这个函式传入了 module
, __webpack_exports__
及 __webpack_require__
(function(modules) {...})
([
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
...
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
...
}),
/* 2 */
(function(module, __webpack_exports__, __webpack_require__) {
...
})
]);
module
: 模组资讯__webpack_exports__
: 模组汇出的资源__webpack_require__
: 引入资源的方法webpack 在 IIFE 中直接引入入口资源,开始执行代码。
(function (modules) {
// webpackBootstrap
// The require function
function __webpack_require__(moduleId) {
// Create a new module
var module = {
i: moduleId,
exports: {},
};
// Execute the module function
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// Return the exports of the module
return module.exports;
}
// Load entry module and return exports
return __webpack_require__(0);
})([
/* 0 */
function (module, __webpack_exports__, __webpack_require__) {},
/* 1 */
function (module, __webpack_exports__, __webpack_require__) {},
/* 2 */
function (module, __webpack_exports__, __webpack_require__) {},
]);
执行 __webpack_require__(0)
时实际上利用 modules[moduleId].call(...)
叫 /* 0 */
函式执行,这时开发者所撰写的内容才真正的执行起来。
引入其他模组时,使用 __webpack_require__
执行模组并将会出内容加至 __webpack_exports__
。
(function (modules) {
// webpackBootstrap
// The require function
function __webpack_require__(moduleId) {
// Create a new module
var module = {
i: moduleId,
exports: {},
};
// Execute the module function
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// Return the exports of the module
return module.exports;
}
})([
/* 0 */
function (module, __webpack_exports__, __webpack_require__) {
var _message_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
console.log(_message_js__WEBPACK_IMPORTED_MODULE_0__["default"]);
},
/* 1 */
function (module, __webpack_exports__, __webpack_require__) {},
/* 2 */
function (module, __webpack_exports__, __webpack_require__) {},
]);
webpack 会将模组内的引入语法改为 __webpack_require__
,这里我们可以发现 webpack 将外部的 __webpack_require__
方法带入的原因是内部代码要做引用时也可以使用此方法处理。
在 /* 0 */
这个入口模组中使用 __webpack_require__(1)
执行并引用 /* 1 */
的资源。
在汇出资源的时候会将资源带进 __webpack_exports__
,而 __webpack_require__
会帮忙 return
出去。
(function (modules) {
// webpackBootstrap
// The require function
function __webpack_require__(moduleId) {
// Create a new module
var module = {
i: moduleId,
exports: {},
};
// Execute the module function
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// Return the exports of the module
return module.exports;
}
})([
/* 0 */
function (module, __webpack_exports__, __webpack_require__) {},
/* 1 */
function (module, __webpack_exports__, __webpack_require__) {
var _demoName_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
__webpack_exports__[
"default"
] = `hello ${_demoName_js__WEBPACK_IMPORTED_MODULE_0__["demoName"]}`;
},
/* 2 */
function (module, __webpack_exports__, __webpack_require__) {},
]);
可以看到 /* 1 */
中汇出的资源被加入 __webpack_exports__["default"]
,如此一来 __webpack_require__
在最後 return module.exports
的时候就可以使外部模组取得期望的资源。
你会发现到三个模组的都有个 __webpack_require__.r(__webpack_exports__)
,这会将此模组标示为 ES Module ,告知此模组为 ES Module 。
(function (modules) {
// define __esModule on exports
__webpack_require__.r = function (exports) {
Object.defineProperty(exports, "__esModule", {
value: true,
});
};
})([
/* 0 */
function (module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
},
/* 1 */
function (module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
},
/* 2 */
function (module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
},
]);
__webpack_require__.r
会将 exports
加上 __esModule
的值,以表示此为 ES Module 。
webpack 对於非 default
的汇出资源处理如下:
(function (modules) {
// define getter function for harmony exports
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
enumerable: true,
get: getter,
});
}
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
})([
/* 0 */
function (module, __webpack_exports__, __webpack_require__) {},
/* 1 */
function (module, __webpack_exports__, __webpack_require__) {},
/* 2 */
function (module, __webpack_exports__, __webpack_require__) {
__webpack_require__.d(__webpack_exports__, "demoName", function () {
return demoName;
});
const demoName = "simple";
},
]);
这个例子中 demoName.js
传回的是 export const demoName = "simple"
, webpack 利用 Object.defineProperty
的方式将 __webpack_exports__.demoName
的 getter
设为 return demoName
如此一来其他模组便能取得此资源值。
webpack 所产出的 bundle 乍看之下很复杂,其实也就只有 require
与 export
两个重点,只要搞懂了这两个概念,看懂 bunlde 就再也不是问题了。
终於要开始写打包器了,下面是整个打包所需的步骤:
简单顺过一遍流程後,细节会在之後讲解,现在我们先建立专案:
// ./demos/read-file/package.json
{
"scripts": {
"build": "node boundler.js"
}
}
在指令中加上 node boundler.js
,我们将会把打包器内容写於 bundler.js
,并使用 node
执行。
接着我们就一步一步来建构我们的打包器吧。
第一步,我们需要将入口模组 ./src/index.js
的内容读入:
// ./demos/read-file/boundler.js
const fs = require("fs");
const content = fs.readFileSync("./src/index.js", "utf-8");
console.log(content);
使用 node.js
的 fs
模组读入档案。
本文所有的范例都采用同步方式,让读者可以容易进入状况。
结果如下:
可以看到 ./src/index.js
的内容顺利读入了。
AST 抽象语法术,他可以以树状结构表示各个代码的内容,使用 AST 就可以去控制或查找目标资料,而不需要直接分析字串。
现在我们使用 @babel/parser
将代码内容转为 AST:
// ./demos/ast-parser/boundler.js
const fs = require("fs");
const { parse } = require("@babel/parser");
const content = fs.readFileSync("./src/index.js", "utf-8");
const ast = parse(content, {
sourceType: "module",
});
console.log(ast);
需要跟 Parser 说明此代码内容为 ES Module ,因此 sourceType
要设为 module
。
输出结果如下:
可以看到代码的内容被转为物件的形式,这样我们就可以用代码的方式去控制语法了。
为了更加方便的使用 AST ,我们可以使用 AST Explorer 来观察 AST 。
webpack 使用的 Parser 为
acorn
,@babel/parser
也是以此为基础做开发的。
有了 AST 的帮助,我们可以用 ImportDeclaration
搜索 AST 找出 import
的内容。
为了方便的搜索 AST ,可以使用 @babel/traverse
:
// ./demos/tree-walker/boundler.js
const fs = require("fs");
const { parse } = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const content = fs.readFileSync("./src/index.js", "utf-8");
const ast = parse(content, {
sourceType: "module",
});
traverse(ast, {
ImportDeclaration: ({ node }) => {
console.log(node);
},
});
结果如下:
我们发现 node.source.value
中有引入的档案路径资讯。
利用上节找出的 import
资讯,我们可以将入口资源的资讯记起来:
// ./demos/create-asset/boundler.js
const fs = require("fs");
const { parse } = require("@babel/parser");
const traverse = require("@babel/traverse").default;
let ID = 0;
function createAsset(fileName) {
const content = fs.readFileSync(fileName, "utf-8");
const ast = parse(content, {
sourceType: "module",
});
const dependencies = [];
traverse(ast, {
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value);
},
});
const id = ID++;
return {
id,
fileName,
dependencies,
};
}
console.log(createAsset("./src/index.js"));
我们用 createAsset
包住之前的程序,为之後要建立相依图做准备,并将相依的路径加入 dependencies
中。
结果如下:
有了相依资讯,我们可以来建立相依图了。
// ./demos/create-graph/boundler.js
const fs = require("fs");
const path = require("path");
const { parse } = require("@babel/parser");
const traverse = require('@babel/traverse').default
let ID = 0
function createAsset(fileName) {
...
}
function createGraph(entry) {
const graph = [createAsset(entry)];
for (const asset of graph) {
const dirname = path.dirname(asset.fileName);
asset.dependencies.forEach((dependencyRelativePath) => {
const dependencyAbsolutePath = path.join(dirname, dependencyRelativePath);
const dependencyAsset = createAsset(dependencyAbsolutePath);
graph.push(dependencyAsset);
});
}
return graph;
}
console.log(createGraph("./src/index.js"));
巡览每个模组的每个相依,将各个模组输出来 Asset 资讯,结果如下:
利用相依图,我们可以清楚地了解各个模组的相依情形,藉此来建立 bundle 。
有了相依的资讯後,接着需要将资讯对应至各个模组的 ID 上,因此要在巡览完当前模组的全部相依後,建立对应表。
// ./demos/mapping/boundler.js
const fs = require("fs");
const path = require("path");
const { parse } = require("@babel/parser");
const traverse = require("@babel/traverse").default;
function createAsset(filename, id) {
...
}
function createGraph(entry) {
let id = 0;
const graph = [createAsset(entry, id++)];
for (const asset of graph) {
asset.mapping = {}; // 初始对应物件
const dirname = path.dirname(asset.filename);
asset.dependencies.forEach((dependencyRelativePath) => {
const dependencyAbsolutePath = path.join(dirname, dependencyRelativePath);
const dependencyAsset = createAsset(dependencyAbsolutePath, id++);
asset.mapping[dependencyRelativePath] = dependencyAsset.id; // 将相依模组与模组的 ID 做对应
graph.push(dependencyAsset);
});
}
return graph;
}
console.log(createGraph("./src/index.js"));
建置结果如下:
现在我们拥有了所有的资讯了,接着就来建立 bundle 的执行代码吧。
执行代码的建立有下面几个要点:
require
方法的实作范例中的代码为 ES Module 的类型,使用 import
的方式引入模组,在建立 bundle 时需要修改引入的方式让其符合 bundle 内的执行使用。
这里我们使用 babel
将代码做转换,以 ./src/index.js
为例:
import message from "./message.js";
console.log(message);
会被转为:
"use strict";
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
console.log(_message.default);
babel
会改为使用 require
引入模组,先记住这点,这让我们有机会客制属於我们自己打包器的引入方式。
我们使用与 webpack 相仿的方式,藉由 IIFE 引入模组,并在执行代码中实作 require
,最後执行入口模组。
(function(modules) {
function require(id) {
...
}
require(0);
})({...})
模组作为参数时包裹为一个函式,此函式传入 module
, exports
, require
三个与 webpack 相仿的物件、方法。
(function(modules) {
function require(id) {
...
}
require(0);
})({
0: [
function (module, exports, require) {},
{ "./message.js": 1 },
],
1: [
function (module, exports, require) {},
{ "./demoName.js": 2 },
],
2: [
function (module, exports, require) {},
{},
],})
这里与 webpack 不同的地方在於我们不只有引入模组,还有引入模组的相依模组对应表,这是因为我们再转至代码时使用的是 babel
这样常规的转译器,它并不能向 webpack 自制的转译器那般可以直接将引入的路径替换成 bundle 时的模组 index
,因此我们在这样要多带个对应的资讯,以利後续引入关联模组。
require
方法的实作(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports : {} };
fn(module, module.exports, localRequire);
return module.exports;
}
require(0);
})({...})
require
是带入 id
资讯localRequire
用来在模组中引入相依模组,须将相依模组的路径转为 bundle 中的 index
module
, exports
, require
module.exports
,使外部模组使用汇出资源详细的 bundle 产生方式比较繁杂,想要了解的可以参考 ./demos/bundle
范例。
最後需要将完成的 bundle 写入输出档案中。
// ./demos/write-file/boundler.js
function output(code, outputPath) {
const dirname = path.dirname(outputPath);
fs.mkdirSync(dirname, { recursive: true });
const prettierCode = prettier.format(code, { parser: "babel" });
fs.writeFileSync(outputPath, prettierCode);
}
fs.mkdirSync
建立资料夹prettier
排版代码fs.writeFileSync
写入档案产生的代码如下所示:
// ./demos/write-file/dist/main.js
(function (modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports: {} };
fn(module, module.exports, localRequire);
return module.exports;
}
require(0);
})({
0: [
function (module, exports, require) {
"use strict";
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
console.log(_message["default"]);
},
{ "./message.js": 1 },
],
1: [
function (module, exports, require) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports["default"] = void 0;
var _demoName = require("./demoName.js");
var _default = "hello ".concat(_demoName.demoName);
exports["default"] = _default;
},
{ "./demoName.js": 2 },
],
2: [
function (module, exports, require) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.demoName = void 0;
var demoName = "simple";
exports.demoName = demoName;
},
{},
],
});
可以直接将其贴在 dev tool 中的 console
就可以看到结果了。
本文分为两段,第一段讲解如何理解 webpack 所产生的 bundle ,第二阶段实际带大家一步一步写出简易的打包器。
理解了整个打包过程,之後对於 webpack 的掌握上会更加了得心应手。
>>: javasScript 进阶笔记二 (object.prototype.call)
股市只有两种人:输家跟赢家。 其中长期赢家只占极少部分,若我们不想成为绝大部分的输家,以下几种行为请...
前一天我们讲到综合版可以接收所有种类的讯息。 @csrf_exempt def callback(r...
C runtime/lib 通常都会有一个测试自己实做正确性的testsuite,像在glibc内部...
好的,Max 的课程进行到这边,是时候来休息一下,整合前面所学的东西了。要来做的是一个可以让使用者新...
各位大大 因企业运维来了一位大哥建议公司把各种网站服务放在VM上,VM可以切很多台服务器出来,这样子...