前言
我们都知道webpack是当前最流行的一款工程化工具,他具有编译代码,压缩代码,处理模块化等等功能,其原理就是将代码转换成运行环境能够识别的代码,那么我们如何实现一个简单的webpack呢?
思路
我们都知道webpack其主要是通过一个entry来进行生成依赖图,通过依赖图生成我们所需要的代码,然后通过output输出到指定文件夹。所以我们大致实现思路是:
- 根据
entry生成依赖图 - 根据依赖图生成运行环境(当前是浏览器)能够识别的代码
- 根据
output将代码生成到指定文件夹下的指定文件中
实现
实例
我们当前需要将以下文件进行打包处理,生成一个js文件。
// test.js
import b from './test2'
console.log('这是第一个例子' + b);
// test2.js
import a from './test1'
const b = '你好,'+ a;
export default b;
// test1.js
const a = '二锅头';
export default a;
生成依赖图
按照上面的例子,我们需要知道具体的某个文件所对应的引用的文件,那么我们就需要有一个办法可以获取到其引用的文件。
获取代码引用的文件名
我们可以通过@babel/traverse中的ImportDeclaration方法获取我们所需要的数据,traverse是一个遍历更新@babel/parser生成的AST的工具,我们可以通过其提供的方法来获取所需要的节点数据,详情可以看@babel/traverse,具体代码实现如下:
const traverse = require('@babel/traverse').default
getDeps(ast) {
let deps = [];
traverse(ast,{
ImportDeclaration({node}) {
deps.push(node.source.value);
}
})
return deps;
}
这样我们就获取了单个文件的依赖了。我们上面说了traverse是遍历@babel/parser生成的AST工具,所以我们要获取依赖,首先就必须先将文件转换成AST格式。
将文件转换成AST格式
我们只需要通过使用@babel/parser中的parse方法就能将文件转换成AST格式的文件了。详细用法可以看@babel/parser因此我们需要将文件转换成二进制的文件,然后传入其方法中。为了能够将文件转换成二进制,我们需要通过fs.readFileSync将文件读取转换成二进制文件。具体代码实现如下:
const parser = require('@babel/parser');
const fs = require('fs');
parse2Ast(fileName) {
return parser.parse(fs.readFileSync(fileName,'utf-8'),{
sourceType:'module'
})
}
至此我们所需要的基础的方法写好了。那么我们就需要去生成所有的依赖图了
获取所有的依赖
我们可以在上面获取具体某一文件所引用的文件,那么我们就可以通过定义一个数组,数组每一项都保存当前文件名以及所引用的文件,具体格式为:
interface Deps {
fileName:string;
dependence:Array<string>;
}
那么我们就得到了以下的代码:
getDepsMap(fileInfo) {
let queue = [fileInfo];
for(const item of queue) {
item.dependence.forEach(deps=>{
queue.push(this.parse(deps));
})
}
return queue;
}
parse(fileName,isEntry) {
const filePath = fileName.indexOf('.js') >=0?fileName:`${fileName}.js`;
const dirName = isEntry?'':path.dirname(this.options.entry);
const absolutePath = path.join(dirName,filePath);
const ast = this.parse2Ast(absolutePath);
return {
fileName,
dependence:this.getDeps(ast)
}
}
这样我们就得到了一个完整的依赖图,是一个数组格式的数据。层层依赖。
生成浏览器可识别的js文件
上面我们已经获取到了整个依赖,最终我们需要将上述的所有的依赖文件全部合并成一个文件,那么我们就需要做到可以将文件中的代码拿出来合并,因此我们就需要将AST格式的文件转换成浏览器可识别的js源码文件
将AST格式文件转换成源码
此时我们需要借助@babel/code,其具有一个方法那就是transformFromAst.该方法是将AST格式文件转换成一个源码。具体方法如下:
const {transofrmFromAst} = require('@babel/core');
parseAst2Code(ast) {
const {code} = transformFromAst(ast,null,{
presets:['@babel/preset-env']
})
return code;
}
因此我们可以在上述的生成依赖图的parse方法中增加一个数据,那就是最终的源码
parse(fileName,isEntry) {
const filePath = fileName.indexOf('.js') >=0?fileName:`${fileName}.js`;
const dirName = isEntry?'':path.dirname(this.options.entry);
const absolutePath = path.join(dirName,filePath);
const ast = this.parse2Ast(absolutePath);
return {
fileName,
dependence:this.getDeps(ast),
code: this.parseAst2Code(ast)
}
}
合并成一个文件
我们上面已经得到了一个依赖图,包含所有的依赖数据以及源码,那么我们只需要循环依赖,然后将源码合并即可,具体方法如下:
build(depsMap) {
let modules = '';
depsMap.forEach(queueItem=>{
modules += `'${queueItem.fileName}': function(require,module,exports){${queueItem.code}}`
})
return `(function(modules)) {
function require(filenName) {
const fn = modules[fileName];
const module = {export:{}};
fn(require,module,module.exports);
return module.exports;
}
require(`${this.options.entry}`)
}({${modules}})`
}
至此我们整个的编译过程完成,然后我们就需要根据output生成文件夹以及生成文件
生成文件
根据上面的方法我们可以得到一个js源码但是如何生成一个文件呢,这就需要借助fs.writeFileSync来生成一个文件,同时我们需要生成一个文件夹,需要借助fs.mkdirSync方法来生成。具体方法如下:
doCompileAndReplace(content) {
const { path, filename } = this.options.output;
let truePath = path ? path : 'dist';
let trueFileName = filename ? filename : 'bundle.js';
fs.mkdirSync(truePath);
fs.writeFileSync(`${truePath}/${trueFileName}`, content);
}
当然这样处理还有问题,那就是当第二次编译的时候就会提示文件夹已存在,因此我们需要做一个判断处理,那就是如果文件夹存在,删除文件夹以及下属所有文件,因此上述方法可以替换为:
doCompileAndReplace(content) {
const { path, filename } = this.options.output;
let truePath = path ? path : 'dist';
let trueFileName = filename ? filename : 'bundle.js';
if (fs.existsSync(truePath)) {
for (const dir of fs.readdirSync(truePath)) {
this.deleteFiles(dir, truePath);
}
fs.writeFileSync(`${truePath}/${trueFileName}`, content);
} else {
fs.mkdirSync(truePath);
fs.writeFileSync(`${truePath}/${trueFileName}`, content);
}
}
deleteFiles(dir, truePath) {
const targetDir = path.resolve(truePath, dir);
if (path.extname(targetDir) === "") {
fs.readdirSync(targetDir).forEach((file) => {
deleteFiles(path.join(targetDir, file), truePath);
});
} else {
fs.unlinkSync(targetDir);
}
}
至此,整个编译过程完成完成。详细代码可异步到个人仓库中看(还未上传,后续补),也可以通过以下命令安装npm包
npm install -g lulisimplewebpack
然后在所需要的地方执行simpleWebpack
附上完整代码,后续删除,放git上
/**
* 第一步:从options中的entry来build文件
* 第二步:在build文件中通过递归获取文件所有的依赖,生成依赖图
* 第三步:在生成依赖图的过程中需要将依赖的文件读取出来后生成源码
* 第四步:将文件读取出来生成源码需要将文件先转换成AST格式,然后再转换成源码
* 第五步:根据options.output配置生成浏览器可识别的js文件,然后输出到指定的位置
*
* @class SimpleWebpack
*/
const parser = require('@babel/parser');
const fs = require('fs');
const path = require('path');
const { transformFromAst } = require('@babel/core');
const traverse = require('@babel/traverse').default;
class SimpleWebpack {
constructor(options) {
this.options = options;
}
/**
* 根据文件的绝对路径,生成AST文件
* @param {string} fileName
* @return {*} AST格式数据
* @memberof SimpleWebpack
*/
parse2Ast(fileName) {
const content = fs.readFileSync(fileName, 'utf-8');
return parser.parse(content, {
sourceType: 'module'
})
}
/**
* 根据ast格式数据,生成源码
* @param {*} ast
* @return {*}
* @memberof SimpleWebpack
*/
parseAst2Code(ast) {
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
return code;
}
/**
* 获取文件中的依赖,使用traverse中的ImportDeclaration来处理
* @param {*} ast
* @return {*}
* @memberof SimpleWebpack
*/
getDeps(ast) {
const deps = [];
traverse(ast, {
ImportDeclaration({ node }) {
deps.push(node.source.value);
}
})
return deps;
}
getDepsMap(fileInfo) {
let queue = [fileInfo];
for (const item of queue) {
item.dependence.forEach(deps => {
queue.push(this.parse(deps));
});
}
return queue;
}
/**
* 根据文件名来生成文件的依赖关系映射以及文件的code信息
* @param {*} fileName
* @param {*} isEntry
* @return {*}
* @memberof SimpleWebpack
*/
parse(fileName, isEntry) {
const filePath = fileName.indexOf('.js') >= 0 ? fileName : `${fileName}.js`;
const dirName = isEntry ? '' : path.dirname(this.options.entry);
const absolutePath = path.join(dirName, filePath);
const ast = this.parse2Ast(absolutePath);
return {
fileName,
dependence: this.getDeps(ast),
code: this.parseAst2Code(ast)
}
}
/**
* 根据
*
* @param {*} depsMap
* @return {*}
* @memberof SimpleWebpack
*/
bundle(depsMap) {
let modules = '';
depsMap.forEach(queueItem => {
modules += `'${queueItem.fileName}': function (require, module, exports) { ${queueItem.code} },`
})
return `(function(modules) {
function require(fileName) {
const fn = modules[fileName];
const module = { exports : {} };
fn(require, module, module.exports);
return module.exports;
}
require('${this.options.entry}');
})({${modules}})`
}
buildFile() {
// 获取入口文件的依赖
let entryFile = this.parse(this.options.entry, true);
// 获取依赖图
let depsMap = this.getDepsMap(entryFile);
// 生成浏览器可识别的js文件
let content = this.bundle(depsMap);
// 生成文件夹以及写入文件
this.doCompileAndReplace(content);
}
/**
* 根据output配置生成dist包,且生成相应的文件
* @param {*} content
* @memberof SimpleWebpack
*/
doCompileAndReplace(content) {
const { path, filename } = this.options.output;
let truePath = path ? path : 'dist';
let trueFileName = filename ? filename : 'bundle.js';
if (fs.existsSync(truePath)) {
for (const dir of fs.readdirSync(truePath)) {
this.deleteFiles(dir, truePath);
}
fs.writeFileSync(`${truePath}/${trueFileName}`, content);
} else {
fs.mkdirSync(truePath);
fs.writeFileSync(`${truePath}/${trueFileName}`, content);
}
}
/**
* 删除文件夹中的文件
*
* @param {*} dir
* @param {*} truePath
* @memberof SimpleWebpack
*/
deleteFiles(dir, truePath) {
const targetDir = path.resolve(truePath, dir);
if (path.extname(targetDir) === "") {
fs.readdirSync(targetDir).forEach((file) => {
deleteFiles(path.join(targetDir, file), truePath);
});
} else {
fs.unlinkSync(targetDir);
}
}
}
module.exports = SimpleWebpack;