实践-模块打包器(mini webpack)

152 阅读1分钟

github 地址

实现了一个 mini 的模块打包器,支持 ES Module 语法。

网上也有一些多少行代码实现 mini webpack 的例子,本文也有参考,但这个例子有两个优点:一是使用基于根目录的绝对路径作为模块文件的唯一索引,确保每个文件只加载一次;二是使用模块缓存,并能处理循环依赖

代码如下,不过100行(但是引入了babel这些库😁):

// pack.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');

function createAsset(filename) {
  const content = fs.readFileSync(filename, 'utf-8');
  const ast = parser.parse(content, {
    sourceType: 'module',
  });

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

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

  return {
    filename,
    dependencies,
    code,
  };
}

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

  const assetMap = {
    [entry]: entryAsset,
  };
  const queue = [entryAsset]; // queue 用来递归遍历所有模块及模块的依赖

  for (const asset of queue) {
    asset.mapping = {};

    const dirname = path.dirname(asset.filename);

    asset.dependencies.forEach((sourcePath) => {
      // sourcePath是该模块文件的相对位置,而filePath是根路径的相对位置
      // 只有filePath才能作为一个模块的唯一索引,存在assetMap中
      const filePath = './' + path.join(dirname, sourcePath);
      asset.mapping[sourcePath] = filePath;

      // 如果此模块未被处理
      if (!assetMap[filePath]) {
        const child = createAsset(filePath);
        queue.push(child);
        assetMap[filePath] = child;
      }
    });
  }

  return assetMap;
}

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

  for (const key in graph) {
    const mod = graph[key];
    modules += `"${mod.filename}": [
        function (require, module, exports) { ${mod.code} },
        ${JSON.stringify(mod.mapping)},
      ],`;
  }

  const result = `
    (function (modules) {
        const module_cache = {};
        function require(id) {
          const [fn, mapping] = modules[id];
      
          function localRequire(name) {
            return require(mapping[name]);
          }
      
          const cachedModule = module_cache[id];
          if (cachedModule !== undefined) {
            return cachedModule.exports;
          }
      
          const module = (module_cache[id] = {
            exports: {},
          });
      
          fn(localRequire, module, module.exports);
      
          return module.exports;
        }
      
        require('${entry}');
    })({${modules}})
  `;

  return result;
}

const entry = './example/entry.js'; // 设置入口文件
const graph = createGraph(entry);
const result = bundle(graph, entry);

fs.writeFileSync('output/index.js', result); // 输出打包结果

使用它也很简单,可以先写一个简单的使用 ES 模块语法的例子,如:

// entry.js
import { name } from './name.js';

console.log(`hello ${name}!`);

// name.js
export const name = 'knockkk';

然后在 pack.js 中设置入口文件路径即可。打包结果如下:

(function (modules) {
  const module_cache = {};
  function require(id) {
    const [fn, mapping] = modules[id];

    function localRequire(name) {
      return require(mapping[name]);
    }

    const cachedModule = module_cache[id];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }

    const module = (module_cache[id] = {
      exports: {},
    });

    fn(localRequire, module, module.exports);

    return module.exports;
  }

  require('./example/entry.js');
})({
  './example/entry.js': [
    function (require, module, exports) {
      'use strict';

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

      console.log('hello '.concat(_name.name, '!'));
    },
    { './name.js': './example/name.js' },
  ],
  './example/name.js': [
    function (require, module, exports) {
      'use strict';

      Object.defineProperty(exports, '__esModule', {
        value: true,
      });
      exports.name = void 0;
      var name = 'knockkk';
      exports.name = name;
    },
    {},
  ],
});

运行后打印:hello knockkk!

nice~👍

github 地址