手寫webpack核心原理,不再怕面試官問我webpack原理

手寫webpack核心原理

1、核心打包原理

1.1 打包的主要流程以下

  1. 須要讀到入口文件裏面的內容。
  2. 分析入口文件,遞歸的去讀取模塊所依賴的文件內容,生成AST語法樹。
  3. 根據AST語法樹,生成瀏覽器可以運行的代碼

1.2 具體細節

  1. 獲取主模塊內容
  2. 分析模塊
    • 安裝@babel/parser包(轉AST)
  3. 對模塊內容進行處理
    • 安裝@babel/traverse包(遍歷AST收集依賴)
    • 安裝@babel/core和@babel/preset-env包 (es6轉ES5)
  4. 遞歸全部模塊
  5. 生成最終代碼

2、基本準備工做

咱們先建一個項目html

項目目錄暫時以下:node

已經把項目放到 githubhttps://github.com/Sunny-lucking/howToBuildMyWebpack。 能夠卑微地要個star嗎webpack

咱們建立了add.js文件和minus.js文件,而後 在index.js中引入,再將index.js文件引入index.html。git

代碼以下:es6

add.jsgithub

export default (a,b)=>{
  return a+b;
}

minus.jsweb

export const minus = (a,b)=>{
    return a-b
}

index.jsnpm

import add from "./add"
import {minus} from "./minus";

const sum = add(1,2);
const division = minus(2,1);

console.log(sum);
console.log(division);

index.html數組

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>

如今咱們打開index.html。你猜會發生什麼???顯然會報錯,由於瀏覽器還不能識別import語法瀏覽器

不過不要緊,由於咱們原本就是要來解決這些問題的。

3、獲取模塊內容

好了,如今咱們開始根據上面核心打包原理的思路來實踐一下,第一步就是 實現獲取模塊內容。

咱們來建立一個bundle.js文件。

// 獲取主入口文件
const fs = require('fs')
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    console.log(body);
}
getModuleInfo("./src/index.js")

目前項目目錄以下

咱們來執行一下bundle.js,看看時候成功得到入口文件內容

哇塞,不出所料的成功。一切盡在掌握之中。好了,已經實現第一步了,且讓我看看第二步是要幹嗎。

哦?是分析模塊了

4、分析模塊

分析模塊的主要任務是 將獲取到的模塊內容 解析成AST語法樹,這個須要用到一個依賴包@babel/parser

npm install @babel/parser

ok,安裝完成咱們將@babel/parser引入bundle.js,

// 獲取主入口文件
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示咱們要解析的是ES模塊
    });
    console.log(ast);
}
getModuleInfo("./src/index.js")

咱們去看下@babel/parser的文檔:


可見提供了三個API,而咱們目前用到的是parse這個API。

它的主要做用是 parses the provided code as an entire ECMAScript program,也就是將咱們提供的代碼解析成完整的ECMAScript代碼。

再看看該API提供的參數


咱們暫時用到的是sourceType,也就是用來指明咱們要解析的代碼是什麼模塊。

好了,如今咱們來執行一下 bundle.js,看看AST是否成功生成。

成功。又是不出所料的成功。

不過,咱們須要知道的是,當前咱們解析出來的不僅僅是index.js文件裏的內容,它也包括了文件的其餘信息。
而它的內容實際上是它的屬性program裏的body裏。如圖所示

咱們能夠改爲打印ast.program.body看看

// 獲取主入口文件
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示咱們要解析的是ES模塊
    });
    console.log(ast.program.body);
}
getModuleInfo("./src/index.js"

執行

看,如今打印出來的就是 index.js文件裏的內容(也就是咱們再index.js裏寫的代碼啦).

5、收集依賴

如今咱們須要 遍歷AST,將用到的依賴收集起來。什麼意思呢?其實就是將用import語句引入的文件路徑收集起來。咱們將收集起來的路徑放到deps裏。

前面咱們提到過,遍歷AST要用到@babel/traverse依賴包

npm install @babel/traverse

如今,咱們引入。

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示咱們要解析的是ES模塊
    });
    
    // 新增代碼
    const deps = {}
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file)
            const abspath = './' + path.join(dirname,node.source.value)
            deps[node.source.value] = abspath
        }
    })
    console.log(deps);


}
getModuleInfo("./src/index.js")

咱們來看下官方文檔對@babel/traverse的描述


好吧,如此簡略

不過咱們不難看出,第一個參數就是AST。第二個參數就是配置對象

咱們看看咱們寫的代碼

traverse(ast,{
    ImportDeclaration({node}){
        const dirname = path.dirname(file)
        const abspath = './' + path.join(dirname,node.source.value)
        deps[node.source.value] = abspath
    }
})

配置對象裏,咱們配置了ImportDeclaration方法,這是什麼意思呢?
咱們看看以前打印出來的AST。

ImportDeclaration方法表明的是對type類型爲ImportDeclaration的節點的處理。

這裏咱們得到了該節點中source的value,也就是node.source.value,

這裏的value指的是什麼意思呢?其實就是import的值,能夠看咱們的index.js的代碼。

import add from "./add"
import {minus} from "./minus";

const sum = add(1,2);
const division = minus(2,1);

console.log(sum);
console.log(division);

可見,value指的就是import後面的 './add' 和 './minus'

而後咱們將file目錄路徑跟得到的value值拼接起來保存到deps裏,美其名曰:收集依賴。

ok,這個操做就結束了,執行看看收集成功了沒?

oh my god。又成功了。

6、ES6轉成ES5(AST)

如今咱們須要把得到的ES6的AST轉化成ES5的AST,前面講到過,執行這一步須要兩個依賴包

npm install @babel/core @babel/preset-env

咱們如今將依賴引入並使用

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 getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示咱們要解析的是ES模塊
    });
    const deps = {}
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file)
            const abspath = "./" + path.join(dirname,node.source.value)
            deps[node.source.value] = abspath
        }
    })
    
    新增代碼
    const {code} = babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })
    console.log(code);

}
getModuleInfo("./src/index.js")

咱們看看官網文檔對@babel/core 的transformFromAst的介紹

害,又是一如既往的簡略。。。

簡單說一下,其實就是將咱們傳入的AST轉化成咱們在第三個參數裏配置的模塊類型。

好了,如今咱們來執行一下,看看結果

個人天,一如既往的成功。可見 它將咱們寫const 轉化成var了。

好了,這一步到此結束,咦,你可能會有疑問,上一步的收集依賴在這裏怎麼沒啥關係啊,確實如此。收集依賴是爲了下面進行的遞歸操做。

7、遞歸獲取全部依賴

通過上面的過程,如今咱們知道getModuleInfo是用來獲取一個模塊的內容,不過咱們還沒把獲取的內容return出來,所以,更改下getModuleInfo方法

const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示咱們要解析的是ES模塊
    });
    const deps = {}
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file)
            const abspath = "./" + path.join(dirname,node.source.value)
            deps[node.source.value] = abspath
        }
    })
    const {code} = babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })
    // 新增代碼
    const moduleInfo = {file,deps,code}
    return moduleInfo
}

咱們返回了一個對象 ,這個對象包括該模塊的路徑(file)該模塊的依賴(deps)該模塊轉化成es5的代碼

該方法只能獲取一個模塊的的信息,可是咱們要怎麼獲取一個模塊裏面的依賴模塊的信息呢?

沒錯,看標題,,你應該想到了就算遞歸。

如今咱們來寫一個遞歸方法,遞歸獲取依賴

const parseModules = (file) =>{
    const entry =  getModuleInfo(file)
    const temp = [entry]
    for (let i = 0;i<temp.length;i++){
        const deps = temp[i].deps
        if (deps){
            for (const key in deps){
                if (deps.hasOwnProperty(key)){
                    temp.push(getModuleInfo(deps[key]))
                }
            }
        }
    }
    console.log(temp)
}

講解下parseModules方法:

  1. 咱們首先傳入主模塊路徑
  2. 將得到的模塊信息放到temp數組裏。
  3. 外面的循壞遍歷temp數組,此時的temp數組只有主模塊
  4. 裏面再得到主模塊的依賴deps
  5. 遍歷deps,經過調用getModuleInfo將得到的依賴模塊信息push到temp數組裏。

目前bundle.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 getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示咱們要解析的是ES模塊
    });
    const deps = {}
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file)
            const abspath = "./" + path.join(dirname,node.source.value)
            deps[node.source.value] = abspath
        }
    })
    const {code} = babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })
    const moduleInfo = {file,deps,code}
    return moduleInfo
}

// 新增代碼
const parseModules = (file) =>{
    const entry =  getModuleInfo(file)
    const temp = [entry]
    for (let i = 0;i<temp.length;i++){
        const deps = temp[i].deps
        if (deps){
            for (const key in deps){
                if (deps.hasOwnProperty(key)){
                    temp.push(getModuleInfo(deps[key]))
                }
            }
        }
    }
    console.log(temp)
}
parseModules("./src/index.js")

按照目前咱們的項目來講執行完,應當是temp 應當是存放了index.js,add.js,minus.js三個模塊。
,執行看看。

牛逼!!!確實如此。

不過如今的temp數組裏的對象格式不利於後面的操做,咱們但願是以文件的路徑爲key,{code,deps}爲值的形式存儲。所以,咱們建立一個新的對象depsGraph。

const parseModules = (file) =>{
    const entry =  getModuleInfo(file)
    const temp = [entry] 
    const depsGraph = {} //新增代碼
    for (let i = 0;i<temp.length;i++){
        const deps = temp[i].deps
        if (deps){
            for (const key in deps){
                if (deps.hasOwnProperty(key)){
                    temp.push(getModuleInfo(deps[key]))
                }
            }
        }
    }
    // 新增代碼
    temp.forEach(moduleInfo=>{
        depsGraph[moduleInfo.file] = {
            deps:moduleInfo.deps,
            code:moduleInfo.code
        }
    })
    console.log(depsGraph)
    return depsGraph
}

ok,如今存儲的就是這種格式啦

8、處理兩個關鍵字

咱們如今的目的就是要生成一個bundle.js文件,也就是打包後的一個文件。其實思路很簡單,就是把index.js的內容和它的依賴模塊整合起來。而後把代碼寫到一個新建的js文件。

咱們把這段代碼格式化一下

// index.js
"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum); console.log(division);
// add.js
"use strict";
Object.defineProperty(exports, "__esModule", {  value: true});
exports["default"] = void 0;
var _default = function _default(a, b) {  return a + b;};
exports["default"] = _default;

可是咱們如今是不能執行index.js這段代碼的,由於瀏覽器不會識別執行require和exports。

不能識別是爲何?不就是由於沒有定義這require函數,和exports對象。那咱們能夠本身定義。

咱們建立一個函數

const bundle = (file) =>{
    const depsGraph = JSON.stringify(parseModules(file))
    
}

咱們將上一步得到的depsGraph保存起來。

如今返回一個整合完整的字符串代碼。

怎麼返回呢?更改下bundle函數

const bundle = (file) =>{
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function (graph) {
                function require(file) {
                    (function (code) {
                        eval(code)
                    })(graph[file].code)
                }
                require(file)
            })(depsGraph)`
    
}

咱們看下返回的這段代碼

(function (graph) {
        function require(file) {
            (function (code) {
                eval(code)
            })(graph[file].code)
        }
        require(file)
    })(depsGraph)

其實就是

  1. 把保存下來的depsGraph,傳入一個當即執行函數。
  2. 將主文件路徑傳入require函數執行
  3. 執行reuire函數的時候,又當即執行一個當即執行函數,這裏是把code的值傳進去了
  4. 執行eval(code)。也就是執行code這段代碼

咱們再來看下code的值

// index.js
"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum); console.log(division);

沒錯執行這段代碼的時候,又會用到require函數。此時require的參數爲add.js的路徑,哎,不是絕對路徑,須要轉化成絕對路徑。所以寫一個函數absRequire來轉化。怎麼實現呢?咱們來看下代碼

(function (graph) {
    function require(file) {
        function absRequire(relPath) {
            return require(graph[file].deps[relPath])
        }
        (function (require,code) {
            eval(code)
        })(absRequire,graph[file].code)
    }
    require(file)
})(depsGraph)

其實是實現了一層攔截。

  1. 執行require('./src/index.js')函數
  2. 執行了
(function (require,code) {
    eval(code)
})(absRequire,graph[file].code)
  1. 執行eval,也就是執行了index.js的代碼。
  2. 執行過程會執行到require函數。
  3. 這時會調用這個require,也就是咱們傳入的absRequire
  4. 而執行absRequire就執行了return require(graph[file].deps[relPath])這段代碼,也就是執行了外面這個require


在這裏return require(graph[file].deps[relPath]),咱們已經對路徑轉化成絕對路徑了。所以執行外面的require的時候就是傳入絕對路徑。

  1. 而執行require("./src/add.js")以後,又會執行eval,也就是執行add.js文件的代碼。

是否是有點繞?實際上是個遞歸。

這樣就將代碼整合起來了,可是有個問題,就是在執行add.js的code時候,會遇到exports這個還沒定義的問題。以下所示

// add.js
"use strict";
Object.defineProperty(exports, "__esModule", {  value: true});
exports["default"] = void 0;
var _default = function _default(a, b) {  return a + b;};
exports["default"] = _default;

咱們發現 這裏它把exports看成一個對象來使用了,可是這個對象還沒定義,所以咱們能夠本身定義一個exports對象。

(function (graph) {
    function require(file) {
        function absRequire(relPath) {
            return require(graph[file].deps[relPath])
        }
        var exports = {}
        (function (require,exports,code) {
            eval(code)
        })(absRequire,exports,graph[file].code)
        return exports
    }
    require(file)
})(depsGraph)

咱們增添了一個空對象 exports,執行add.js代碼的時候,會往這個空對象上增長一些屬性,

// add.js
"use strict";
Object.defineProperty(exports, "__esModule", {  value: true});
exports["default"] = void 0;
var _default = function _default(a, b) {  return a + b;};
exports["default"] = _default;

好比,執行完這段代碼後

exports = {
  __esModule:{  value: true},
  default:function _default(a, b) {  return a + b;}
}

而後咱們把exports對象return出去。

var _add = _interopRequireDefault(require("./add.js"));

可見,return出去的值,被_interopRequireDefault接收,_interopRequireDefault再返回default這個屬性給_add,所以_add = function _default(a, b) { return a + b;}

如今明白了,爲何ES6模塊 引入的是一個對象引用了吧,由於exports就是一個對象。

至此,處理;兩個關鍵詞的功能就完整了。

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 getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示咱們要解析的是ES模塊
    });
    const deps = {}
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file)
            const abspath = "./" + path.join(dirname,node.source.value)
            deps[node.source.value] = abspath
        }
    })
    const {code} = babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })
    const moduleInfo = {file,deps,code}
    return moduleInfo
}
const parseModules = (file) =>{
    const entry =  getModuleInfo(file)
    const temp = [entry]
    const depsGraph = {}
    for (let i = 0;i<temp.length;i++){
        const deps = temp[i].deps
        if (deps){
            for (const key in deps){
                if (deps.hasOwnProperty(key)){
                    temp.push(getModuleInfo(deps[key]))
                }
            }
        }
    }
    temp.forEach(moduleInfo=>{
        depsGraph[moduleInfo.file] = {
            deps:moduleInfo.deps,
            code:moduleInfo.code
        }
    })
    return depsGraph
}
// 新增代碼
const bundle = (file) =>{
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function (graph) {
        function require(file) {
            function absRequire(relPath) {
                return require(graph[file].deps[relPath])
            }
            var exports = {}
            (function (require,exports,code) {
                eval(code)
            })(absRequire,exports,graph[file].code)
            return exports
        }
        require('${file}')
    })(${depsGraph})`

}
const content = bundle('./src/index.js')

console.log(content);

來執行下,看看效果


確實執行成功。接下來,把返回的這段代碼寫入新建立的文件中

//寫入到咱們的dist目錄下
fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js',content)

至次,咱們的手寫webpack核心原理就到此結束了。

咱們參觀下生成的bundle.js文件

發現其實就是將咱們早期收集的全部依賴做爲參數傳入到當即執行函數當中,而後經過eval來遞歸地執行每一個依賴的code。

如今咱們將bundle.js文件引入index.html看看能不能執行

成功。。。。。驚喜。。

感謝您也恭喜您看到這裏,我能夠卑微的求個star嗎!!!

相關文章
相關標籤/搜索