webpack 是一個用 nodejs 寫的前端打包工具,從官網上的圖片能夠看出,能夠將不一樣類型和總錯複雜的依賴關係的文件打包成簡單的瀏覽器能夠認識的文件。:::_javascript
webpack 有入口、輸出、loader、插件等幾個重要的概念前端
webpack 打包的起點,用來分析依賴的入口。vue
output 屬性告訴 webpack 在哪裏輸出它所建立的 bundle,以及如何命名這些文件。主要輸出文件的默認值是 ./dist/main.js,其餘生成文件默認放置在 ./dist 文件夾中。java
webpack 默認只能理解 JavaScript 和 JSON 文件。loader 可讓 webpack 可以去處理其餘類型的文件,並將它們轉換爲有效模塊,例如 vue-loader 可讓 webpack 去處理.vue 文件。node
loader 用於轉換某些類型的模塊,而插件則能夠用於執行範圍更廣的任務。包括:打包優化,資源管理,注入環境變量。webpack
我把一個簡單的文件引用打包後的文件精簡以下:git
(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。github
分析了 webpack 打包後的文件後,如今咱們開始嘗試本身寫一個簡單的打包工具。web
在本地新建一個目錄,我這裏取名叫 xpack,進入 xpack 目錄執行以下命令初始化 npmnpm
npm init -y
新增文件 src 目錄,並建立 index.js 和 template.js
完整目錄以下:
src
package.json
我這裏就直接貼代碼了,裏面有詳細的註釋
#!/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();
同樣直接貼代碼
(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 中添加以下選項
"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
xpack.config.js
package.json
隨便寫一些測試代碼,而後執行 xpack 指令打包,不出意外就能正常打包了,完整的代碼在jianjunx/my-pack。