學習webpack 寫一個js程式碼打包工具
webpack 是一個用 nodejs 寫的前端打包工具,從官網上的圖片可以看出,可以將不同型別和總錯複雜的依賴關係的檔案打包成簡單的瀏覽器可以認識的檔案。:::_
核心概念
webpack 有入口、輸出、loader、外掛等幾個重要的概念
入口(entry)
webpack 打包的起點,用來分析依賴的入口。
輸出(output)
output 屬性告訴 webpack 在哪裡輸出它所建立的 bundle,以及如何命名這些檔案。主要輸出檔案的預設值是 ./dist/main.js,其他生成檔案預設放置在 ./dist 資料夾中。
loader
webpack 預設只能理解 JavaScript 和 JSON 檔案。loader 可以讓 webpack 能夠去處理其他型別的檔案,並將它們轉換為有效模組,例如 vue-loader 可以讓 webpack 去處理.vue 檔案。
外掛
loader 用於轉換某些型別的模組,而外掛則可以用於執行範圍更廣的任務。包括:打包優化,資源管理,注入環境變數。
分析打包後的檔案
我把一個簡單的檔案引用打包後的檔案精簡如下:
(function (modules) { // 儲存執行過的模組 let installModules = {}; /** * 自定義require方法,打包時會把所有的require替換為__webpack_require__ * @param {*} moduleId 就是模組的相對路徑名 */ function __webpack_require__(moduleId) { if (installModules[moduleId]) { return installModules[moduleId].exports; } // 初始化module物件 const module = (installModules[moduleId] = { exports: {}, }); // 根據傳入的模組id呼叫模組 modules[moduleId].call( module.exports, module, module.exports, __webpack_require__ ); return module.exports; } return __webpack_require__("./src/index.js" /** 這裡一般是入口檔案 */); })( { "./src/index.js": function (module, exports, __webpack_require__) { eval( "const home = __webpack_require__('./src/home.js');\n\nfunction getHome() {\n return home;\n}\n\nconsole.log(getHome());\n\nmodule.exports = {\n getHome,\n name: 'test webpack',\n};\n" ); }, "./src/home.js": function (module, exports, __webpack_require__) { eval( "const { hey } = __webpack_require__('./src/test.js');\n\nmodule.exports = { home: 'hell-home', hey };\n" ); }, "./src/test.js": function (module, exports, __webpack_require__) { eval("exports.hey = function () {\n return 'hey 哥們';\n};\n"); }, } /** 被替換成每個模組的內容,格式為一個物件key為路徑,值為匿名函式裡面使用eval包裹的檔案程式碼 */ );
可以看到打包後的檔案中有一個自執行函式,傳入的是一個物件,物件的 key 為檔案被 require 的路徑,值為一個函式 裡面有一個 eval 方法,我們的模組內容被打包成字串放在了 eval 中。
自執行函式中有一個webpack_require方法,仔細看我們模組程式碼中的 require 方法都被替換成了webpack_require。
自己寫一個打包工具
分析了 webpack 打包後的檔案後,現在我們開始嘗試自己寫一個簡單的打包工具。
初始化專案
在本地新建一個目錄,我這裡取名叫 xpack,進入 xpack 目錄執行如下命令初始化 npm
npm init -y
新增檔案 src 目錄,並建立 index.js 和 template.js
完整目錄如下:
src
- index.js // 主邏輯
- template.js // 打包模板檔案
package.json
index.js
我這裡就直接貼程式碼了,裡面有詳細的註釋
#!/usr/bin/env node
const path = require("path");
const fs = require("fs");
// 預設配置
const defuaultConf = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
},
};
// 合併配置檔案
const config = Object.assign(
defuaultConf,
require(path.resolve("./xpack.config.js"))
);
class Xpack {
constructor(config) {
this.config = config; // 儲存配置項
this.entry = config.entry; // 儲存配置項中的入口檔案地址
this.root = process.cwd(); // 獲取命令執行的目錄
this.modules = {};
}
/**
* 程式碼解析和依賴分析
* @param {*} code 模組程式碼
* @param {*} parent 模組路徑
*/
parse(code, parent) {
const deps = []; // 依賴模組的路徑
const r = /require\('(.*)'\)/g; // 正則匹配依賴模組
code = code.replace(r, function (match, arg) {
const retpath = path.join(parent, arg.replace(/'|"/g), "");
deps.push(retpath);
return `__xpack__require__('./${retpath}')`;
});
return { deps, code };
}
generateMoudle() {
const temp = [];
// 將modules轉成字串
for (const [key, val] of Object.entries(this.modules)) {
temp.push(`'${key}' : ${val}`);
}
return `{${temp.join(",")}}`;
}
generateFile() {
// 讀取模板檔案
const template = fs.readFileSync(
path.resolve(__dirname, "./template.js"),
"utf-8"
);
// 替換__modules_content__和__entry__
this.template = template
.replace("__entry__", this.entry)
.replace("__modules_content__", this.generateMoudle());
// 生成打包後的檔案
fs.writeFileSync(
path.join("./dist", this.config.output.filename),
this.template
);
}
/**
* 遞迴解析模組並按引入路徑儲存到modules
* @param {*} modulePath 模組的真實路徑
* @param {*} name 模組地址
*/
createModule(modulePath, name) {
// 讀取模組檔案內容,入口檔案和require的檔案
const moduleContent = fs.readFileSync(modulePath, "utf-8");
// 解析讀取的模組內容
const { code, deps } = this.parse(moduleContent, path.dirname(name));
/**
* 將模組程式碼存放到modules中,模組引入路徑為key,模組中的程式碼用eval包裹
* eval可以將字串當成js來執行,外面包裹的函式中傳入了定義好的module, exports, __webpack_require__
* 當遇到commonjs模組匯出時就換呼叫對應的引數
*/
this.modules[name] = `function (module, exports, __webpack_require__) {
eval("${code.replace(/\n/g, "\\n")}")
}`;
// 迴圈依賴項,並呼叫this.createModule繼續解析
deps.forEach((dep) => {
this.createModule(path.join(this.root, dep), `./${dep}`);
});
}
// 開始函式
start() {
const entryPath = path.resolve(this.root, this.entry);
this.createModule(entryPath, this.entry);
// console.log(this.modules);
this.generateFile();
}
}
// 初始化,並傳入配置項
const xpack = new Xpack(config);
xpack.start();
template.js
一樣直接貼程式碼
(function (modules) {
// 儲存執行過的模組
let installModules = {};
/**
* 自定義require方法,打包時會把所有的require替換為__xpack_require__
* @param {*} moduleId 就是模組的相對路徑名
*/
function __xpack_require__(moduleId) {
if (installModules[moduleId]) {
return installModules[moduleId].exports;
}
// 初始化module物件
const module = (installModules[moduleId] = {
exports: {},
});
// 根據傳入的模組id呼叫模組
modules[moduleId].call(
module.exports,
module,
module.exports,
__xpack_require__
);
return module.exports;
}
return __xpack_require__(
"__entry__" /** 被替換成this.entry 配置項中的入口檔案地址 */
);
})(
__modules_content__ /** 被替換成每個模組的內容,格式為一個物件key為路徑,值為匿名函式裡面使用eval包裹的檔案程式碼 */
);
試用
以上程式碼邏輯寫好以後我們就可以看一下新打包工具的威力了,在這之前我們先做一些處理。
package.json
在 package.json 中新增如下選項
"bin": {
"xpack": "./src/index.js"
}
然後執行
npm link
第一步的意思是,輸入命令列指令 xpack 後執行./src/index.js 檔案,這裡注意 index.js 頂部要新增 *#!/usr/bin/env node *就是告訴系統可以在 PATH 目錄中查詢指令。
第二步 npm link 是將當前這個 npm 包連結到全域性,相當於 npm install xpack -g ,這樣就可以在命令列使用 xpack 指令了。
打包
我們新建一個測試專案,結構如下
src
- index.js
- test.js
xpack.config.js
package.json
隨便寫一些測試程式碼,然後執行 xpack 指令打包,不出意外就能正常打包了,完整的程式碼在jianjunx/my-pack。