[實踐系列] 主要是讓咱們經過實踐去加深對一些原理的理解。前端
[實踐系列]前端路由node
[實踐系列]Babel 原理webpack
[實踐系列]你能手寫一個 Promise 嗎?Yes I promise。es6
有興趣的同窗能夠關注 [實踐系列] 。 求 star 求 follow~github
本文經過實現一個簡單 webpack,來理解它的打包原理,看完不懂直接盤我 !!!
web
本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle。webpack 就像一條生產線,要通過一系列處理流程後才能將源文件轉換成輸出結果。 這條生產線上的每一個處理流程的職責都是單一的,多個流程之間有存在依賴關係,只有完成當前處理後才能交給下一個流程去處理。 插件就像是一個插入到生產線中的一個功能,在特定的時機對生產線上的資源作處理。
webpack 經過 Tapable 來組織這條複雜的生產線。 webpack 在運行過程當中會廣播事件,插件只須要監聽它所關心的事件,就能加入到這條生產線中,去改變生產線的運做。 webpack 的事件流機制保證了插件的有序性,使得整個系統擴展性很好。 -- 深刻淺出 webpack 吳浩麟promise
入口起點(entry point)指示 webpack 應該使用哪一個模塊,來做爲構建其內部依賴圖的開始。瀏覽器
進入入口起點後,webpack 會找出有哪些模塊和庫是入口起點(直接和間接)依賴的。緩存
每一個依賴項隨即被處理,最後輸出到稱之爲 bundles 的文件中。
output 屬性告訴 webpack 在哪裏輸出它所建立的 bundles,以及如何命名這些文件,默認值爲 ./dist。
基本上,整個應用程序結構,都會被編譯到你指定的輸出路徑的文件夾中。
模塊,在 Webpack 裏一切皆模塊,一個模塊對應着一個文件。Webpack 會從配置的 Entry 開始遞歸找出全部依賴的模塊。
代碼塊,一個 Chunk 由多個模塊組合而成,用於代碼合併與分割。
loader 讓 webpack 可以去處理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。
loader 能夠將全部類型的文件轉換爲 webpack 可以處理的有效模塊,而後你就能夠利用 webpack 的打包能力,對它們進行處理。
本質上,webpack loader 將全部類型的文件,轉換爲應用程序的依賴圖(和最終的 bundle)能夠直接引用的模塊。
loader 被用於轉換某些類型的模塊,而插件則能夠用於執行範圍更廣的任務。
插件的範圍包括,從打包優化和壓縮,一直到從新定義環境中的變量。插件接口功能極其強大,能夠用來處理各類各樣的任務。
Webpack 的運行流程是一個串行的過程,從啓動到結束會依次執行如下流程 :
在以上過程當中,Webpack 會在特定的時間點廣播出特定的事件,插件在監聽到感興趣的事件後會執行特定的邏輯,而且插件能夠調用 Webpack 提供的 API 改變 Webpack 的運行結果。
class Compiler { constructor(options) { // webpack 配置 const { entry, output } = options // 入口 this.entry = entry // 出口 this.output = output // 模塊 this.modules = [] } // 構建啓動 run() {} // 重寫 require函數,輸出bundle generate() {} }
咱們這裏使用@babel/parser,這是 babel7 的工具,來幫助咱們分析內部的語法,包括 es6,返回一個 AST 抽象語法樹。
// webpack.config.js const path = require('path') module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, './dist'), filename: 'main.js' } } //
const fs = require('fs') const parser = require('@babel/parser') const options = require('./webpack.config') const Parser = { getAst: path => { // 讀取入口文件 const content = fs.readFileSync(path, 'utf-8') // 將文件內容轉爲AST抽象語法樹 return parser.parse(content, { sourceType: 'module' }) } } class Compiler { constructor(options) { // webpack 配置 const { entry, output } = options // 入口 this.entry = entry // 出口 this.output = output // 模塊 this.modules = [] } // 構建啓動 run() { const ast = Parser.getAst(this.entry) } // 重寫 require函數,輸出bundle generate() {} } new Compiler(options).run()
Babel 提供了@babel/traverse(遍歷)方法維護這 AST 樹的總體狀態,咱們這裏使用它來幫咱們找出依賴模塊。
const fs = require('fs') const path = require('path') const options = require('./webpack.config') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const Parser = { getAst: path => { // 讀取入口文件 const content = fs.readFileSync(path, 'utf-8') // 將文件內容轉爲AST抽象語法樹 return parser.parse(content, { sourceType: 'module' }) }, getDependecies: (ast, filename) => { const dependecies = {} // 遍歷全部的 import 模塊,存入dependecies traverse(ast, { // 類型爲 ImportDeclaration 的 AST 節點 (即爲import 語句) ImportDeclaration({ node }) { const dirname = path.dirname(filename) // 保存依賴模塊路徑,以後生成依賴關係圖須要用到 const filepath = './' + path.join(dirname, node.source.value) dependecies[node.source.value] = filepath } }) return dependecies } } class Compiler { constructor(options) { // webpack 配置 const { entry, output } = options // 入口 this.entry = entry // 出口 this.output = output // 模塊 this.modules = [] } // 構建啓動 run() { const { getAst, getDependecies } = Parser const ast = getAst(this.entry) const dependecies = getDependecies(ast, this.entry) } // 重寫 require函數,輸出bundle generate() {} } new Compiler(options).run()
將 AST 語法樹轉換爲瀏覽器可執行代碼,咱們這裏使用@babel/core 和 @babel/preset-env。
const fs = require('fs') const path = require('path') const options = require('./webpack.config') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const { transformFromAst } = require('@babel/core') const Parser = { getAst: path => { // 讀取入口文件 const content = fs.readFileSync(path, 'utf-8') // 將文件內容轉爲AST抽象語法樹 return parser.parse(content, { sourceType: 'module' }) }, getDependecies: (ast, filename) => { const dependecies = {} // 遍歷全部的 import 模塊,存入dependecies traverse(ast, { // 類型爲 ImportDeclaration 的 AST 節點 (即爲import 語句) ImportDeclaration({ node }) { const dirname = path.dirname(filename) // 保存依賴模塊路徑,以後生成依賴關係圖須要用到 const filepath = './' + path.join(dirname, node.source.value) dependecies[node.source.value] = filepath } }) return dependecies }, getCode: ast => { // AST轉換爲code const { code } = transformFromAst(ast, null, { presets: ['@babel/preset-env'] }) return code } } class Compiler { constructor(options) { // webpack 配置 const { entry, output } = options // 入口 this.entry = entry // 出口 this.output = output // 模塊 this.modules = [] } // 構建啓動 run() { const { getAst, getDependecies, getCode } = Parser const ast = getAst(this.entry) const dependecies = getDependecies(ast, this.entry) const code = getCode(ast) } // 重寫 require函數,輸出bundle generate() {} } new Compiler(options).run()
const fs = require('fs') const path = require('path') const options = require('./webpack.config') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const { transformFromAst } = require('@babel/core') const Parser = { getAst: path => { // 讀取入口文件 const content = fs.readFileSync(path, 'utf-8') // 將文件內容轉爲AST抽象語法樹 return parser.parse(content, { sourceType: 'module' }) }, getDependecies: (ast, filename) => { const dependecies = {} // 遍歷全部的 import 模塊,存入dependecies traverse(ast, { // 類型爲 ImportDeclaration 的 AST 節點 (即爲import 語句) ImportDeclaration({ node }) { const dirname = path.dirname(filename) // 保存依賴模塊路徑,以後生成依賴關係圖須要用到 const filepath = './' + path.join(dirname, node.source.value) dependecies[node.source.value] = filepath } }) return dependecies }, getCode: ast => { // AST轉換爲code const { code } = transformFromAst(ast, null, { presets: ['@babel/preset-env'] }) return code } } class Compiler { constructor(options) { // webpack 配置 const { entry, output } = options // 入口 this.entry = entry // 出口 this.output = output // 模塊 this.modules = [] } // 構建啓動 run() { // 解析入口文件 const info = this.build(this.entry) this.modules.push(info) this.modules.forEach(({ dependecies }) => { // 判斷有依賴對象,遞歸解析全部依賴項 if (dependecies) { for (const dependency in dependecies) { this.modules.push(this.build(dependecies[dependency])) } } }) // 生成依賴關係圖 const dependencyGraph = this.modules.reduce( (graph, item) => ({ ...graph, // 使用文件路徑做爲每一個模塊的惟一標識符,保存對應模塊的依賴對象和文件內容 [item.filename]: { dependecies: item.dependecies, code: item.code } }), {} ) } build(filename) { const { getAst, getDependecies, getCode } = Parser const ast = getAst(filename) const dependecies = getDependecies(ast, filename) const code = getCode(ast) return { // 文件路徑,能夠做爲每一個模塊的惟一標識符 filename, // 依賴對象,保存着依賴模塊路徑 dependecies, // 文件內容 code } } // 重寫 require函數,輸出bundle generate() {} } new Compiler(options).run()
const fs = require('fs') const path = require('path') const options = require('./webpack.config') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const { transformFromAst } = require('@babel/core') const Parser = { getAst: path => { // 讀取入口文件 const content = fs.readFileSync(path, 'utf-8') // 將文件內容轉爲AST抽象語法樹 return parser.parse(content, { sourceType: 'module' }) }, getDependecies: (ast, filename) => { const dependecies = {} // 遍歷全部的 import 模塊,存入dependecies traverse(ast, { // 類型爲 ImportDeclaration 的 AST 節點 (即爲import 語句) ImportDeclaration({ node }) { const dirname = path.dirname(filename) // 保存依賴模塊路徑,以後生成依賴關係圖須要用到 const filepath = './' + path.join(dirname, node.source.value) dependecies[node.source.value] = filepath } }) return dependecies }, getCode: ast => { // AST轉換爲code const { code } = transformFromAst(ast, null, { presets: ['@babel/preset-env'] }) return code } } class Compiler { constructor(options) { // webpack 配置 const { entry, output } = options // 入口 this.entry = entry // 出口 this.output = output // 模塊 this.modules = [] } // 構建啓動 run() { // 解析入口文件 const info = this.build(this.entry) this.modules.push(info) this.modules.forEach(({ dependecies }) => { // 判斷有依賴對象,遞歸解析全部依賴項 if (dependecies) { for (const dependency in dependecies) { this.modules.push(this.build(dependecies[dependency])) } } }) // 生成依賴關係圖 const dependencyGraph = this.modules.reduce( (graph, item) => ({ ...graph, // 使用文件路徑做爲每一個模塊的惟一標識符,保存對應模塊的依賴對象和文件內容 [item.filename]: { dependecies: item.dependecies, code: item.code } }), {} ) this.generate(dependencyGraph) } build(filename) { const { getAst, getDependecies, getCode } = Parser const ast = getAst(filename) const dependecies = getDependecies(ast, filename) const code = getCode(ast) return { // 文件路徑,能夠做爲每一個模塊的惟一標識符 filename, // 依賴對象,保存着依賴模塊路徑 dependecies, // 文件內容 code } } // 重寫 require函數 (瀏覽器不能識別commonjs語法),輸出bundle generate(code) { // 輸出文件路徑 const filePath = path.join(this.output.path, this.output.filename) // 懵逼了嗎? 沒事,下一節咱們捋一捋 const bundle = `(function(graph){ function require(module){ function localRequire(relativePath){ return require(graph[module].dependecies[relativePath]) } var exports = {}; (function(require,exports,code){ eval(code) })(localRequire,exports,graph[module].code); return exports; } require('${this.entry}') })(${JSON.stringify(code)})` // 把文件內容寫入到文件系統 fs.writeFileSync(filePath, bundle, 'utf-8') } } new Compiler(options).run()
咱們經過下面的例子來進行講解,先死亡凝視 30 秒
;(function(graph) { function require(moduleId) { function localRequire(relativePath) { return require(graph[moduleId].dependecies[relativePath]) } var exports = {} ;(function(require, exports, code) { eval(code) })(localRequire, exports, graph[moduleId].code) return exports } require('./src/index.js') })({ './src/index.js': { dependecies: { './hello.js': './src/hello.js' }, code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));' }, './src/hello.js': { dependecies: {}, code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.say = say;\n\nfunction say(name) {\n return "hello ".concat(name);\n}' } })
// 定義一個當即執行函數,傳入生成的依賴關係圖 ;(function(graph) { // 重寫require函數 function require(moduleId) { console.log(moduleId) // ./src/index.js } // 從入口文件開始執行 require('./src/index.js') })({ './src/index.js': { dependecies: { './hello.js': './src/hello.js' }, code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));' }, './src/hello.js': { dependecies: {}, code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.say = say;\n\nfunction say(name) {\n return "hello ".concat(name);\n}' } })
// 定義一個當即執行函數,傳入生成的依賴關係圖 ;(function(graph) { // 重寫require函數 function require(moduleId) { ;(function(code) { console.log(code) // "use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack")); eval(code) // Uncaught TypeError: Cannot read property 'code' of undefined })(graph[moduleId].code) } // 從入口文件開始執行 require('./src/index.js') })({ './src/index.js': { dependecies: { './hello.js': './src/hello.js' }, code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));' }, './src/hello.js': { dependecies: {}, code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.say = say;\n\nfunction say(name) {\n return "hello ".concat(name);\n}' } })
能夠看到,咱們在執行"./src/index.js"文件代碼的時候報錯了,這是由於 index.js 裏引用依賴 hello.js,而咱們沒有對依賴進行處理,接下來咱們對依賴引用進行處理。
// 定義一個當即執行函數,傳入生成的依賴關係圖 ;(function(graph) { // 重寫require函數 function require(moduleId) { // 找到對應moduleId的依賴對象,調用require函數,eval執行,拿到exports對象 function localRequire(relativePath) { return require(graph[moduleId].dependecies[relativePath]) // {__esModule: true, say: ƒ say(name)} } // 定義exports對象 var exports = {} ;(function(require, exports, code) { // commonjs語法使用module.exports暴露實現,咱們傳入的exports對象會捕獲依賴對象(hello.js)暴露的實現(exports.say = say)並寫入 eval(code) })(localRequire, exports, graph[moduleId].code) // 暴露exports對象,即暴露依賴對象對應的實現 return exports } // 從入口文件開始執行 require('./src/index.js') })({ './src/index.js': { dependecies: { './hello.js': './src/hello.js' }, code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));' }, './src/hello.js': { dependecies: {}, code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.say = say;\n\nfunction say(name) {\n return "hello ".concat(name);\n}' } })
這下應該明白了吧 ~ 能夠直接複製上面代碼到控制檯輸出哦~
Webpack 是一個龐大的 Node.js 應用,若是你閱讀過它的源碼,你會發現實現一個完整的 Webpack 須要編寫很是多的代碼。 但你無需瞭解全部的細節,只需瞭解其總體架構和部分細節便可。對 Webpack 的使用者來講,它是一個簡單強大的工具; 對 Webpack 的開發者來講,它是一個擴展性的高系統。
Webpack 之因此能成功,在於它把複雜的實現隱藏了起來,給用戶暴露出的只是一個簡單的工具,讓用戶能快速達成目的。 同時總體架構設計合理,擴展性高,開發擴展難度不高,經過社區補足了大量缺失的功能,讓 Webpack 幾乎能勝任任何場景。
若是你和我同樣喜歡前端,也愛動手摺騰,歡迎關注我一塊兒玩耍啊~ ❤️
前端時刻