美文网首页
模块打包器的实现(一)

模块打包器的实现(一)

作者: 一拾五 | 来源:发表于2020-06-07 11:40 被阅读0次

    什么是打包器

    一个完整的 JavaScript 项目(比如各种前端SPA)由各种各样的资源模块(module)组成,包括 JavaScript 代码,CSS 样式以及图片等各种文件。打包器(module bundler)可以分析入口文件(entry)引用了哪些模块,找到对应的文件,将其合并到一起。这样执行输出文件(output)的时候,一个完整的项目会呈现出来。

    单页面应用包含大量 JavaScript 代码,为了合理地管理代码,开发时会将代码拆分到不同文件里面。在各个模块的代码编写完成之后,bundler 可以帮我们把各个分散的 JS 文件合并起来,输出一个完整的 JS 文件。

    对于样式文件和图片等资源,我们也可以指定如何处理它们。一般的处理方式是直接插入到 HTML 或者 JS 文件中,或者通过指定文件网络地址(public path),在需要该文件的时候浏览器会通过网络请求获取到这些资源。

    功能完备的打包器可以把各种资源模块聚集到一起,生成完整的 web app。但本文作为开篇只讨论如何实现一个 JavaSript bundler。

    过程分析

    一个最简单的 JS bundler 可以帮助我们:

    • 找到 entry JavaScript 文件引用到的所有其他 JS 文件,并将其合并到目标 JS 文件(output)中。
    • 保证各个模块的 JavaScript 代码都在自己的作用域中执行,避免命名冲突。

    为了保证每个模块的 JS 代码都在自己的作用域中执行,可以参考 Node 执行 JS 代码的方式。可以概括为 5 步:

    • resolve:通过 require 中的 string 定位到文件的真实地址。
    • load:加载这个文件。
    • wrap:将引入的代码包含在一个函数中,保证定义的变量只作用在本文件中。
    • execute:执行代码。
    • cache:缓存执行结果。

    而打包的流程可以概括为:

    • 找到起始文件的依赖文件,将其加载并描述为一个资源模块(asset),其包含的信息包括:
      • id:唯一 id
      • filename:绝对文件路径
      • code:模块代码,将其包含在一个函数里面。并且需要把 ESM 的 import 和 export 改成 require 和 exports,这样可以执行函数参数里面的 require 和 exports,函数参数的 require 可以帮我们通过相对路径找到实际文件。
      • dependencies:引入的模块。
      • mapping:记录以来模块的相对路径和其模块 id 的对应关系。
    • 当依赖的文件有其他依赖的时候,继续加载依赖文件。最终生成一个依赖图(dependency graph),包含所有模块之间的依赖关系。
    • 拼凑一个完整 string,包含所有模块信息,并且执行起始文件。输出这个 string。

    代码实现

    转换 JS 文件为资源模块(asset):

    const path = require('path');
    const fs = require('fs');
    
    const parser = require('babel-parser');
    const { transformFromAst } = require('@babel/core');
    const traverse = require('@babel/traverse').default;
    
    let ID = 0;
    
    function createAsset(filename) {
      const content = fs.readFileSync(filename, 'utf-8');
      const ast = parser.parse(content, {
        sourceType: 'module',
      });
      
      const dependencies = [];
      traverse(ast, {
        importDelcaration: ({ node }) => {
          dependencies.push(node.source.value);
        }
      });
      
      const { code } = tranformFromAst(ast, null, {
        presets: ['@babel/preset-env'],
      });
      
      return {
        id: ID++,
        filename,
        dependencies,
        code
      };
    }
    

    生成依赖图:

    function createGraph(entry) {
      const mainAsset = createAsset(entry);
      
      const queue = [mainAsset];
      
      queue.forEach(asset => {
        asset.mapping = {};
        const dirname = path.dirname(asset.filename);
        
        asset.dependencies.forEach((relativePath) => {
          const filename = path.join(dirname, relativePath);
          const child = createAsset(filename);
         
          asset.mapping[relativePath] = child.id;
          queue.push(child);
        });
      });
      
      return queue;
    }
    

    合并 string:

    function createBundle(entry) {
      const graph = createGraph(entry);
      
      let modules = '{';
      
      graph.forEach((asset) => {
        modules +=
          `${asset.id}: [function (requre, module, exports) { ${asset.code} }, ${JSON.stringify(asset.mapping)}],`;
      });
      
      modules += '}';
      
      const result = `function(modules) {
        function require(id) {
          const [fn, mapping] = modules[id];
          
          function localRequire(relativePath) {
            require(mapping[relativePath]));
          }
          
          const module = { exports: {} };
          
          fn(localRequire, module, exports);
          
          return moduele.exports;
        }
        
        require(0);
      }(${modules)`;
    }
    

    备注项目依赖

    "dependencies": {
        "@babel/core": "7.9.6",
        "@babel/parser": "7.9.6",
        "@babel/preset-env": "7.9.6",
        "@babel/traverse": "7.9.6"
      }
    

    附录

    相关文章

      网友评论

          本文标题:模块打包器的实现(一)

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