你們好,我是神三元,今天經過一道面試題來和你們聊一聊webpack。前端
我相信,儘管不少開發者會根據官方文檔進行webpack的相關配置,但仍然並不瞭解Webpack到底是起什麼做用的,在前端工程化扮演者什麼角色,觀念仍然簡單地停留在「代碼打包工具」上。真的是這樣嗎? 讓咱們來看看官方定義:node
本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle。webpack
相信這個定義已經說的很是清楚了。首先,它的本質是一個模塊打包器,其工做是將每一個模塊打包成相應的bundle。那麼在這中間究竟作了什麼事情呢?web
假如你是一個面試者,請看題:面試
在src目錄下有如下文件:npm
//word.js
export const word = 'hello'
複製代碼
//message.js
import {word} from './word.js';
const message = `say ${word}`
export default message;
複製代碼
//index.js
import message from './message.js'
console.log(message)
複製代碼
請編寫一個bundler.js,將其中的ES6代碼轉換爲ES5代碼,並將這些文件打包,生成一段能在瀏覽器正確運行起來的代碼。(最後輸出say hello)前端工程化
若是你真正理解了Webpack的定義,那麼這裏思路應該很是清晰:數組
接下來,讓咱們來一步步解開bundler的面紗。瀏覽器
轉換代碼須要利用@babel/parser生成AST抽象語法樹,而後利用@babel/traverse進行AST遍歷,記錄依賴關係,最後經過@babel/core和@babel/preset-env進行代碼的轉換babel
//先安裝好相應的包
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D
複製代碼
//導入包
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
複製代碼
function stepOne(filename){
//讀入文件
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'//babel官方規定必須加這個參數,否則沒法識別ES Module
})
const dependencies = {}
//遍歷AST抽象語法樹
traverse(ast, {
//獲取經過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//轉換後的代碼
}
}
複製代碼
//entry爲入口文件
function stepTwo(entry){
const entryModule = stepOne(entry)
//這個數組是核心,雖然如今只有一個元素,日後看你就會明白
const graphArray = [entryModule]
for(let i = 0; i < graphArray.length; i++){
const item = graphArray[i];
const {dependencies} = item;//拿到文件所依賴的模塊集合(鍵值對存儲)
for(let j in dependencies){
graphArray.push(
stepOne(dependencies[j])
)//敲黑板!關鍵代碼,目的是將入口模塊及其全部相關的模塊放入數組
}
}
//接下來生成圖譜
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph
}
複製代碼
//測試一下
console.log(stepTwo('./src/index.js'))
//結果以下,是否是很神奇鴨
{
'./src/index.js':
{ dependencies: { './message.js': './src\\message.js' },
code:
'"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_message["default"]);'
},
'./src\\message.js':
{ dependencies: { './word.js': './src\\word.js' },
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nvar _word = require("./word.js");\n\nvar message = "say ".concat(_word.word);\nvar _default = message;\nexports["default"] = _default;' },
'./src\\word.js':
{ dependencies: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.word = void 0;\nvar word = \'hello\';\nexports.word = word;'
}
}
複製代碼
//下面是生成代碼字符串的操做,仔細看,不要眨眼睛哦!
function step3(entry){
//要先把對象轉換爲字符串,否則在下面的模板字符串中會默認調取對象的toString方法,參數變成[Object object],顯然不行
const graph = JSON.stringify(stepTwo(entry))
return ` (function(graph) { //require函數的本質是執行一個模塊的代碼,而後將相應變量掛載到exports對象上 function require(module) { //localRequire的本質是拿到依賴包的exports變量 function localRequire(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function(require, exports, code) { eval(code); })(localRequire, exports, graph[module].code); return exports;//函數返回指向局部變量,造成閉包,exports變量在函數執行後不會被摧毀 } require('${entry}') })(${graph})`
}
複製代碼
//最終測試
const code = step3('./src/index.js')
console.log(code)
複製代碼
將生成的這段代碼字符串放在瀏覽器端執行,
大功告成!其實你再新建一個dist目錄,將這些字符串放在main.js文件裏,是否是跟你平日裏開發npm run build同樣的效果呢?
那這個時候就有人要"發炎"了,說你這題目不是讓人手寫一個Webpack嗎?確實,可是真正意義上的Webpack須要考慮很是多的因素,事實上要龐大不少,不過經過這一波實踐你應該更加理解了Webpack所作的事情,對Webpack有了一個清晰的認知,這樣個人目的也就達到了。中間會有一部分代碼比較繞,但不要緊,相信你很快就能啃下來,必定會收穫滿滿。
我是神三元,但願這篇文章可以幫助到更多的同窗。加油吧!