手写webpack——低配版webpack

本文中,“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”

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容