前期准备
在webpack中,我们发现配置我们能天然的使用esmodule这种模块化语法,那大家有没有好奇过呢?他究竟是怎么实现的呢?下面一起来探究一下,webpack究竟是怎么解析打包esmodule语法的。
在研究之前,我们需要有一定的node的基础知识,应为我们如果想要实现webpack类似的功能,那么,我们必须要借助node的一些模块,比如path模块、比如fs模块,等,这些都是node的基础模块 接下来,我们还需要babel的一些模块,给我们做一些转化比如babel/parser模块、比如**@babel/traverse模块**、在比如babel/core模块等等,接下来,我们分别介绍一下用到的这些模块
模块介绍
path
NodeJS中的Path对象,用于处理目录的对象,提高开发效率
我们在配置webpack的时候也经常用到,他的常见用法就是我们的目录转换比如:
代码语言:javascript复制//引入进来
const path = require('path');
//拼接这些链接
console.log(path.join('/Users','node/path','../','join.js'));
fs
fs模块可以对文件进行一些读写操作
我们在webpack 中由于要转义语法,所以对文件的读写必不可少,使用方式也非常简单
代码语言:javascript复制//引入模块
const fs = require('fs');
//读取文件,readFileSync指的是同步读取文件,filename指的是文件路径,第二个参数指的是格式
const content = fs.readFileSync(filename, 'utf-8');
babel/parser
babel/parser是babel的一个模块,它能帮我们分析代码,并且转换长AST也就是抽象语法树
使用方式也非常简单
代码语言:javascript复制//引入进来
const parser = require('@babel/parser');
//解析成抽象语法树 第一个参数表示我们的代码,第二个参数是一系列配置sourceType 表示是哪种语法
const ast = parser.parse(content, {
sourceType: 'module'
});
babel/traverse
babel/traverse能根据抽象语法树中的信息解析出代码中的依赖关系,从而可以解析出整个esmodule的代码
使用方式也非常简单
代码语言:javascript复制//引入模块
const traverse = require('@babel/traverse').default;
//第一个参数接受抽象语法树,
//第二个参数是个对象,配置的是我们需要找出的依赖关系的配置
traverse(ast, {
ImportDeclaration({ node }) {
}
});
babel/core
babel的核心模块,可以给我我们的代码转成浏览器的可以识别的代码
使用方式也不是那么难
代码语言:javascript复制//引入模块
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语法
代码语言:javascript复制//引入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这个表达式了
接下来我们就要去拿到依赖关系了,应该怎么处理呢?
代码语言:javascript复制
//使用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的一个核心模块,给抽象语法树转换成浏览器的可执行代码,如此依赖,我们便成功了一半,来看代码
代码语言:javascript复制//使用babel 的core模块的transformFromAst方法,给抽象语法树转换成我们浏览器可执行的代码
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
});
他转换后的效果图如下
是不是很像我们平常使用webpack打包之后的代码了,至于中间的这些传参,在开始时我已经介绍过了,这样一来我们简单的打包其实就已经可以使用了,但是,模块间依赖的代码应该怎么处理呢?
2、解析依赖代码,完成整个项目打包
我们在编写上方的webpck的方法时,我们发现他除了解析入口的代码,其实各个依赖的代码也能用同样的套路解析出来,并且存放在一个地方,于是我们就得给他变成一个通用方法,并且在加入一个函数去在这个函数中递归调用解析方法,解析出依赖文件,存入数组中保存,这样我们就能拿到所有的转换后的文件了。好废话少说,开干
代码语言:javascript复制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打包后的传入的依赖代码不一样啊,他好像是个对象,并不是一个数组,接下来我们来转化一下,废话少说,上代码:
代码语言:javascript复制 //创建一个存转化后的代码的空对象
const graph = {};
//遍历数组
graphArray.forEach(item => {
//将每一项转换成对象的形式
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
});
console.log(graph)
如上图,这样就和我们webpack中的形式一样了
3、打包生成合并依赖图谱,合并成浏览器可运行的代码
在上面两个步骤中,我们我们通过两个方法,拿到了最终左右的解析后的代码,我们在来一个方法,去初期最终生成的代码,直接上代码
代码语言:javascript复制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文件中即可,我们便不再赘述。
最后
首先附上完成代码
代码语言:javascript复制//引入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,站在巨人的肩膀上真好!