专栏首页佛曰不可说丶带你探究webpack究竟是如何解析打包模块语法的

带你探究webpack究竟是如何解析打包模块语法的

前期准备

在webpack中,我们发现配置我们能天然的使用esmodule这种模块化语法,那大家有没有好奇过呢?他究竟是怎么实现的呢?下面一起来探究一下,webpack究竟是怎么解析打包esmodule语法的。

在研究之前,我们需要有一定的node的基础知识,应为我们如果想要实现webpack类似的功能,那么,我们必须要借助node的一些模块,比如path模块、比如fs模块,等,这些都是node的基础模块 接下来,我们还需要babel的一些模块,给我们做一些转化比如babel/parser模块、比如**@babel/traverse模块**、在比如babel/core模块等等,接下来,我们分别介绍一下用到的这些模块

模块介绍

path

NodeJS中的Path对象,用于处理目录的对象,提高开发效率

我们在配置webpack的时候也经常用到,他的常见用法就是我们的目录转换比如:

//引入进来
const path = require('path');
//拼接这些链接
console.log(path.join('/Users','node/path','../','join.js'));

fs

fs模块可以对文件进行一些读写操作

我们在webpack 中由于要转义语法,所以对文件的读写必不可少,使用方式也非常简单

//引入模块
const fs = require('fs');
//读取文件,readFileSync指的是同步读取文件,filename指的是文件路径,第二个参数指的是格式
const content = fs.readFileSync(filename, 'utf-8');

babel/parser

babel/parser是babel的一个模块,它能帮我们分析代码,并且转换长AST也就是抽象语法树

使用方式也非常简单

//引入进来
const parser = require('@babel/parser');
//解析成抽象语法树 第一个参数表示我们的代码,第二个参数是一系列配置sourceType 表示是哪种语法
const ast = parser.parse(content, {
		sourceType: 'module'
	});

babel/traverse

babel/traverse能根据抽象语法树中的信息解析出代码中的依赖关系,从而可以解析出整个esmodule的代码

使用方式也非常简单

//引入模块 
const traverse = require('@babel/traverse').default;
//第一个参数接受抽象语法树,
//第二个参数是个对象,配置的是我们需要找出的依赖关系的配置
	traverse(ast, {
		ImportDeclaration({ node }) {
		}
	});

babel/core

babel的核心模块,可以给我我们的代码转成浏览器的可以识别的代码

使用方式也不是那么难

//引入模块
const babel = require('@babel/core');
//使用transformFormAst方法
//第一个参数为ast
//最后一个参数是转换规则,转换成啥
const code = babel.transformFromAst(ast, null, {
		presets: ["@babel/preset-env"]
	});

babel/preset-env

babel/preset-env 我们是不是很熟悉,如果我们经常配置webpack的话我们会在.babelrc中配置上这一段东西,其实他就是告诉我们使用哪种规则去转化我们的es6语法,

脚手架搭建

首先我们要新建一个webpack一样的目录,里面有src有index.js的入口文件,只不过不同的是我们需要新建一个webpack.js去代替webpack目录接口如下:

探究原理

前期准备工作完成,接下来,我们开始手撸一个解析打包模块化语法的webpack

1、找到入口文件,解析入口文件语法

首先我们需要找到入口文件解析出入口文件的js语法

//引入node模块
const fs = require('fs');
const path = require('path');
//引入babel模块
const parser = require('@babel/parser');
//创建方法
const webpack=(filename)=>{
//拿到js入口文件中的内容
const content=fs.readFileSync(filename,'utf-8')
//打印内容
console.log(content)
//使用parser的parse解析成ast语法树
const ast = parser.parse(content, {
    sourceType: 'module'
});
//打印抽象语法树
console.log(ast)
}
webpack('./src/index.js')

上述代码中,我们可以拿到ast抽象语法树,我们先开看看长什么样子

我们惊喜的发现,他其实就是用一个对象去描述js语句,以及js的依赖关系,你又会说了,他的代码和依赖关系在哪呢?我们找到program下的body看一看

接下来你会发现一个醒目的value是不是找到了我们的依赖关系,而下面这个就是我们的console这个表达式了

接下来我们就要去拿到依赖关系了,应该怎么处理呢?

//使用parser的parse解析成ast语法树
const ast = parser.parse(content, {
    sourceType: 'module'
});
//打印抽象语法树
// console.log(ast.program.body)
//创建存放解析完依赖关系的对象
const dependencies = {};
//使用traverse梳理依赖关系并且解析到对象中
traverse(ast, {
    //对象参数中,由于需要找到依赖关系放入对象,所以只需要ImportDeclaration类型的回调即可
    ImportDeclaration({ node }) {
        //使用node的path模块,取出当前的文件的路径目录
        const dirname = path.dirname(filename); 
        //拼接处相工程文件根目录下的路径
        const newFile = './' + path.join(dirname, node.source.value);
        //拿到路径存入对象中
        dependencies[node.source.value] = newFile;
    }
});
console.log(dependencies)

其实也很简单,我们只需要引用babel的模块后,在回调中稍微处理一下,便可拿到,打印结果如下

如此,我们便拿到了抽象对应的依赖关系路径,但是拿到依赖关系还不够,我们现在的代码已经被转换成抽象语法树了,那么我们浏览器没办法运行啊,这时我们需要用babel的一个核心模块,给抽象语法树转换成浏览器的可执行代码,如此依赖,我们便成功了一半,来看代码

//使用babel 的core模块的transformFromAst方法,给抽象语法树转换成我们浏览器可执行的代码
const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
});

他转换后的效果图如下

是不是很像我们平常使用webpack打包之后的代码了,至于中间的这些传参,在开始时我已经介绍过了,这样一来我们简单的打包其实就已经可以使用了,但是,模块间依赖的代码应该怎么处理呢?

2、解析依赖代码,完成整个项目打包

我们在编写上方的webpck的方法时,我们发现他除了解析入口的代码,其实各个依赖的代码也能用同样的套路解析出来,并且存放在一个地方,于是我们就得给他变成一个通用方法,并且在加入一个函数去在这个函数中递归调用解析方法,解析出依赖文件,存入数组中保存,这样我们就能拿到所有的转换后的文件了。好废话少说,开干

const DependenceMap=(entry)=>{
    //首先这个方法中去解析入口文件的语法
    const entryModule = webpack(entry);
    //将解析后的对象存入数组中
    const graphArray = [ entryModule ];
    //遍历数组,递归解析当前数组中的依赖关系
    //注意:数组长度不是固定的为graphArray.length
	for(let i = 0; i < graphArray.length; i++) {
        //拿到数组中的每一项
        const item = graphArray[i];
        //拿到依赖当前解析对象中的dependencies就是依赖的每一项
		const { dependencies } = item;
		if(dependencies) {
            //for in 去遍历对象
			for(let j in dependencies) {
                //再次解析当前文件的依赖文件,然后压入数组
                //注意这块就是数组长度graphArray.length的妙用
                //可以完全的去解析出来所有的文件
				graphArray.push(
					webpack(dependencies[j])
				);
			}
		}
	}
}

我们单独定义一个方法,去解析所有的依赖关系并且存入数组中,其中使用循环次数为数组的长度的妙用,来解析出来整个依赖图谱,如下图我们发现,所有的依赖关系全部在这一个数组中了

上图中我们发现,这跟我们webpack打包后的传入的依赖代码不一样啊,他好像是个对象,并不是一个数组,接下来我们来转化一下,废话少说,上代码:

 //创建一个存转化后的代码的空对象
    const graph = {};
    //遍历数组
	graphArray.forEach(item => {
        //将每一项转换成对象的形式
		graph[item.filename] = {
			dependencies: item.dependencies,
			code: item.code
		}
    });
    console.log(graph)

如上图,这样就和我们webpack中的形式一样了

3、打包生成合并依赖图谱,合并成浏览器可运行的代码

在上面两个步骤中,我们我们通过两个方法,拿到了最终左右的解析后的代码,我们在来一个方法,去初期最终生成的代码,直接上代码

const generateCode=(entry)=>{
    //由于我们要返回一段代码段,所以必须用字符串的方式去返回
    const graph = JSON.stringify(DependenceMap(entry));
    //需要避免全局污染,必须用闭包的形式,去处理
    //我们在看解析完成之后的代码段发现,他有require的语法,于是我们在导出的时候需要自己模拟一个类似的方法,防止报错
    return `
    
    (function(graph){
        //浏览器模拟require方法
        function require(module) { 
            //由于转换后的代码中执行require的时候,他是根据相对路径去执行的
            //但是我们的依赖对象中的key值是一个绝对路径
            //于是我们需要去写一个转换方法
            function localRequire(relativePath) {
                return require(graph[module].dependencies[relativePath]);
            }
            //由于是模拟require方法,我们还需要一个exports导出对象
            var exports = {};
            //在加入一个闭包,防止印象外部已经定义的变量
            (function(require, exports, code){
              //执行代码
                eval(code)
            })(localRequire, exports, graph[module].code);
            return exports;
        };
        //执行require语法
        require('${entry}')
    })(${graph});
    `
}

上边代码中,我们发现,我们通过一个自定义的require语法就能实现,整个依赖图谱的代码执行,并且不会污染全局环境,我们来看一下导出的结果

上图的代码中我们是不是就发现和webpack导出的代码非常像啊,接下来我们给我们调用fs的写入文件方法,给代码写入js文件中即可,我们便不再赘述。

最后

首先附上完成代码

//引入node模块
const fs = require('fs');
const path = require('path');
//引入babel模块
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
//创建方法
const webpack=(filename)=>{
//拿到js入口文件中的内容
const content=fs.readFileSync(filename,'utf-8')
//打印内容
console.log(content)
//使用parser的parse解析成ast语法树
const ast = parser.parse(content, {
    sourceType: 'module'
});
//打印抽象语法树
// console.log(ast.program.body)
//创建存放解析完依赖关系的对象
const dependencies = {};
//使用traverse梳理依赖关系并且解析到对象中
traverse(ast, {
    //对象参数中,由于需要找到依赖关系放入对象,所以只需要ImportDeclaration类型的回调即可
    ImportDeclaration({ node }) {
        //使用node的path模块,取出当前的文件的路径目录
        const dirname = path.dirname(filename); 
        //拼接处相工程文件根目录下的路径
        const newFile = './' + path.join(dirname, node.source.value);
        //拿到路径存入对象中
        dependencies[node.source.value] = newFile;
    }
});
//使用babel 的core模块的transformFromAst方法,给抽象语法树转换成我们浏览器可执行的代码
const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
});
//导出用到的信息
return {
    filename,
    dependencies,
    code
}
}
const DependenceMap=(entry)=>{
    //首先这个方法中去解析入口文件的语法
    const entryModule = webpack(entry);
    //将解析后的对象存入数组中
    const graphArray = [ entryModule ];
    //遍历数组,递归解析当前数组中的依赖关系
    //注意:数组长度不是固定的为graphArray.length
	for(let i = 0; i < graphArray.length; i++) {
        //拿到数组中的每一项
        const item = graphArray[i];
        //拿到依赖当前解析对象中的dependencies就是依赖的每一项
		const { dependencies } = item;
		if(dependencies) {
            //for in 去遍历对象
			for(let j in dependencies) {
                //再次解析当前文件的依赖文件,然后压入数组
                //注意这块就是数组长度graphArray.length的妙用
                //可以完全的去解析出来所有的文件
				graphArray.push(
					webpack(dependencies[j])
				);
			}
		}
    }
    //console.log(graphArray)
    //创建一个存转化后的代码的空对象
    const graph = {};
    //遍历数组
	graphArray.forEach(item => {
        //将每一项转换成对象的形式
		graph[item.filename] = {
			dependencies: item.dependencies,
			code: item.code
		}
    });
    // console.log(graph)
	 return graph;
}
const generateCode=(entry)=>{
    //由于我们要返回一段代码段,所以必须用字符串的方式去返回
    const graph = JSON.stringify(DependenceMap(entry));
    //需要避免全局污染,必须用闭包的形式,去处理
    //我们在看解析完成之后的代码段发现,他有require的语法,于是我们在导出的时候需要自己模拟一个类似的方法,防止报错
    return `
    
    (function(graph){
        //浏览器模拟require方法
        function require(module) { 
            //由于转换后的代码中执行require的时候,他是根据相对路径去执行的
            //但是我们的依赖对象中的key值是一个绝对路径
            //于是我们需要去写一个转换方法
            function localRequire(relativePath) {
                return require(graph[module].dependencies[relativePath]);
            }
            //由于是模拟require方法,我们还需要一个exports导出对象
            var exports = {};
            //在加入一个闭包,防止印象外部已经定义的变量
            (function(require, exports, code){
              //执行代码
                eval(code)
            })(localRequire, exports, graph[module].code);
            return exports;
        };
        //执行require语法
        require('${entry}')
    })(${graph});
    `
}

const code=generateCode('./src/index.js')
 console.log(code)

当我们完整的看完了一个es模块的打包流程之后,相信大家已经了然于胸,反正我研究完了之后解决了之前的很多困惑,而且当我们掌握了完整的流程之后,对webpack的原理基本也掌握了7、8成了,其实webpack就是在中间我们转换代码的过程中多加了一点lorder,和plugins,从而实现了强大的功能。这样如果想去大厂的你,是不是心中又多了一点信心!

结束,再次感谢巨人dell lee,站在巨人的肩膀上真好!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 为什么 webpack4 默认支持 ES6 语法的压缩?

    在使用 webpack 的时候,很常见的一个构建优化手段就是缩小构建目标。比如在构建阶段只构建 src 里面的模块代码,对于 node_modules 里面所引...

    腾讯IVWEB团队
  • 如何写一手漂亮的 Vue

    前几日听到一句生猛与激励并存,可怕与尴尬同在,最无奈也无解的话:“90后,你的中年危机已经杀到”。这令我很受触动。显然,这有些夸张了,但就目前这日复一日的庸碌下...

    晚晴幽草轩轩主
  • Webpack 原理—如何实现代码打包

    ? 这是第 122 篇不掺水的原创,想要了解更多,请戳上方蓝色字体:政采云前端团队 关注我们吧~

    政采云前端团队
  • 【Webpack】241-Webpack 是怎样运行的?

    在平时开发中我们经常会用到Webpack这个时下最流行的前端打包工具。它打包开发代码,输出能在各种浏览器运行的代码,提升了开发至发布过程的效率。

    pingan8787
  • 从Webpack源码探究打包流程,萌新也能看懂~

    上一篇讲述了如何理解tapable这个钩子机制,因为这个是webpack程序的灵魂。虽然钩子机制很灵活,而然却变成了我们读懂webpack道路上的阻碍。每当we...

    小美娜娜
  • 跨年都在更新的 vite 到底有多香?

    2020年太难了,终于等到元旦能放假休息几天,闲着没事逛微博,然后,收到了来自米国的礼物:Vite2.0;

    西岭老湿
  • 前端-Webpack 之 treeShaking

    在 github 上直接观看 markdown 会把图片转存到缓存中,github 转存后的图片清晰度很有问题,因此如果图片看不清,可以移步知乎上的相同文章

    grain先森
  • webpack4.0各个击破(4)—— Javascript & splitChunk

    javascript之所以需要打包合并,是因为模块化开发的存在。开发阶段我们需要将js文件分开写在很多零碎的文件中,方便调试和修改,但如果就这样上线,那首页的h...

    大史不说话
  • Webpack 打包优化之体积篇

    谈及如今欣欣向荣的前端圈,不仅有各类框架百花齐放,如Vue, React, Angular等等,就打包工具而言,发展也是如火如荼,百家争鸣;从早期的王者Brow...

    晚晴幽草轩轩主
  • 【万字长文】如何阅读源码 —— 以 Vetur 为例

    我很早就意识到,能熟练、高效阅读开源前端框架源码是成为一个高级前端工程师必须具备的基本技能之一,所以在我职业生涯的最早期,就已经开始做了很多次相关的尝试,但结果...

    童欧巴
  • webpack 拍了拍你,给了你一份图解指南(模块化部分)

    在前面一篇文章中《模块化系列》彻底理清 AMD,CommonJS,CDM,UMD,ES6 我们可以学到了各种模块化的机制。那么接下里我们就来分析一下 webpa...

    秋风的笔记
  • 【Cute-Webpack】Webpack4 入门手册(共 18 章)

    最近和部门老大,一起在研究团队【EFT - 前端新手村】的建设,目的在于:帮助新人快速了解和融入公司团队,帮助零基础新人学习和入门前端开发并且达到公司业务开发水...

    pingan8787
  • webpack打包原理入门探究(五)css-loader初探

    在 src/app.js 引入 src/styles/css/common.css

    公众号---人生代码
  • 【Webpack】319- Webpack4 入门手册(共 18 章)(上)

    最近和部门老大,一起在研究团队【EFT - 前端新手村】的建设,目的在于:帮助新人快速了解和融入公司团队,帮助零基础新人学习和入门前端开发并且达到公司业务开发水...

    pingan8787
  • B乎问题:通俗的解释下Vite能用来干嘛?是怎么回事?

    最近在B乎看到了这么一个问题,能不能通俗地讲 Vite 到底是用来干嘛的,一开始觉得这个问题没什么意思,因为 Vite 这个话题有太多的人讲了。

    秋风的笔记
  • 介绍一下TreeShaking及其工作原理

    面试官:是因为最近面了好多同学,大家都说熟悉webpack,在项目中如何去使用、如何去优化,也都或多或少会提到tree shaking,但是每当我深入去问其工作...

    JowayYoung
  • 借助Babel 7和Webpack构建React Toolchain

    React不是完全开箱即用的。它使用了一些最近node才支持的关键字和语法(在本教程中我使用了v 9.3.0版本)。因此需要一些很麻烦的设置,但是Faceboo...

    ArrayZoneYour
  • Webpack4 教程:入口、输入和ES6模块(第一章)

    你好!今天我们会开始一个 Webpack 4的入门教程。我们会以Webpack的基本概念开始,随着教程逐渐深入。这一次,我们将学习用ES6 modules进行模...

    Javanx
  • 了解可执行的NPM包

    NPM是Node.js的包管理工具,随着Node.js的出现,以及前端开发开始使用gulp、webpack、rollup以及其他各种优秀的编译打包工具(大多数采...

    贾顺名

扫码关注云+社区

领取腾讯云代金券