這款簡易版 webpack 主要實現的功能以下:javascript
Webpack 的運行流程是一個串行的過程,從啓動到結束會依次執行如下流程:java
類型 | 如何辨別node |
使用要點 |
Basic | hook 中不包含如下三個類型關鍵字的linux |
不關心監聽函數是否有返回值 |
Bail | hook 中包含 Bailwebpack |
保險式: 只要監聽函數中有返回值(不爲 undefined ),則跳過以後的監聽函數 |
Waterfall | hook 中包含 Waterfallgit |
瀑布式: 上一步的返回值交給下一步使用 |
Loop | hook 中包含 Loopes6 |
循環類型: 若是該監聽函數返回 true,則這個監聽函數會反覆執行,若是返回undefined 則退出循環 |
node_modules\webpack\bin\webpack.js
github
// 找到這裏的代碼
// webpack 有兩種命令行工具: webpack-cli 和 webpack-command
// 由於 webpack-cli 功能更強大,通常都是用 webpack-cli,因此會執行下面的語句
else if (installedClis.length === 1) {
const path = require("path");
const pkgPath = require.resolve(`${installedClis[0].package}/package.json`);
const pkg = require(pkgPath);
require(path.resolve(
path.dirname(pkgPath),
pkg.bin[installedClis[0].binName]
));
} 複製代碼
node_modules\webpack\lib\webpack.js
web
node_modules\webpack\declarations
typescript
node_modules\webpack\lib
// debugger.js 和項目中的 packge.json 同級
// 右鍵運行 debugger.js ,至關於使用 npx webpack 運行 webpack
// npx webpack 其實就是用 node 執行 bin 下面的 cli.js
// npx webpack = node ./node_modules/webpack-cli/bin/cli.js
// 找到 webpack-cli/bin/cli.js ,設置斷點,就能夠開始調試了(這是第一種方法)
const path = require("path");
const pkgPath = require.resolve(`webpack-cli/package.json`);
const pkg = require(pkgPath);
require(path.resolve(
path.dirname(pkgPath),
'./bin/cli.js'
//pkg.bin['webpack-cli']
));複製代碼
let webpack = require("webpack");
let webpackOptions = require("./webpack.config");
const compiler = webpack(webpackOptions);
compiler.run((err, stat) => {
console.log(err);
console.log(stat)
});複製代碼
const path = require('path');
module.exports = {
// 用開發模式打包代碼 !!!!!!
mode:'development',
devtool:'none',
entry:'./src/index.js',
output:{
path:path.resolve(__dirname,'dist'),
filename:'bundle.js'
},
};複製代碼
// src/index.js
import {logMsg} from './sync-module';
console.log(logMsg);
let button = document.createElement('button');
button.innerHTML = '請點我';
button.addEventListener('click',()=>{
import(/*webpackChunkName: 'async-module'*/'./async-module.js').then(result=>{
console.log(result.default);
});
});
document.body.appendChild(button);
// src/async-module.js
module.exports = "我是異步模塊";
// src/sync-module.js
export function logMsg() {
console.log('我是同步模塊');
}複製代碼
/* src/ index.js sync-module.js async-module.js */
// webpack 打包後,會把引用模塊的相對路徑變成相對於 webpack.config.js 的相對路徑
// 在 index.js 中 引入 "./sync-module.js" => 最終會變成 "./src/sync-module.js"
// webpack 啓動代碼的自執行函數
(function(modules) { // webpackBootstrap
// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
// data => [
// [chunkName],
// { chunkID : chunk 內容}
// ]
var chunkIds = data[0];
var moreModules = data[1];
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
// installedChunks[chunkId] => [resolve, reject, Promise]
// 將 resolve 存到 resolves 數組中,先不着急執行 resolve()
resolves.push(installedChunks[chunkId][0]);
}
// 設置爲 0 ,表示已經加載成功
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
// 異步 chunk 加載完成後,將異步 chunk 的代碼合併到 modules 中
// 這樣以後加載該 chunk 時,能夠直接從 modules 中獲取到
// key 是模塊 ID ,value 是模塊內容
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction){
parentJsonpFunction(data);
}
while(resolves.length) {
// 執行 resolve
resolves.shift()();
}
}
// The module cache
// 普通模塊的緩存:只要加載初始化過一次的模塊都放到這,以後再使用這個模塊時
// 直接從這裏獲取,不須要再初始化一遍
var installedModules = {};
// object to store loaded and loading chunks
// !!!!!!!!!!!!!!!!!!!!!!
// 存儲已加載的或者加載中的 chunk (這裏的 chunk 包含: 入口 chunk 和異步加載的 chunk)
// !!!!!!!!!!!!!!!!!!!!!!
// installedChunks 對象中,每一個 key 對應的 value 值的意思
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// undefined 表示 chunk 還未加載,null 表示 chunk 會預加載
// Promise = chunk loading, 0 = chunk loaded
// Promise 表示 chunk 正在加載中,0 表示 chunk 加載完成
var installedChunks = {
// 若是是單入口,key 的默認值是 main
"main": 0
};
// script path function
// 設置異步 chunk 的請求 url
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + chunkId + ".bundle.js"
}
// The require function
// webpack 本身實現的一個 require 方法,能夠直接在瀏覽器中運行
function __webpack_require__(moduleId) {
// Check if module is in cache
// 加載模塊前,先從緩存列表中查找,是否已經加載過
if(installedModules[moduleId]) {
// 若是有,說明模塊已經緩存過,直接返回該模塊的導出對象 exports
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
// 建立一個新的模塊對象,而且放到緩存列表中
var module = installedModules[moduleId] = {
// 模塊 ID
i: moduleId,
// 是否已經加載 loaded:false
l: false,
// 模塊導出對象,默認是一個空對象
exports: {}
};
// Execute the module function
// 加載模塊
// modules 是自執行函數接收的參數——一個包含模塊信息的對象
// key 是模塊路徑, value 是一個函數,裏面包含了模塊的內容
// {
// "./src/a.js":
// (function(module, __webpack_exports__, __webpack_require__) {
// 模塊內容:xxx
// 模塊內容:xxx
// 模塊內容:xxx
// },
// }
// 從 modules 對象中找到對應的 key,執行函數(value)並將內部的 this 指向上面新建 module 的 exports 對象
// 目的是將函數內部的內容都放置到 module.exports 中
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;
}
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
// 加載異步 chunk
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// JSONP chunk loading for javascript
// 用 jsonp 來請求加載異步 chunk
var installedChunkData = installedChunks[chunkId];
// 0 means "already installed".
// 若是要加載的 chunk 沒有初始化過
if(installedChunkData !== 0) {
// a Promise means "currently loading".
// 排除了 0,緩存 chunk 列表裏的值就剩下 undefined/null/Promise
// 當模塊正在加載中時
if(installedChunkData) {
promises.push(installedChunkData[2]);
}
// 當模塊還未加載過
else {
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
// 新建一個 promise 時,會當即執行它的函數體
// 將當前 chunk 的狀態設置爲 Promise,表示正在加載中
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// 給當前的 installedChunkData 添加一個值
// 而後將 installedChunkData 添加到 promises 數組中
promises.push(installedChunkData[2] = promise);
// start chunk loading
// 用 jsonp 來請求加載異步 chunk
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
// 設置請求 url
script.src = jsonpScriptSrc(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
// 執行完全部的 promise後再返回結果
return Promise.all(promises);
};
// expose the modules object (__webpack_modules__)
// 將模塊列表放到 __webpack_require__ 的 m 屬性上
__webpack_require__.m = modules;
// expose the module cache
// 將緩存列表放到 __webpack_require__ 的 c 屬性上
__webpack_require__.c = installedModules;
// define getter function for harmony exports
// 在 exports 對象上定義 name 屬性的 getter 方法
__webpack_require__.d = function(exports, name, getter) {
// 判斷 exports 對象上是否有 name 屬性
if(!__webpack_require__.o(exports, name)) {
// 在 exports 對象上添加 name 屬性,可枚舉爲 true
// get 的值爲 getter,當訪問該屬性時,該方法會被執行
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// define __esModule on exports
// 在 exports 對象上定義一個 __esModule 屬性,用來判斷當前模塊是否爲 es6 模塊
__webpack_require__.r = function(exports) {
// 若是當前瀏覽器支持 Symbol
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
// 設置前
// console.log(exports.toString());// [object Object]
// 給 exports 對象類型設置爲 Module
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
// 設置後
// console.log(exports.toString());// [object Module]
}
// 不然給 exports 對象添加一個表示 esm 的屬性
Object.defineProperty(exports, '__esModule', { value: true });
};
// create a fake namespace object 建立一個命名空間對象
/ 爲何要建立一個命名空間對象?
// 由於 import('xxx.js') 加載的 js,多是 esm ,也多是 cjs
// 因此須要兼容處理
// mode & 1: value is a module id, require it 若是值是模塊ID,加載它
// mode & 2: merge all properties of value into the ns 把全部的屬性合併到命名空間上 ns —— nameSpace
// mode & 4: return value when already ns object 當已是命名空間對象的話直接返回值
// mode & 8|1: behave like require 就像 require 同樣
// mode 爲何要用二進制來判斷? 高效。節約內存
// linux 裏面的權限判斷也是用的二進制, 7 => 111 可讀可寫可執行
__webpack_require__.t = function(value, mode) {
// value 最開始是模塊 ID
// 直接加載模塊
if(mode & 1) value = __webpack_require__(value);
// 不用加載模塊,直接返回模塊內容
if(mode & 8) return value;
// 若是 value 已是一個對象而且 __esModule 屬性爲 true 的話就直接返回 value
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
// 不然就建立一個空對象,加載這個對象,
var ns = Object.create(null);
// 在對象上設置 __esModule 屬性爲true
__webpack_require__.r(ns);
// 給 ns 對象定義一個 default 屬性
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
// 若是 mode 爲2,而且 value 不是字符串,把值的全部屬性都定義到 ns 對象上
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;//{__esModule:true,default:'模塊內容'}
};
// getDefaultExport function for compatibility with non-harmony modules
// 一個能獲取模塊內容的函數
__webpack_require__.n = function(module) {
// 若是是 __esModule,說明是 es6 模塊,須要返回模塊的 default 屬性
// 若是不是,說明是 cjs 模塊,直接返回模塊自己
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
//給 getter 添加一個 a 的屬性,就是 getter 方法自己
__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 = "";
// on error function for async loading
// 加載異步 chunk 時的錯誤輸出
__webpack_require__.oe = function(err) { console.error(err); throw err; };
// 第一次執行的時候,window["webpackJsonp"] 會是一個空數組
// jsonpArray 和 window["webpackJsonp"] 共同指向同一塊內存地址
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 綁定 this,將老的數組的 push 方法始終指向 jsonpArray
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 若是不綁定 this 的話,那麼在 webpackJsonpCallback 中執行 parentJsonpFunction(data) 的時候
// 就至關於執行了一個 「裸的」數組原生 的 push,data 不知道該添加給誰
// var oldJsonpFunction = jsonpArray.push;
//重寫 jsonArray 的 push 方法,賦值爲 webpackJsonpCallback
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
// 爲何要保留老的數組的 push 方法?
// 避免 "重複發請求" 加載 chunk ,若是已經加載好了的,就拿來直接用
var parentJsonpFunction = oldJsonpFunction;
// 總結:
// 一、window["webpackJsonp"] 的 push 方法被重寫,再也不是數組原生的方法,而是用來執行 jsonp 回調函數的
// 二、這時候若是想要給 window["webpackJsonp"] 這個數組添加數據時,就沒法用 push 來添加了
// 三、因此這裏多定義一個 jsonpArray 數組,它和 window["webpackJsonp"] 共同指向同一塊內存地址
// 四、經過給 jsonpArray 添加(push)數據,那麼相應的 window["webpackJsonp"] 就能獲取到這些數據
// Load entry module and return exports
// 加載入口模塊而且返回導出對象
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
/* src/ index.js sync-module.js async-module.js */
// webpack 打包後,會把引用模塊的相對路徑變成相對於 webpack.config.js 的相對路徑
// 在 index.js 中 引入 "./sync-module.js" => 最終會變成 "./src/sync-module.js"
({
// key 是模塊 ID ,value 是模塊內容
"./src/index.js":
/*!**********************!*\ // 入口 chunk !*** ./src/index.js ***! \**********************/
/*! no exports provided */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _sync_module__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./sync-module */ "./src/sync-module.js");
console.log(_sync_module__WEBPACK_IMPORTED_MODULE_0__["logMsg"]);
let button = document.createElement('button');
button.innerHTML = '請點我';
button.addEventListener('click',()=>{
__webpack_require__.e(/*! import() | async-module */ "async-module")
.then(__webpack_require__.t.bind(null, /*! ./async-module.js */ "./src/async-module.js", 7))
.then(result=>{// result = {__esModule:true,default:'模塊內容'}
console.log(result.default);
});
});
document.body.appendChild(button);
}),
"./src/sync-module.js":
/*!****************************!*\ !*** ./src/sync-module.js ***! // 入口 chunk 依賴的同步模塊 \****************************/
/*! exports provided: logMsg */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "logMsg", function() { return logMsg; });
function logMsg() {
console.log('我是同步模塊');
}
})
});複製代碼
(function(modules) { // webpack 的啓動代碼自執行函數
// The module cache 模塊的緩存
var installedModules = {};
// The require function webpack本身實現了一個require方法
function __webpack_require__(moduleId) {
// Check if module is in cache 判斷一下這個模塊ID是否在緩存中
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;//若是有,說明此模塊加載過,直接返回導出對象exports
}
// Create a new module (and put it into the cache)
// 建立一個新的模塊對象而且把它放到緩存中
var module = installedModules[moduleId] = {
i: moduleId,// 模塊ID
l: false,//是否已經加載loaded false
exports: {} //導出對象,默認是一個空對象
};
// Execute the module function 執行此模塊對應的方法,目的是給module.exports賦值
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;
}
// the startup function
function startup() {
// Load entry module and return exports
// 加載入口模塊而且返回導出對象
return __webpack_require__("./src/index.js");
}
// run startup 執行啓動方法
return startup();
})
({
"./src/hello.js":
(function(module) {
module.exports = "hello";
}),
"./src/index.js":
(function(__unusedmodule, __unusedexports, __webpack_require__) {
let hello = __webpack_require__( "./src/hello.js");
console.log(hello);
})
});複製代碼
Javascript 代碼中的語法單元主要包括如下這麼幾種
const
、let
、var
等const babylon = require('@babel/parser');
// @babel/core 裏面內置了 babylon/parser,也能夠用它來轉換 AST
const babel = require('@babel/core');
let types = require('@babel/types');
let generate = require('@babel/generator').default;
let traverse = require('@babel/traverse').default;
const originalSource = "const a = (a, b) => a + b;";
// 將 當前模塊 的內容轉換成 AST
const ast = babylon.parse(originalSource);
// @babel/core 裏面內置了 babylon/parser,也能夠用它來轉換 AST
// const ast = babel.parse(originalSource);
// 遍歷語法樹,尋找要修改的目標節點
traverse(ast, {
// 若是當前節點是一個 箭頭函數 時
ArrowFunctionExpression: (nodePath) => {
let node = nodePath.node;
let body = node.body;
if(!types.isBlockStatement(node.body)){
body = types.blockStatement([types.returnStatement(node.body)])
}
let newNode = types.functionExpression(null,node.params,body);
nodePath.replaceWith(newNode);
}
});
// 把轉換後的抽象語法樹從新生成代碼
let {code} = generate(ast);
console.log('新的 code =>', code);複製代碼
const babylon = require('@babel/parser');
let types = require('@babel/types');
let generate = require('@babel/generator').default;
let traverse = require('@babel/traverse').default;
const originalSource = `class Person{ constructor(name){ this.name = name; } getName(){ return this.name } }`;
// 將 當前模塊 的內容轉換成 AST
const ast = babylon.parse(originalSource);
// 遍歷語法樹,尋找要修改的目標節點
traverse(ast, {
// 若是當前節點是一個 class 時
ClassDeclaration: (nodePath) => {
let node = nodePath.node;
let bodys = node.body.body;
let id = node.id;
bodys = bodys.map(body => {
if (body.kind === 'constructor') {
return types.functionExpression(id, body.params, body.body)
} else {
let left = types.memberExpression(id, types.identifier('prototype'));
left = types.memberExpression(left, body.key);
let right = types.functionExpression(null, body.params, body.body);
return types.assignmentExpression('=', left, right);
}
});
nodePath.replaceWithMultiple(bodys);
}
});
// 把轉換後的抽象語法樹從新生成代碼
let {code} = generate(ast);
console.log('新的 code =>', code);複製代碼
developer.mozilla.org/zh-CN/docs/…