AnnVoV / blog

24 stars 2 forks source link

写一个minipack #17

Open AnnVoV opened 6 years ago

AnnVoV commented 6 years ago

背景

无意中看到了一个视频BUILD YOUR OWN WEBPACK by Ronen Amiel,讲得非常好!看完后总结如下:

思路

我们要实现一个自己的简易打包工具,大致的思路为: 1.找到根入口文件 2.根据根入口文件找到其依赖文件,以及依赖文件中的依赖文件形成一张图 3.根据图将文件打包到一起 4.输出结果,正常执行

一.根据入口文件,查找依赖

// entry.js
import message from './message.js';
console.log(message);

入口文件,如上所示,我们如何根据我们的这份文件,查找出我们的入口文件的依赖关系呢?

引入babylon 得到AST

很明显我们需要借助AST语法树去完成这个过程(同babel插件实现原理),借助AST语法树,我们就可以获取整个代码的树形结构及其组织关系。借助ast 在线编辑器我们可以看到下面这张图。

1 所以我们需要引入babel的babylon这个包, 利用node的fs读取入口文件后,利用这个包来帮助我们获取到读到的代码的AST

const fs = require('fs');
const babylon = require('babylon'); // 生成ast

const createAsset = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8'); // 读取文件内容
  const ast = babylon.parse(content, {
    sourceType: "module",
  }); // 得到代码的ast
}
createAsset('./example/entry.js');

引入babel-traverse 遍历ast

如果你写过babel插件,或者了解babel插件底层原理,你应该知道光有ast是不行的,我们还需要遍历ast,并在遍历ast的过程中,获取我们要的节点信息,做一些处理。所以我们还需要借助babel-traverse这个包。 通过利用babel-traverse这个包,在遍历ast的过程中获取import的钩子,我们就可以拿到import对应的节点,即就找到了文件的依赖文件名

const fs = require('fs');
const babylon = require('babylon'); // 生成ast
const traverse = require('babel-traverse').default; // 遍历ast, 提取我们要的信息

const createAsset = (filename) => {
    const dependencies = [];
    const content = fs.readFileSync(filename, 'utf-8');
    const ast = babylon.parse(content, {
        sourceType: "module",
    }); // 在线ast 生成器 https://astexplorer.net/
    traverse(ast, {
        ImportDeclaration({node}) {
            const relativePath = node.source.value; // 查找到依赖的相对路径
            dependencies.push(relativePath); // 依赖
        },
    });
};
createAsset('./example/entry.js');

通过这两步,我们就获取到了入口entry文件的dependencies文件数组

二.生成依赖图

入口的依赖找到了,同理,我们还需要获取依赖自己的依赖文件。到这里你可能会发现其实我们需要给每一个模块一个通过的对象去描述,这个对象包含:

然后我们就需要去实现我们的createGraph方法,通过根文件,去寻找所有的依赖文件

const path = require('path');

const createAsset = (filename) => {
  ///...
});
const createGraph = (entry) => {
    const mainAsset = createAsset(entry); // 主入口模块
    const queue = [mainAsset]; // 队列目前只有一个入口文件
    for(const asset of queue) {
      asset.dependencies.forEach((dep) => {
          const relativePath = dep; // 相对路径
          const absolutePath = path.join(dirname, relativePath); // 绝对路径
          const child = createAsset(absolutePath); // 注意我们得通过绝对路径找到模块
          queue.push(child); // 注意这里queue是在动态增加的,为依赖文件生成模块后会继续查找该模块的依赖
      })
    }
    console.log(queue);
    return queue;
}

createGraph('./example/entry.js');

此时我们就得到了从入口文件开始的所有依赖,形成了下面这张图,通过这张图我们就可以获得所有我们需要的文件

2

三.打包所有文件,输出一个文件

我们需要明确我们的终极目标,我们的终极目标是将多个文件最终打包成一个文件,并且这个文件能正常的在浏览器中运行。这一步,我们需要实现一个bundle方法来完成这个过程。 我们目前遇到的问题有:

OK,那我们大致就知道我们的思路了

const modules = createGraph('./example/entry.js');
const bundle = (modules) => {
  const result = `(function(modules) {
    // 入口文件是modules[0]
    // 我们需要获取modules[0]的代码内容并且执行才可以,所以我们发现,
    // 我们还需要在模块上添加code属性来存放这个模块的代码内容,
    // 因此我们需要修改下我们的createAsset方法
  })(`${modules}`)`
}

修改createAsset方法,引入babel-core将es6转换为es5,把转换后的code 放在code属性上

const fs = require('fs');
const babylone = require('babylon'); // 生成ast
const traverse = require('babel-traverse').default; // 遍历ast, 提取我们要的信息
const path = require('path');
const babel = require('babel-core');
let ID = 0;

const createAsset = (filename) => {
    const dependencies = [];
    const content = fs.readFileSync(filename, 'utf-8');
    const id = ID++;
    const ast = babylone.parse(content, {
        sourceType: "module",
    }); // 在线ast 生成器 https://astexplorer.net/
    traverse(ast, {
        ImportDeclaration({node}) {
            dependencies.push(node.source.value); // 存放依赖的相对路径
        },
    });
    const {code} = babel.transformFromAst(ast, null, {
        presets: ['env'],
    });
    return {
        id,
        filename,
        dependencies,
        code,
    }; //  每一个模块都由用这个对象去描述,包括id唯一标识, 文件名,和依赖数组
};

此时的graph 再打印出来看看 3

继续,我们的bundle实现

const graph = createGraph('./example/entry.js');
const bundle = (graph) => {
  const result = `(function(modules) {
      function require(id) {
          const moduleFn = modules[id].code;
          // 我们发现后面的思路很清晰了,获取到code, 执行code,
          // 提供require, module, exports的实现,让code可执行
          // 但是我们会发现,我们缺少code中出现的相对路径与我们的依赖文件的映射,所以我们需要修改我们的createGraph 代码, 补上这段映射
          // moduleFn(require, module, module.exports)
      }
      require(0);
  })(`${graph}`)`
}

为createGraph 方法添加mapping 相关代码

const createGraph = (entry) => {
    const mainAsset = createAsset(entry); 
    const queue = [mainAsset];
    for(const asset of queue) {
      // 这里为asset 添加一个mapping 属性,用于查找相对路径在modules中对应的模块
      asset.mapping = {};
      asset.dependencies.forEach((dep) => {
          const relativePath = dep; 
          const absolutePath = path.join(dirname, relativePath); 
          const child = createAsset(absolutePath); 
          asset.mapping[relativePath] = child.id; // relative path 与 id映射成功
          queue.push(child);
      })
    }
    return queue;
}

继续写我们的bundle

const bundle = (graph) => {
    let modules = '';
    graph.forEach(module => {
        modules += `
            ${module.id}: [
                function(require, module, exports) {
                    ${module.code}
                },
                ${JSON.stringify(module.mapping)}
            ],
        `;
    });
    /**
     * 入口函数
     * function(require, module, exports) {
            "use strict";

            var _message = require("./message.js");

            var _message2 = _interopRequireDefault(_message);

            function _interopRequireDefault(obj) {
                return obj && obj.__esModule ? obj : {
                    default: obj
                };
            }

            console.log(_message2.default);
        }
     * @type {string}
     */
    const result = `
        (function(modules) {
            // 需要开始执行入口函数
            function require(id) {
                const [fn, mapping] = modules[id]; // 得到function 和依赖 
                function localRequire(relativePath) {
                    return require(mapping[relativePath]);
                }
                const module = {
                    exports: {}
                };
                fn(localRequire, module, module.exports);
                return module.exports;
            }
            require(0);
        })({
            ${modules}
        })`;
    return result;
};

const graph = createGraph('./example/entry.js');
const result = bundle(graph);
console.log(result);

把输出的result 粘贴到浏览器中,我们就看到正确的运行结果了,这样一个minipack也就实现了

核心视频: 1.BUILD YOUR OWN WEBPACK by Ronen Amiel https://www.youtube.com/watch?v=Gc9-7PBqOC8 2.Babylon和babel-traverse详解 https://github.com/xtx1130/blog/issues/7 3.深入理解 ES6 模块机制 https://zhuanlan.zhihu.com/p/33843378 4.浏览器加载 CommonJS 模块的原理与实现 http://www.ruanyifeng.com/blog/2015/05/commonjs-in-browser.html

shangraochq commented 6 years ago

优秀6666