美文网首页webpack
手写webpack——低配版webpack

手写webpack——低配版webpack

作者: 成熟稳重的李先生 | 来源:发表于2019-07-14 19:57 被阅读0次

    本文中,“webpackSimple”指项目,而“localWebpack”或者“demo-start'”均指手写的webpack工具
    上节,我们再本地开发了一个极简npm包,这节我们顺道来完善它

    先新建一个webpack项目,并打包
    image.png
    目录结构很简单,文件详情如下:
    //index.js
    let str = require("./a.js");
    console.log(str);
    //a.js
    let b = require("./base/b.js");
    module.exports = "a" + b;
    //b.js
    module.exports = "b";
    //webpack.config.js
    let path = require("path");
    module.exports = {
      mode: "development",
      entry: "./src/index.js",
      output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "dist")
      }
    };
    //打包后的bundle.js(我删掉了一些多余的部分,为了更清晰的看出webpack具体做了什么)
    (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 = "./src/index.js"));
    })({  // 将引用的文件以键值对的形式引入,创建依赖关系
      "./src/a.js": function(module, exports, __webpack_require__) {
        eval(
          'let b = __webpack_require__(/*! ./base/b.js */ "./src/base/b.js");\r\nmodule.exports = "a" + b;\r\n\n\n//# sourceURL=webpack:///./src/a.js?'
        );
      },
    
      "./src/base/b.js": function(module, exports) {
        eval(
          'module.exports = "b";\r\n\n\n//# sourceURL=webpack:///./src/base/b.js?'
        );
      },
    
      "./src/index.js": function(module, exports, __webpack_require__) {
        eval(
          'let str = __webpack_require__(/*! ./a.js */ "./src/a.js");\r\n\r\nconsole.log(str);\r\n\n\n//# sourceURL=webpack:///./src/index.js?'
        );
      }
    });
    

    以上,我们可以看出,webpack内部将引用的文件以键值对的形式创建了依赖关系。并且在它内部实现了一个require方法“webpack_require”来达到文件的引用,使用“eval”来运行文件内容
    下边,我们开始自己写,修改上节的极简npm包,目录结构如下:

    image.png
    #!/usr/bin/env node
    // 1) 需要找到当前执行名的路径,拿到webpack.config.js
    let path = require("path");
    
    // config配置文件
    
    let config = require(path.resolve("webpack.config.js"));  // 首先拿到用户的webpack配置
    
    let Compiler = require("../lib/Compiler.js"); 
    
    let compiler = new Compiler(config); // 编译配置
    // 标识运行编译
    compiler.run();
    
    
    //Compiler.js
    let path = require("path");
    let fs = require("fs");
    class Compiler {
      constructor(config) {
        //entry,output。。。也就是webpack.config.js
        this.config = config;
        // 需要保存入口文件的路径
        this.entryId; // './src/index.js',这个留到后边补充
        // 需要保存所有的模块依赖
        this.modules = {};
        this.entry = config.entry; // 入口路径
        // 工作路径(运行命令的路径)
        this.root = process.cwd();
      }
      getSource(modulePath) {
        let content = fs.readFileSync(modulePath, "utf8");
        return content;
      }
      // 构建模块
      buildModules(modulePath, isEntry) {
        let source = this.getSource(modulePath); //“绝对路径”, 编码utf-8
        // 模块ID(查看webpack打包后的文件,文件key值为相对路径,而我们此处拿到的是绝对路径,因此需要减去这些前置路径)
        // modulePath = modulePath - this.root
        let moduleName = "./" + path.relative(this.root, modulePath); //这样,就解析出了一个相对路径
        console.log(source, moduleName);  // 这里,我们要打印出文件的内容和路径
      }
      emitFile() {
        // 发射文件
      }
      run() {
        // 执行
        // 创建模块的依赖关系
        this.buildModules(path.resolve(this.root, this.entry), true); // 入口路径, 是否主模块
        // 发射一个文件 -> 打包后的文件
        this.emitFile();
      }
    }
    
    module.exports = Compiler;
    

    在webpackSimple项目下运行‘npx demo-start’

    image.png
    然后修改buildModules函数
      // 构建模块
      buildModules(modulePath, isEntry) {
        let source = this.getSource(modulePath); //“绝对路径”, 编码utf-8, 这里,拿到主入口的内容
    
        // 模块ID(查看webpack打包后的文件,文件key值为相对路径,而我们此处拿到的是绝对路径,因此需要减去这些前置路径)
        // modulePath = modulePath - this.root
        let moduleName = "./" + path.relative(this.root, modulePath); //这样,就解析出了一个相对路径
    
        if (isEntry) {
          this.entryId = moduleName; // 保存入口的名字
        }
        // 解析,需要把source源码进行改造,返回一个依赖列表
        // 还进行一些别的工作,比如将"./a.js"解析为"./src/a.js"
        // 这些工作都由parse方法来完成,此处的parse函数接下来详细解释
        let { sourceCode, dependencies } = this.parse(
          source,
          path.dirname(moduleName)
        );
        // 把相对路径和模块中的内容对应起来({文件名:解析后的文件内容}
        this.modules[moduleName] = sourceCode;
      }
    

    接着我们要来完善parse函数,这里就要引入一个新的概念了 ---- AST语法树
    首先需要引入三个包

    //babylon  主要是把源码转化为AST
    //@babel/traverse  (遍历到对应节点)通过AST生成一个便于操作、转换的path对象,供我们的babel插件处理
    //@babel/types  替换节点
    //@babel/generator  读取AST并将其转换为代码和源码映射。
    
    // 解析源码
      parse(source, parentPath) {
        // AST解析语法树
        let ast = babylon.parse(source); //转换成ast
        let dependencies = []; //依赖的数组
        traverse(ast, {
          // 遍历ast
          CallExpression(p) {
            // 调用表达式, 即functionName()这种形式的
            // 以下内容,详见  https://astexplorer.net/
            let node = p.node; //对应的节点
            if (node.callee.name === "require") {
              node.callee.name = "__webpack_require__";
              let moduleName = node.arguments[0].value; // 这里就取到了模块引用的名字
              moduleName = moduleName + (path.extname(moduleName) ? "" : ".js"); //  加文件后缀 -> ./a.js
              moduleName = "./" + path.join(parentPath, moduleName); //改造路径 -> './src/a.js'
              dependencies.push(moduleName);
              node.arguments = [t.stringLiteral(moduleName)]; //重写节点的arguments -> 改写源码
            }
          }
        });
        let sourceCode = generator(ast).code;
        return {   // 解析后的源码和依赖return出去
          sourceCode,
          dependencies
        };
      }
    
    // 构建模块
      buildModules(modulePath, isEntry) {
        let source = this.getSource(modulePath); //“绝对路径”, 编码utf-8, 这里,拿到主入口的内容
    
        // 模块ID(查看webpack打包后的文件,文件key值为相对路径,而我们此处拿到的是绝对路径,因此需要减去这些前置路径)
        // modulePath = modulePath - this.root
        let moduleName = "./" + path.relative(this.root, modulePath); //这样,就解析出了一个相对路径
    
        if (isEntry) {
          this.entryId = moduleName; // 保存入口的名字
        }
        // 解析,需要把source源码进行改造,返回一个依赖列表
        // 还进行一些别的工作,比如将"./a.js"解析为"./src/a.js", 解析依赖关系
        let { sourceCode, dependencies } = this.parse(
          source,
          path.dirname(moduleName)
        );
        // 把相对路径和模块中的内容对应起来({文件名:解析后的文件内容})
        this.modules[moduleName] = sourceCode;
    
        dependencies.forEach(dep => {  //循环各个依赖
          // 副模块的加载
          this.buildModules(path.join(this.root, dep), false);
        });
      }
    

    *在run方法中加一行 *

    run() {
        // 执行
        // 创建模块的依赖关系
        this.buildModules(path.resolve(this.root, this.entry), true); // 入口路径, 是否主模块
        console.log(this.modules, this.entryId);
        // 发射一个文件 -> 打包后的文件
        this.emitFile();
      }
    

    运行后:

    image.png
    可以看到,将modules整理为 key: value的形式,并且也将entryId赋为index.js的路径+文件名。
    接下来,就用这个对象渲染输出了。
    上边我们把webpack生成的bundle.js简化了,为了减少工作量,我们将它复制出来,并且新建一个模板文件main.ejs(你需要先安装ejs并且引入)
    //main.ejs
    (function(modules) {
      var installedModules = {};
      function __webpack_require__(moduleId) {
        if (installedModules[moduleId]) {
          return installedModules[moduleId].exports;
        }
        var module = (installedModules[moduleId] = {
          i: moduleId,
          l: false,
          orts: {}
        });
        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){%>      // 这里遍历modules,生成key: value形式的文件映射
      "<%-key%>":
      (function(module, exports, __webpack_require__) {
      eval(`<%-modules[key]%>`);
      }),
      <%}%>
    });
    

    最后一步,在Compiler.js中渲染

    emitFile() {
        // 发射文件(输出)
        //用数据渲染ejs
        //拿到配置的出口
        let main = path.join(this.config.output.path, this.config.output.filename);
        let templateStr = this.getSource(path.join(__dirname, "main.ejs"));
        let code = ejs.render(templateStr, {  // 用entryId和modules渲染ejs模板
          entryId: this.entryId,
          modules: this.modules
        });
        this.assets = {}; // 因为输出的文件可能不止一个js
        // 资源中路径对应的代码
        this.assets[main] = code;
        fs.writeFileSync(main, this.assets[main]);
      }
    

    大功告成,现在,这个‘demo-start’就有了简单的webpack的功能。我们可以在webpackSimple项目下试用:


    demo-start.gif

    正确输出了“ab”

    相关文章

      网友评论

        本文标题:手写webpack——低配版webpack

        本文链接:https://www.haomeiwen.com/subject/lpbbkctx.html