webpack是一個打包模塊化 JavaScript 的工具,在 webpack裏一切文件皆模塊,經過 Loader 轉換文件,經過 Plugin 注入鉤子,最後輸出由多個模塊組合成的文件。 webpack專一於構建模塊化項目。前端
咱們先從簡單的入手看,當 webpack 的配置只有一個出口時,不考慮分包的狀況,其實咱們只獲得了一個bundle.js的文件,這個文件裏包含了咱們全部用到的js模塊,能夠直接被加載執行。那麼,我能夠分析一下它的打包思路,大概有如下4步:node
咱們會可使用這幾個包:webpack
ImportDeclaration
獲取經過import引入的模塊,FunctionDeclaration
獲取函數由這幾個模塊的做用,其實已經能夠推斷出應該怎樣獲取單個文件的依賴模塊了,轉爲Ast->遍歷Ast->調用ImportDeclaration。代碼以下:git
// exportDependencies.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const exportDependencies = (filename)=>{
const content = fs.readFileSync(filename,'utf-8')
// 轉爲Ast
const ast = parser.parse(content, {
sourceType : 'module' //babel官方規定必須加這個參數,否則沒法識別ES Module
})
const dependencies = {}
//遍歷AST抽象語法樹
traverse(ast, {
//調用ImportDeclaration獲取經過import引入的模塊
ImportDeclaration({node}){
const dirname = path.dirname(filename)
const newFile = './' + path.join(dirname, node.source.value)
//保存所依賴的模塊
dependencies[node.source.value] = newFile
}
})
//經過@babel/core和@babel/preset-env進行代碼的轉換
const {code} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return{
filename,//該文件名
dependencies,//該文件所依賴的模塊集合(鍵值對存儲)
code//轉換後的代碼
}
}
module.exports = exportDependencies
複製代碼
能夠跑一個例子:github
//info.js
const a = 1
export a
// index.js
import info from './info.js'
console.log(info)
//testExport.js
const exportDependencies = require('./exportDependencies')
console.log(exportDependencies('./src/index.js'))
複製代碼
控制檯輸出以下圖: web
看圖就能夠理解,輸出的依賴是什麼啦~有了獲取單個文件依賴的基礎,咱們就能夠在這基礎上,進一步得出整個項目的模塊依賴圖譜了。首先,從入口開始計算,獲得entryMap,而後遍歷entryMap.dependencies,取出其value(即依賴的模塊的路徑),而後再獲取這個依賴模塊的依賴圖譜,以此類推遞歸下去便可,代碼以下:數組
const exportDependencies = require('./exportDependencies')
//entry爲入口文件路徑
const exportGraph = (entry)=>{
const entryModule = exportDependencies(entry)
const graphArray = [entryModule]
for(let i = 0; i < graphArray.length; i++){
const item = graphArray[i];
//拿到文件所依賴的模塊集合,dependencies的值參考exportDependencies
const { dependencies } = item;
for(let j in dependencies){
graphArray.push(
exportDependencies(dependencies[j])
)//關鍵代碼,目的是將入口模塊及其全部相關的模塊放入數組
}
}
//接下來生成圖譜
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
//能夠看出,graph實際上是 文件路徑名:文件內容 的集合
return graph
}
module.exports = exportGraph
複製代碼
這裏就不貼測試例子圖了,有興趣的能夠本身跑如下~~緩存
首先,咱們的代碼被加載到頁面中的時候,是須要當即執行的。因此輸出的bundle.js實質上要是一個當即執行函數。咱們主要注意如下幾點:微信
所以,咱們要作這些工做:babel
const exportGraph = require('./exportGraph')
// 寫入文件,能夠用fs.writeFileSync等方法,寫入到output.path中
const exportBundle = require('./exportBundle')
const exportCode = (entry)=>{
//要先把對象轉換爲字符串,否則在下面的模板字符串中會默認調取對象的toString方法,參數變成[Object object]
const graph = JSON.stringify(exportGraph(entry))
exportBundle(` (function(graph) { //require函數的本質是執行一個模塊的代碼,而後將相應變量掛載到exports對象上 function require(module) { //InnerRequire的本質是拿到依賴包的exports變量 function InnerRequire(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function(require, exports, code) { eval(code); })(InnerRequire, exports, graph[module].code); return exports; //函數返回指向局部變量,造成閉包,exports變量在函數執行後不會被摧毀 } require('${entry}') })(${graph})`)
}
module.exports = exportCode
複製代碼
這裏,直接使用node的內置模塊,fs來寫入文件。根據webpack的output.path,來輸出到對應的目錄便可。我這裏,爲了簡便,直接固定了輸出路徑,代碼以下:
const fs = require('fs')
const path = require('path')
const exportBundle = (data)=>{
const directoryPath = path.resolve(__dirname,'dist')
if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath)
}
const filePath = path.resolve(__dirname, 'dist/bundle.js')
fs.writeFileSync(filePath, `${data}\n`)
}
const access = async filePath => new Promise((resolve, reject) => {
fs.access(filePath, (err) => {
if (err) {
if (err.code === 'EXIST') {
resolve(true)
}
resolve(false)
}
resolve(true)
})
})
module.exports = exportBundle
複製代碼
至此,簡單打包完成。我貼一下我跑的demo的結果。bundle.js的文件內容爲:
(function(graph) {
//require函數的本質是執行一個模塊的代碼,而後將相應變量掛載到exports對象上
function require(module) {
//InnerRequire的本質是拿到依賴包的exports變量
function InnerRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(InnerRequire, exports, graph[module].code);
return exports;//函數返回指向局部變量,造成閉包,exports變量在函數執行後不會被摧毀
}
require('./src/index.js')
})({"./src/index.js":{"dependencies":{"./info.js":"./src/info.js"},"code":"\"use strict\";\n\nvar _info = _interopRequireDefault(require(\"./info.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_info[\"default\"]);"},"./src/info.js":{"dependencies":{"./name.js":"./src/name.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _name = require(\"./name.js\");\n\nvar info = \"\".concat(_name.name, \" is beautiful\");\nvar _default = info;\nexports[\"default\"] = _default;"},"./src/name.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.name = void 0;\nvar name = 'winty';\nexports.name = name;"}})
複製代碼
至此,簡單打包模型完成。須要看例子的移步至:github.com/LuckyWinty/…
webpack的運行流程是一個串行的過程,從啓動到結束會依次執行如下流程:
在以上過程當中, Webpack 會在特定的時間點廣播特定的事件,插件在監聽到感興趣的事件後會執行特定的邏輯,井且插件能夠調用 Webpack 提供的 API 改變 Webpack 的運行結果。其實以上7個步驟,能夠簡單概括爲初始化、編譯、輸出,三個過程,而這個過程其實就是前面說的基本模型的擴展。
下面,咱們直接看看,webpack4 打包後的代碼長什麼樣,跟咱們上文的簡化模型有何區別(爲了格式好看點,我精簡了一下,用圖片表示):
能夠看到,webpack中,有個__webpack_require__
和
__webpack_exports__
是否是很眼熟?而後再認真觀察,有個Module對象,key是模塊名,value是代碼塊。輸出的也是當即執行函數,從入口開始執行...
這裏也是放一個簡化的圖,由於源碼,太多啦!以下:
能夠看到,這裏的核心邏輯,和簡化版實際上是同樣的。這裏的moduleId就是模塊路徑,如./src/commonjs/index.js。要注意的是,webpack4中只有optimization.namedModules
爲true,此時moduleId纔會爲模塊路徑,不然是數字id。爲了方便開發者調試,在development
模式下optimization.namedModules參數默認爲true。
其實簡單模型仍是很好理解的。咱們理解了以後,就能夠更方便地深刻去了解webpack的多入口打包(應該一樣的機制跑2次就能夠了吧),公共包抽離(由於模塊加載時有緩存,只有加上一個次數記錄就能夠知道這個包被加載了多少次,就能夠抽離出來作公共包)了。固然仍是不少細節的地方,須要耐心細緻地去理解的。持續學習吧!