实现一个Bundler

226 阅读1分钟

这是我参与更文挑战的第8天,活动详情查看:更文挑战

前言

本文我们来实现一个非常简单的类似webpack这样的bundler,实现这个bundler我们可以更加清楚的了解类似于webpack这种打包工具它的底层原理。

项目结构

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

上述代码最终是输出一个say hello字符串,只不过里面它涉及到了几个模块之间的相互调用。现在我们运行上述代码肯定是不能在浏览器上运行的,因为浏览器根本就不认识import这种语法,所以我们需要通过一个webpack或者类似的打包工具,帮我们去做项目打包,接下来我们对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');
    // 转换成ast
    const ast = parser.parse(content, {
        sourceType: 'module'
    });
    const dependencies = {};
    // 遍历ast 如果ast中包含了ImportDeclaration字段就执行
    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 makeDependenciesGraph = (entry) => {
    // 接收模块分析的结果
    const entryModule = moduleAnalyser(entry);
    // 将结果用数组包裹,方便往里写入数据,此时graphArray的长度是1
    const graphArray = [ entryModule ];
    // 因为graphArray目前的长度是1,所以只会遍历一次
    for(let i = 0; i < graphArray.length; i++) {
        const item = graphArray[i];
        // 获取该文件依赖
        const { dependencies } = item;
        // 如果该文件有引入依赖 就进入
        if(dependencies) {
            for(let j in dependencies) {
                graphArray.push(
                    // 将依赖的入口传入模块分析,再次进行解析
                    // push进去后,graphArray的长度+1就可以继续进行遍历
                    // 达到一种类似递归的效果 一层层找每个文件里的依赖进行解析
                    moduleAnalyser(dependencies[j])
                );
            }
        }
    }

    const graph = {};
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    });
    // graph结果如图4
    return graph;
}

const generateCode = (entry) => {
    const graph = JSON.stringify(makeDependenciesGraph(entry));
    return `
        // 接收图4的结果
        (function(graph){
            // 执行require函数
            function require(module) { 
                function localRequire(relativePath) {
                    return require(graph[module].dependencies[relativePath]);
                }
                var exports = {};
                // 进入此立即函数 第一个参数localRequire在 eval(code)里被调用
                (function(require, exports, code){
                    eval(code)
                })(localRequire, exports, graph[module].code);
                return exports;
            };
            require('${entry}')
        })(${graph});
    `;
}

const code = generateCode('./src/index.js');
// 返回的结果,放入浏览器执行的结果,看图5
console.log(code);

moduleAnalyser就是分析模块,传入入口文件进行分析,fs获取入口文件的内容,接下来我们打印content

image.png

  • @babel/parser将内容转换成AST,如果我们的代码是用es module编写的sourceType就要填写module;
  • @babel/traverse用来遍历AST,遍历AST往dependencies里写入模块依赖;

image.png

  • @babel/core配合@babel/preset-env将es6代码转换成es5代码;

image.png

图4

image.png

图5

image.png 结果能打印出 say hello

总结

以上是个人的一些理解,具体实现形式还请翻阅其他文档进行学习。