手写mini版webpack

77 阅读5分钟

1、 npm init -y 创建package.json文件

2、创建创建minipack.js

3、创建测试文件夹和文件

4、下载依赖npm i babylon babel-traverse babel-core babel-preset-env -D

5、启动项目:node minipack.js

babel-preset-env:这种现象是由于在 .babelrc 文件中设置了env 选项,需要插件 babel-preset-env 处理

image.png mini版webpack未涉及loader和plugin等复杂功能,是一个非常简单的例子。

mini版的webpack打包流程
const fs = require('fs');
const path = require('path');
// babylon解析js语法,生产AST 语法树
// ast将js代码转化为一种JSON数据结构
const babylon = require('babylon');
// babel-traverse是一个对ast进行遍历的工具, 对ast进行替换
const traverse = require('babel-traverse').default;
// 将es6 es7 等高级的语法转化为es5的语法
const { transformFromAst } = require('babel-core');

// 每一个js文件,对应一个id
let ID = 0;

// filename参数为文件路径, 读取内容并提取它的依赖关系
function createAsset(filename) {
    const content = fs.readFileSync(filename, 'utf-8');

    // 获取该文件对应的ast 抽象语法树
    const ast = babylon.parse(content, {
        sourceType: 'module'
    });

    // dependencies保存所依赖的模块的相对路径
    const dependencies = [];

    // 通过查找import节点,找到该文件的依赖关系
    // 因为项目中我们都是通过 import 引入其他文件的,找到了import节点,就找到这个文件引用了哪些文件
    traverse(ast, {
        ImportDeclaration: ({ node }) => {
            // 查找import节点
            dependencies.push(node.source.value);
        }
    });

    // 通过递增计数器,为此模块分配唯一标识符, 用于缓存已解析过的文件
    const id = ID++;
    // 该`presets`选项是一组规则,告诉`babel`如何传输我们的代码.
    // 用`babel-preset-env`将代码转换为浏览器可以运行的东西.
    const { code } = transformFromAst(ast, null, {
        presets: ['env']
    });

    // 返回此模块的相关信息
    return {
        id, // 文件id(唯一)
        filename, // 文件路径
        dependencies, // 文件的依赖关系
        code // 文件的代码
    };
}

// 我们将提取它的每一个依赖文件的依赖关系,循环下去:找到对应这个项目的`依赖图`
function createGraph(entry) {
    // 得到入口文件的依赖关系
    const mainAsset = createAsset(entry);
    const queue = [mainAsset];
    for (const asset of queue) {
        asset.mapping = {};
        // 获取这个模块所在的目录
        const dirname = path.dirname(asset.filename);
        asset.dependencies.forEach((relativePath) => {
            // 通过将相对路径与父资源目录的路径连接,将相对路径转变为绝对路径
            // 每个文件的绝对路径是固定、唯一的
            const absolutePath = path.join(dirname, relativePath);
            // 递归解析其中所引入的其他资源
            const child = createAsset(absolutePath);
            asset.mapping[relativePath] = child.id;
            // 将`child`推入队列, 通过递归实现了这样它的依赖关系解析
            queue.push(child);
        });
    }

    // queue这就是最终的依赖关系图谱
    return queue;
}

// 自定义实现了require 方法,找到导出变量的引用逻辑
function bundle(graph) {
    let modules = '';
    graph.forEach((mod) => {
        modules += `${mod.id}: [
      function (require, module, exports) { ${mod.code} },
      ${JSON.stringify(mod.mapping)},
    ],`;
    });
    const result = `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];
        function localRequire(name) {
          return require(mapping[name]);
        }
        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);

// ⬅️ 创建dist目录,将打包的内容写入main.js中
fs.mkdir('dist', (err) => {
    if (!err)
        fs.writeFile('dist/main.js', result, (err1) => {
            if (!err1) console.log('打包成功');
        });
});

1)从入口文件开始解析
2)查找入口文件引入了哪些js文件,找到依赖关系
3)递归遍历引入的其他js,生成最终的依赖关系图谱
4)同时将ES6语法转化成ES5
5)最终生成一个可以在浏览器加载执行的 js 文件

dist/main.js文件

    // 文件里是一个立即执行函数
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];
        function localRequire(name) {
        // ⬅️ 第四步 跳转到这里 此时mapping[name] = 1,继续执行require(1)
        // ⬅️ 第六步 又跳转到这里 此时mapping[name] = 2,继续执行require(2)
          return require(mapping[name]);
        }
        const module = { exports : {} };
        // 第二步 ,执行fn函数
        fn(localRequire, module, module.exports);
        return module.exports;
      }
      // 第一步  执行require(0)
      require(0);
    })({
    
    // 立即执行函数的参数是一个对象,该对象有3个属性 
    // 0 代表entry.js; 
    // 1 代表message.js 
    // 2 代表name.js
  0: [
  // 第三步 跳转到这里 继续执行require('./message.js')
    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 }; }

      // 将message的内容显示到页面中
      var p = document.createElement('p');
      p.innerHTML = _message2.default;
      document.body.appendChild(p);
    },
    { "./message.js": 1 },
  ], 1: [
    function (require, module, exports) {
      "use strict";
    // ⬅️ 第五步 跳转到这里 继续执行require('./name.js')
      Object.defineProperty(exports, "__esModule", {
        value: true
      });

      var _name = require("./name.js");
    // ⬅️ 第八步 跳到这里 此时_name为{name: 'Webpack'}, 在exports对象上设置default属性,值为'hello Webpack!'
      exports.default = "hello " + _name.name + "!";
    },
    { "./name.js": 2 },
  ], 2: [
    function (require, module, exports) {
      "use strict";
    // ⬅️ 第七步 跳到这里 在传入的exports对象上添加name属性,值为'Webpack'
      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      var name = exports.name = 'Webpack';
    },
    {},
  ],
})


```js
分析文件的执行过程

1)整体大致分为10步,第一步从require(0)开始执行,调用内置的自定义require函数,跳转到第二步,执行fn函数

2)执行第三步require('./message.js'),继续跳转到第四步 require(mapping['./message.js']), 最终转化为require(1)

3)继续执行require(1),获取modules[1],也就是执行message.js的内容

4)第五步require('./name.js'),最终转化为require(2),执行name.js的内容

5)通过递归调用,将代码中导出的属性,放到exports对象中,一层层导出到最外层

6)最终通过_message2.default获取导出的值,页面显示hello Webpack!

Webpack的打包流程

总结一下webpack完整的打包流程

1)webpack从项目的entry入口文件开始递归分析,调用所有配置的 loader对模块进行编译

因为webpack默认只能识别js代码,所以如css文件、.vue结尾的文件,必须要通过对应的loader解析成js代码后,webpack才能识别

2)利用babel(babylon)将js代码转化为ast抽象语法树,然后通过babel-traverse对ast进行遍历

3)遍历的目的找到文件的import引用节点

因为现在我们引入文件都是通过import的方式引入,所以找到了import节点,就找到了文件的依赖关系

4)同时每个模块生成一个唯一的id,并将解析过的模块缓存起来,如果其他地方也引入该模块,就无需重新解析,最后根据依赖关系生成依赖图谱

5)递归遍历所有依赖图谱的模块,组装成一个个包含多个模块的 Chunk(块)

6)最后将生成的文件输出到 output 的目录中

原文链接:juejin.cn/post/714697…