手写webpack简化版源码

1、webpack配置

let path = require("path");

//定义的插件
class A {
    //apply是固定的格式
    apply(compiler) {
        //挂上每个钩子等待触发
        compiler.hooks.parse.tap("xxx", function () {
            console.log("parse")
        });
    }
}

class B {
    apply(compiler) {
        compiler.hooks.emitFile.tap("xxx",function () {
            console.log("emitFile")
        })
    }
}

module.exports = {
    mode:"development",
    entry:"./src/index.js",
    output:{
        filename:"bundle.js",
        path: "./dist"
    },
    module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                    path.resolve(__dirname, "loaders", "style-loader.js"),
                    path.resolve(__dirname, "loaders", "less-loader.js")
                ]
            }
        ]
    },
    plugins: [
        new A,
        new B
    ]
};

2、源码编写

let path = require("path");
let fs = require("fs");
let babylon = require("babylon");
let traverse = require("@babel/traverse").default;
let generator = require("@babel/generator").default;
let {SyncHook} = require("tapable");


class Compiler {
    constructor(config) {
        //获取根目录 运行命令的位置
        this.root = process.cwd();
        //获取用户配置
        this.config = config;
        //入口文件
        this.entry = config.entry;
        //入口
        this.entryId = "";
        //所有依赖关系
        this.modules = {};

        this.hooks = {
            entryOption:new SyncHook(["compiler"]), //接收到入口选项的时候
            emitFile:new SyncHook(["compiler"]), //发射文件的时候
            parse:new SyncHook(["compiler"]) //解析文件的时候
        };

        //实行所有插件
        config.plugins.forEach(instance=>{
            instance.apply(this);
        })
    }

    //解析函数
    parser(source,parentDir){ 
        // 解析source  src 解析成为抽象语法树
        let ast = babylon.parse(source);
        //保存依赖
        let dependencies = [];
        //遍历语法树
        traverse(ast,{ // visitor
            //修改require和文件路径
            CallExpression(p){
                // 匹配所有的调用表达式
                let node = p.node;
                if(node.callee.name === 'require'){
                    //如果遇到require,将它转为__webpack_require__
                    node.callee.name = '__webpack_require__';
                    // 增加一个后缀名 .js
                    node.arguments[0].value = path.extname(node.arguments[0].value)?node.arguments[0].value:node.arguments[0].value+'.js';
                    // ./src/util/b.js  修改文件的路径
                    node.arguments[0].value = './'+path.join(parentDir,node.arguments[0].value);
                    //将每个依赖文件保存起来
                    dependencies.push(node.arguments[0].value);
                }
            }
        });
        //生成树
        let r = generator(ast);
        //当解析文件完成的时候,我就出发parser钩子函数
        this.hooks.parse.call(this);
        //返回代码和依赖
        return {r:r.code,dependencies}

    }

    创建依赖函数
    buildModule(modulePath,isMain){
        // /usr/local/work
        // /usr/local/work/src/index.js 获取到相对路径
        let relativePath = './'+path.relative(this.root,modulePath); // src/index
        //获取父路径
        let parentDir = path.dirname(relativePath); // src
        //获取文件内容
        let source = this.getSource(modulePath);

        // ast 抽象语法树 esprima traverse         generator
        // @babel/core  babylon @babel/traverse  @babel/generator
        if(isMain){
            this.entryId = relativePath; // 我们当前的主模块 标记为主模块入口
        }
        //解析内容
        let {r,dependencies} = this.parser(source,parentDir);

        //添加依赖图谱 {key:value}
        this.modules[relativePath] = r;
        //遍历依赖数组,对每个依赖一一解析
        dependencies.forEach(dep=>{ // ./src/util/b.js
            this.buildModule(path.join(this.root,dep));
        });
    }

    //获取文件内容函数
    getSource(modulePath) {
        //根据路径读取文件中的内容
        let source = fs.readFileSync(modulePath, "utf8");

        //根据loader,对内容进行比配
        let rules = this.config.module.rules;
        //遍历loader,对比配的文件进行内容转换
        for (let i = 0; i < rules.length; i++) {
            let {test: reg, use} = rules[i];
            //通过文件后缀名进行比配test的规则 找到需要替换的文件
            if(reg.test(modulePath)) {
                //取出最后一个loader
                let len = rules.length;
                function normalLoader() {
                    let loader = use[len--];
                    if(loader){
                        //引入对应的loader
                        let loaderCurrent = require(loader);
                        //转换完成第一loader,进行下一个loader
                        source = loaderCurrent(source);
                        normalLoader();
                    }
                }

                normalLoader();
            }
        }
        return source;
    }

    //发射文件函数
    emitFile() {
        //引入模板
        let ejs = require("ejs");
        //获取模板内容
        let templateStr = this.getSource(path.resolve(__dirname, "./ejs.ejs"));
        //替换内容
        let str = ejs.render(templateStr,{
            entryId:this.entryId,
            modules:this.modules
        });

        let {filename,path:p} = this.config.output;
        //可能内容有很多,可以将文件名和对应的内容放入对象,然后遍历处理
        this.assets = {
            [filename]: str
        };

        Object.keys(this.assets).forEach(key => {
            //将内容写入文件
            fs.writeFileSync(path.join(p,key),this.assets[key]);
        });

        //当 我发射文件的时候,我就触发emiFile钩子函数
        this.hooks.emitFile.call(this);

    }

    run() {
        //创建模块 {key,value}
        this.buildModule(path.join(this.root,this.entry), true);
        //发射文件
        this.emitFile();
    }
}

module.exports = Compiler;

3、ejs.ejs模板

(function(modules) {
    var installedModules = {};
  
    function __webpack_require__(moduleId) {
      if (installedModules[moduleId]) {
        return installedModules[moduleId].exports;
      }
      var module = (installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
      });
  
      modules[moduleId].call(
        module.exports,
        module,
        module.exports,
        __webpack_require__
      );
  
      module.l = true;
  
      return module.exports;
    }
    return __webpack_require__((__webpack_require__.s = "<%-entryId%>"));
  })({
    <%for(let key in modules){%>
      "<%-key%>": function(module, exports, __webpack_require__) {
        eval(
          `<%-modules[key]%>`
        );
      },
    <%}%>
  });
  // 需要把所有用到的模块依赖 做成一个对象 key就是当前的文件名
  // require ->__webpack_require__
  // 需要改造引用的路径
  // 确定入口文件 加载内容
  
  // 创建依赖图谱 把所有的依赖做成列表
  // 把模板 和 我们解析出来的列表 进行渲染 打包到目标文件中

4、bin文件的配置

#! /usr/bin/env node

// commander - yarns
//获取文件路径
let path = require("path");
let configPath = path.resolve("webpack.config.js");
let config = require(configPath);

let Compiler = require("../src/Compiler");

//创建Compiler实例
let compiler = new Compiler(config);

//开始打包
compiler.run();

这是webpack源码的地址:https://github.com/weikehang/webpack4.0-learn.git

发布了80 篇原创文章 · 获赞 5 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_28473733/article/details/95488795