Skip to content

100 行代码实现 webpack #6

@zhuping

Description

@zhuping

100 行代码实现 webpack

前段时间在油管上看到一个 BUILD YOUR OWN WEBPACK 的演讲,作者以简单易懂的方式,现场一步步实现了一个 100 行代码左右的webpack,顿时让我对研究 webpack 多了一丢丢的兴趣。webpack 功能虽然很强大,但那复杂的配置,也是让一波猿望而却步。今天我们就来看下,这 100 行代码是如何实现一个简易的 webpack 的。

在正式分析前,我们先来看几个 babel 的包:

@babel/parser 的作用是把一段代码转换成 AST,比如下面这段代码:

import message from '/message.js';

console.log(message);

经过转换变成:

你可以通过 astexplorer 自己试下。

@babel/traverse 的作用是可以增删改查 parse 生成的语法树。

@babel/core 的作用就是代码的转换。

好了,了解过这三个包的大致用法后,我们来看下下面这个函数,

let ID = 0;
function createAsset(filename) {
  const content = fs.readFileSync(filename, 'utf-8');

  const ast = babylon.parse(content, {
    sourceType: 'module'
  });

  let dependencies = [];

  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value);
    }
  });

  const id = ID++;

  const { code } = babel.transformFromAstSync(ast, null, {
    presets: ['@babel/preset-env']
  });

  return {
    id,
    filename,
    dependencies,
    code
  };
}

createAsset 方法很简单,根据传入的文件路径,获取到文件内容,然后通过 @babel/parser 包生成对应的 AST;接着使用 traverse 方法来获取该文件 import 了哪些文件,并且把这些依赖存储在 dependencies 数组中;然后用 transformFromAstSync 方法把 AST 转换成 es5 的代码。最终返回一个对象,包括文件 ID,文件路径,依赖及内容代码。(下文中提到的对象数据,都是指返回的这个对象)

那么对于那些被依赖的文件,如何来遍历查询他们的依赖关系呢?

function createGraph(entry) {
  const mainAsset = createAsset(entry);

  const queue = [mainAsset];

  for (const asset of queue) {
    const dirname = path.dirname(asset.filename);

    asset.mapping = {};

    asset.dependencies.forEach(relativePath => {
      const absolutePath = path.join(dirname, relativePath);

      const child = createAsset(absolutePath);
      
      asset.mapping[relativePath] = child.id;
      
      queue.push(child);
      
    });
  }

  return queue;
}

首页根据入口文件拿到它所对应的对象数据,既然要做广度遍历,那先放入一个数组当中,然后逐层开始遍历。首先获取 entry 文件所在目录,然后依赖文件路径,获取依赖文件的绝对路径,再次调用 createAsset 方法返回依赖文件所包含的对象数据,同时标记文件 ID,然后 push 到数组中,等待下次的循环遍历。最后的 queue 队列,则包含了所有文件的依赖关系。

那么当我们拿到完整的依赖关系后,怎么让代码能在浏览器中跑起来呢,我们知道经过 babel 转换后的代码是 CommonJS 规范的,如果不转换格式,是不能在浏览器端执行的。

function bundle(graph) {
  let modules = '';

  graph.forEach(mod => {
    modules += `${mod.id}:[
      function(module, exports, require) {
        ${mod.code}
      },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });

  const results = `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];
        function localRequire(relativePath) {
          return require(mapping[relativePath]);
        }
        const module = { exports: {} };

        fn(module, module.exports, localRequire);
        return module.exports;
      }
      require(0);
    })({${modules}})
  `;
  return results;
}

要想跑起来,我们得先提供 moduleexportsrequire 三个变量。如果结合经过 babel 转换后的代码来看,可能会更好的理解上面这段代码:

// name.js
export const nickname = 'jax';

转换后变成:

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.nickname = void 0;
var nickname = 'jax';
exports.nickname = nickname;
// message.js
import { nickname } from './name.js';

export default `hello ${nickname}!`;

转换后变成:

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _name = require("./name.js");

var _default = "hello ".concat(_name.nickname, "!");

exports.default = _default;

思考题

在分析模块之间依赖关系的时候,我们的实现有什么问题吗?如果存在一个模块被多个模块引用,或者存在两个模块互相引用又会出现上面问题呢?

答案在 minipack 的 PR 中可以找到。

参考

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions