Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

编写一个类似webpack的bundler #13

Open
2 tasks
mbaxszy7 opened this issue Jun 21, 2020 · 0 comments
Open
2 tasks

编写一个类似webpack的bundler #13

mbaxszy7 opened this issue Jun 21, 2020 · 0 comments
Labels

Comments

@mbaxszy7
Copy link
Owner

mbaxszy7 commented Jun 21, 2020

看了webpack打包出来后的代码,觉得很精妙,想尝试写一个极其简易版的js bundler
项目地址:https://github.com/mbaxszy7/make-bundler
运行: node ./bundler.js

项目的模块

入口模块: index.js
index模块依赖的模块: hello.js ,console.js
hello模块依赖的模块:world.js

bundler 实现的要点

  1. 需要有一个模块的分析器,能分析模块的其他模块依赖,产生依赖图谱
  2. 转化import,类似webpack中打包后的_webpack_require_
  3. 产出bundle.js作为打包结果,可以在浏览器上直接运行

模块分析器的实现

模块分析器主要是来转换import语句,分析模块的import chanining和收集这些import依赖。从源码层面来分析代码结构就需要用到抽象语法树ast, 这里使用了@babel/parser。然后需要遍历分析ast的节点,来收集import依赖,我们需要使用@babel/traverse这个库。收集到的import依赖的文件路径是相对于entry 文件的路径,需要处理一下变为相对于根目录的文件路径。最后,需要把ast转换为实际的代码。具体代码如下:

  // 相对于entry 文件的路径 -> 相对于根目录的路径
const makeSrcPath = (fileName, moduleSrc) => {
  const dirName = path.join(path.dirname(fileName), moduleSrc);
  return `./${dirName}`;
};

const moduleAnalysis = (fileName) => {
  const content = fs.readFileSync(fileName, "utf-8");
  const ast = parser.parse(content, {
    // parse in strict mode and allow module declarations
    sourceType: "module",
  });
  const dependencies = {};
  traverse(ast, {
    ImportDeclaration({ node }) {
      const moduleSrc = node.source.value;
      const path = makeSrcPath(fileName, moduleSrc);
      dependencies[moduleSrc] = path;
    },
  });
  const { code } = babel.transformFromAst(ast, null);
  return {
    fileName,
    dependencies,
    code,
  };
};

产出的dependencies结构如下:

// {
//   相对于entry 文件的路径:  相对于根目录的路径
// }

dependencies =  {
  './hello.js': './src/hello.js',
  './console.js': './src/console.js'
}

这样的结构便于我们下一步遍历每个模块的dependencies,产出整个dependencies。

产出dependencies

为了收集全部模块的dependencies,需要遍历每个模块的import依赖。分析这个项目的模块依赖,我们可以把它简化为一个多叉树。那么遍历依赖就变成了广度优先的遍历方式,其中为了简单处理循环引用,在dependencies result中已经存在的模块就不再次遍历。代码如下:

 const generateDependenciesGraph = (entry) => {
  const entryModule = moduleAnalysis(entry);
  console.log(entryModule);
  // 构造队列,处理广度优先遍历
  const queue = [entryModule];
  const ret = {
    [entry]: {
      dependencies: entryModule.dependencies,
      code: entryModule.code,
    },
  };

  while (queue.length) {
    const item = queue.shift();

    const { dependencies } = item;
    if (dependencies) {
      for (const [k, v] of Object.entries(dependencies)) {
       // 如果不在dependencies result中
        if (!ret[v]) {
          const res = moduleAnalysis(v);
          // 把依赖推入queue
          queue.push(res);
          const { dependencies, code } = res;
          ret[v] = {
            dependencies,
            code,
          };
        }
      }
    }
  }
  return ret;
};

最后返回的ret:

{
  "./src/index.js": {
    dependencies: {
      "./hello.js": "./src/hello.js",
      "./console.js": "./src/console.js",
    },
    code:
      '"use strict";\n\nvar _hello = _interopRequireDefault(require("./hello.js"));\n\nvar _console = require("./console.js");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\ndocument.getElementById("root").innerText = _hello.default;\n(0, _console.log)(_hello.default);',
  },
  "./src/hello.js": {
    dependencies: { "./world.js": "./src/world.js" },
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.default = void 0;\n\nvar _world = _interopRequireDefault(require("./world.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconst hello = `Hello. ${_world.default}`;\nvar _default = hello;\nexports.default = _default;',
  },
  "./src/console.js": {
    dependencies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.log = void 0;\n\nconst log = (...props) => console.log(...props);\n\nexports.log = log;',
  },
  "./src/world.js": {
    dependencies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.default = void 0;\nconst world = "A brave new world";\nvar _default = world;\nexports.default = _default;',
  },
}

生成可运行的代码

在上一阶段产出的dependencies中每个dependency的code就是对应模块的源码(index.js 为例):

var _hello = _interopRequireDefault(require("./hello.js"));
var _console = require("./console.js");
function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj };
}
document.getElementById("root").innerText = _hello.default;
(0, _console.log)(_hello.default);

但是这段代码在浏览器上运行不来,因为我们没有实现代码里的require函数,下面就来分析实现这段代码里的require函数。

代码里用require函数是这么运行的:require("./console.js"),所以要用./console.js得到./src/hello.js, 也就是我们之前构造好的
dependencies。然后为了传入require函数运行模块代码和防止模块变量污染外部的变量,我们需要把代码放在一个闭包中运行。最后,生成可运行的代码的函数返回的也是一个立即执行函数,接受的modules参数就是上一步的ret:

const generateCode = (entry) => {
  const modules = JSON.stringify(generateDependenciesGraph(entry));
  return `
    (
      function(modules) {
        function _bundler_require_(module) {
          function _relative_require_(relativePath) {
            return _bundler_require_(modules[module].dependencies[relativePath])
          }

          var exports = {};
          (
            function(require, exports, code) {
              eval(code)
            }
          )(_relative_require_, exports, modules[module].code)

          return exports
        }

        _bundler_require_('${entry}')
      }
    )(${modules})
  `;
};

这个立即执行函数参考的就是webpack打包出来的那个立即执行函数。

输出dist目录

代码如下:

 fs.writeFile("./dist/bundle.js", generateCode("./src/index.js"), (error) => {
  console.error(error);
});

后续ToDo

  • 实现动态import
  • 实现babel的polyfill
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant