實現一個簡單的Webpack

你們好,我是神三元,今天經過一道面試題來和你們聊一聊webpack。前端

1、什麼是Webpack

我相信,儘管不少開發者會根據官方文檔進行webpack的相關配置,但仍然並不瞭解Webpack到底是起什麼做用的,在前端工程化扮演者什麼角色,觀念仍然簡單地停留在「代碼打包工具」上。真的是這樣嗎? 讓咱們來看看官方定義:node

本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle。webpack

相信這個定義已經說的很是清楚了。首先,它的本質是一個模塊打包器,其工做是將每一個模塊打包成相應的bundle。那麼在這中間究竟作了什麼事情呢?web

2、場景引入

假如你是一個面試者,請看題:面試

在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的定義,那麼這裏思路應該很是清晰:數組

  • 一、利用babel完成代碼轉換,並生成單個文件的依賴
  • 二、生成依賴圖譜
  • 三、生成最後打包代碼

接下來,讓咱們來一步步解開bundler的面紗。瀏覽器

3、步步爲營

第一步:轉換代碼、 生成依賴

轉換代碼須要利用@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有了一個清晰的認知,這樣個人目的也就達到了。中間會有一部分代碼比較繞,但不要緊,相信你很快就能啃下來,必定會收穫滿滿。

我是神三元,但願這篇文章可以幫助到更多的同窗。加油吧!

相關文章
相關標籤/搜索