經過bundler學習webpack模塊依賴分析

你知道webpack是如何分析模塊各個依賴關係的?是如何將ES6代碼編譯成瀏覽器可執行代碼的嗎?node

項目初始化

  • 建立文件夾
mkdir bundler
cd bundler
複製代碼
  • 建立相關文件 在bundler中建立src文件夾,在src文件夾新建index.js,message.js,word.js。文件內容以下:
// word.js
export const word="word";

// message.js
import {word} from "./word.js";
const message=`hello ${word}`;
export default message;

// index.js
import message from "./message.js";
console.log(message);
複製代碼

若是想直接在瀏覽器中運行index.js的話,固然是不能的,瀏覽器沒法識別es6的語法,之前咱們都是經過相似webpack的打包工具將es6代碼轉換成es5的代碼,而後直接在瀏覽器中運行。webpack

入口文件依賴分析

在項目根目錄下新建一個bundler文件,實現打包過程。其實所謂的webpack編譯打包就是經過一些特定的方法函數將源代碼轉換成瀏覽器可識別的代碼es6

  • 定義一個模塊分析函數
const moduleAnalyser=(filename)=>{

}
moduleAnalyser("./src/index.js");// 入口函數
複製代碼
  • 讀取文件內容 這裏使用了node中的一個核心模塊fs。
const fs=require("fs");

const moduleAnalyser=(filename)=>{
    const content=fs.readFileSync(filename,"utf-8");// 讀取文件內容
    console.log(content);
}
moduleAnalyser("./src/index.js");// 入口函數
複製代碼

在終端中執行node命令web

node bundler.js
複製代碼

就會輸出index.js的文件內容 npm

  • 解析文件依賴

(1)執行npm init -y初始化數組

(2)安裝一個babel模塊瀏覽器

npm install @babel/parser --save
複製代碼

(3)使用parserbash

const fs=require("fs");
const parser=require("@babel/parser");

const moduleAnalyser=(filename)=>{
    const content=fs.readFileSync(filename,"utf-8");// 讀取文件內容
    console.log(parser.parse(content,{
        sourceType:"module"
    }));
}

moduleAnalyser("./src/index.js");// 入口函數
複製代碼

再次執行node命令,node bundler.js,查看文件內容,輸出的就是常說的AST,描述了文件的相關依賴關係。 babel

修改一下bundler函數

...
const ast=parser.parse(content,{
        sourceType:"module"
    })
    console.log(ast.program.body);
...
複製代碼

執行node bundler.js命令就會獲得以下輸出內容

輸出的就是文件相關依賴,type爲ImportDeclaration表示是引入聲明,type爲ExpressionStatement表示是表達式。接下來要作的就是遍歷body的內容獲得依賴關係。
(4)安裝模塊

npm install @babel/traverse --save
複製代碼

(5)使用traverse

...
traverse(ast,{
        ImportDeclaration({node}){
            console.log(node)// 查看node內容
        }
    })
...
複製代碼

繼續改寫bundler.js

const dependencies=[];
    traverse(ast,{
        ImportDeclaration({node}){
            dependencies.push(node.source.value);
        }
    })
    console.log(dependencies)// 獲得依賴數組
複製代碼

繼續改寫bundler.js

const dependencies={};// 變成對象,key是依賴路徑,value是相對依賴路徑。便於以後使用
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname=path.dirname(filename);//filename對應的文件夾路徑
            const newFile="./"+path.join(dirname,node.source.value);
            dependencies[node.source.value]=newFile;
        }
    })
複製代碼

(6)安裝babel/core轉換代碼

npm install @babel/core @babel/preset-env --save
複製代碼

(7)轉換代碼

const { code }=babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })//轉換ast
    console.log(code);
複製代碼

執行node bundler.js命令就會獲得以下輸出內容

入口文件的依賴分析就完成了。完整代碼以下:

const fs=require("fs");
const path=require("path");
const babel=require("@babel/core");
const parser=require("@babel/parser");
const traverse=require("@babel/traverse").default;// 默認es module導出

const moduleAnalyser=(filename)=>{
    const content=fs.readFileSync(filename,"utf-8");// 讀取文件內容
    const ast=parser.parse(content,{
        sourceType:"module"
    })
    const dependencies={};
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname=path.dirname(filename);//filename對應的文件夾路徑
            const newFile="./"+path.join(dirname,node.source.value);
            dependencies[node.source.value]=newFile;
        }
    })
    const { code } = babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })//轉換ast
    return {
        filename,
        dependencies,
        code
    }
}

const moduleInfo=moduleAnalyser("./src/index.js");// 入口函數
console.log(moduleInfo);
複製代碼

構建依賴圖譜

一個項目不可能只有一個文件,這就須要咱們分析整個項目的依賴關係,即生成獲得依賴圖譜。

  • 定義生成依賴圖譜方法
const makeDependenciesGraph=(entry)=>{
    const entryModule=moduleAnalyser(entry);
    console.log(entryModule);
}
複製代碼
  • 從入口開始,循環遞歸分析依賴關係
const makeDependenciesGraph=(entry)=>{
    const entryModule=moduleAnalyser(entry);
    const graphArray=[entryModule];
    for(let i=0;i<graphArray.length;i++){
        const item=graphArray[i];
        const { dependencies } = item; // 解構出依賴
        if(dependencies){
            for(let j in dependencies){
                // 遞歸分析依賴,放入依賴圖譜數組
                graphArray.push(moduleAnalyser(dependencies[j]))
            }
        }
    }
    console.log(graphArray);
}
複製代碼

執行node bundler.js命令就會獲得以下輸出內容

  • 生成依賴圖譜graph對象,完整代碼以下:
const makeDependenciesGraph=(entry)=>{
    const entryModule=moduleAnalyser(entry);
    const graphArray=[entryModule];
    for(let i=0;i<graphArray.length;i++){
        const item=graphArray[i];
        const { dependencies } = item; // 解構出依賴
        if(dependencies){
            for(let j in dependencies){
                // 遞歸分析依賴,放入依賴圖譜數組
                graphArray.push(moduleAnalyser(dependencies[j]))
            }
        }
    }
    // 轉換爲對象 便於使用
    const graph={}
    graphArray.forEach(item=>{
        graph[item.filename]={
            dependencies:item.dependencies,
            code:item.code
        }
    });
    return graph;
}
複製代碼

生成瀏覽器可識別代碼

const generateCode=(entry)=>{
    const graph=JSON.stringify(makeDependenciesGraph(entry));
    return `
        (function(graph){
            function require(module){
                function localRequire(relativePath){
                    return require(graph[module].dependencies[relativePath])
                }
                var exports={};
                (function(require,exports,code){
                    eval(code)
                })(localRequire,exports,graph[module].code);
                return exports;
            };
            require('${entry}');
        })(${graph})
    `;
}
複製代碼

執行node bundler.js命令就會獲得以下輸出內容

將輸入內容拷貝後到瀏覽器console中執行,便會獲得代碼的正常輸出

bundler文件完整代碼:

const fs=require("fs");
const path=require("path");
const babel=require("@babel/core");
const parser=require("@babel/parser");
const traverse=require("@babel/traverse").default;// 默認es module導出

const moduleAnalyser=(filename)=>{
    const content=fs.readFileSync(filename,"utf-8");// 讀取文件內容
    const ast=parser.parse(content,{
        sourceType:"module"
    })
    const dependencies={};
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname=path.dirname(filename);//filename對應的文件夾路徑
            const newFile="./"+path.join(dirname,node.source.value);
            dependencies[node.source.value]=newFile;
        }
    })
    const { code }=babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })//轉換ast
    return {
        filename,
        dependencies,
        code
    }
}

const makeDependenciesGraph=(entry)=>{
    const entryModule=moduleAnalyser(entry);
    const graphArray=[entryModule];
    for(let i=0;i<graphArray.length;i++){
        const item=graphArray[i];
        const { dependencies } = item; // 解構出依賴
        if(dependencies){
            for(let j in dependencies){
                // 遞歸分析依賴,放入依賴圖譜數組
                graphArray.push(moduleAnalyser(dependencies[j]))
            }
        }
    }
    // 轉換爲對象 便於使用
    const graph={}
    graphArray.forEach(item=>{
        graph[item.filename]={
            dependencies:item.dependencies,
            code:item.code
        }
    });
    return graph;
}

const generateCode=(entry)=>{
    const graph=JSON.stringify(makeDependenciesGraph(entry));
    return `
        (function(graph){
            function require(module){
                function localRequire(relativePath){
                    return require(graph[module].dependencies[relativePath])
                }
                var exports={};
                (function(require,exports,code){
                    eval(code)
                })(localRequire,exports,graph[module].code);
                return exports;
            };
            require('${entry}');
        })(${graph})
    `;
}

const code=generateCode("./src/index.js");// 入口函數
console.log(code);
複製代碼

以上就是一個webpack代碼轉換編譯的整個過程。繼續學習中!

參考資料

相關文章
相關標籤/搜索