webpack 之 Bundler 编写

159 阅读1分钟

「本文正在参与技术专题征文Node.js进阶之路,点击查看详情

为了更好的理解webpack 编译原理, 来写一个 类似webpackbundler打包工具。

话不多说,直接开始 ⬇️⬇️⬇️

1. 建立文件结构

  • bundler.js
  • src/index.js
    import message from './message.js';

    console.log(message);
  • src/message.js
        import { word } from './word.js';
        const message = `say ${word}`;
    
        export default message;
    
  • src/word.js
    export const word = 'hello';
    

2. 对文件进行分析

类webpack:

1、使用nodeJS读取入口文件;

2、@babel/parser逐个获取入口文件中的依赖文件,解析代码生成AST语法树

babel会解析出某个文件的AST,根据AST中对每行代码的type字段来判定该节点是否为依赖;

3、@babel/traverse遍历AST找到type为ImportDeclaretion的节点里的文件名,生成依赖关系树

  1. es6转换为es5,安装@babel/core@babel/preset-env

3. 分析单文件 moduleAnalyser

// bundler.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core'); //

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);
            // 相对路径转为真实的路径
            const newFile = './' + path.join(dirname, node.source.value);
            dependencies[node.source.value] = newFile;
        }
    });
    const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    });
    
    return {
        filename,
        dependencies,
        code
    }
}

const moduleInfo = moduleAnalyser('./src/index.js');
console.log(moduleInfo);

4 依赖图谱 分析( Dependencies Graph )

依赖图谱: 循环入口文件的分析结果,获取该入口文件的依赖文件,继续循环依赖文件的键值对。

// bundler.js

// ... 
// const moduleAnalyser = (filename) => { …… }

// 依赖图谱分析
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
				graphArray.push(
					moduleAnalyser(dependencies[j])
				);
			}
		}
	}
	const graph = {}; // 绝对路径对应:依赖和code
	graphArray.forEach(item => {
		graph[item.filename] = {
			dependencies: item.dependencies,
			code: item.code
		}
	});
	return graph;
}

const graghInfo = makeDependenciesGraph('./src/index.js');
console.log(graghInfo);

graph 打印内容 如下:

image.png

5. Bundler 源码编写( 生成代码

构建require、export

网页中执行的代码放在闭包中,避免污染全局环境

// bundler.js
// ……

// 单个文件时解析
// ... const moduleAnalyser = (filename) => {  ……  }

// 依赖图谱分析
// const makeDependenciesGraph = (entry) => {  ……  }

// 生成代码
const generateCode = (entry) => {
	const graph = JSON.stringify(makeDependenciesGraph(entry));
        // 构建require、export
	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);

6. 运行结果

image.png