通过bundler学习webpack模块依赖分析

in 应用教程
关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9

你知道webpack是如何分析模块各个依赖关系的?是如何将ES6代码编译成浏览器可执行代码的吗?

项目初始化

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的代码,然后直接在浏览器中运行。

入口文件依赖分析

在项目根目录下新建一个bundler文件,实现打包过程。其实所谓的webpack编译打包就是通过一些特定的方法函数将源代码转换成浏览器可识别的代码

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命令

node bundler.js

就会输出index.js的文件内容

(1)执行npm init -y初始化

(2)安装一个babel模块

npm install @babel/parser --save

(3)使用parser

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,描述了文件的相关依赖关系。

修改一下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命令就会得到如下输出内容

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代码转换编译的整个过程。继续学习中!

参考资料

关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9
扫一扫关注公众号添加购物返利助手,领红包
Comments are closed.

推荐使用阿里云服务器

超多优惠券

服务器最低一折,一年不到100!

朕已阅去看看