webpack 收集依赖、打包输出精简实现

安装babel插件

  • 由于ES6转ES5中需要用到babel,所以要用到一下插件
npm install @babel/core @babel/parser @babel/traverse @babel/preset-env --save-dev

目录结构及代码:

- demo
    - entry.js
    - message.js
    - name.js
- bundler.js //最后打包的文件

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

// message.js
import {name} from './name.js';
export default `hello ${name}!`;

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

读取文件信息,获取当前js文件的依赖关系

const fs = require("fs");
const path = require("path");
const babylon = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

let ID = 0;
//读取文件信息,并获得当前js文件的依赖关系
function createAsset(filename) {
  //获取文件,返回值是字符串
  const content = fs.readFileSync(filename, "utf-8");

  //讲字符串为ast(抽象语法树, 这个是编译原理的知识,说得简单一点就是,可以把js文件里的代码抽象成一个对象,代码的信息会存在对象中)
  //babylon 这个工具是是负责解析字符串并生产ast。
  const ast = babylon.parse(content, {
    sourceType: "module",
  });

  //用来存储 文件所依赖的模块,简单来说就是,当前js文件 import 了哪些文件,都会保存在这个数组里
  const dependencies = [];

  //遍历当前ast(抽象语法树)
  traverse(ast, {
    //找到有 import语法 的对应节点
    ImportDeclaration: ({ node }) => {
      //把当前依赖的模块加入到数组中,其实这存的是字符串,
      //例如 如果当前js文件 有一句 import message from './message.js',
      //'./message.js' === node.source.value
      dependencies.push(node.source.value);
    },
  });

  //模块的id 从0开始, 相当一个js文件 可以看成一个模块
  const id = ID++;

  //这边主要把ES6 的代码转成 ES5
  const { code } = babel.transformFromAstSync(ast, null, {
    presets: ["@babel/preset-env"],
  });

  return {
    id,
    filename,
    dependencies,
    code,
  };
}

在这里插入图片描述

广度遍历获取所有依赖图

//从入口开始分析所有依赖项,形成依赖图,采用广度遍历
function createGraph(entry) {
  const mainAsset = createAsset(entry);
  console.log(mainAsset);

  //既然要广度遍历肯定要有一个队列,第一个元素肯定是 从 "./demo/entry.js" 返回的信息
  const queue = [mainAsset];

  for (const asset of queue) {
    //获取文件夹路径
    const dirname = path.dirname(asset.filename);

    //新增一个属性来保存子依赖项的数据
    //保存类似 这样的数据结构 --->  {"./message.js" : 1}
    asset.mapping = {};

    //采用广度遍历
    asset.dependencies.forEach((relativePath) => {
      const absolutePath = path.join(dirname, relativePath);

      //获得子依赖(子模块)的依赖项、代码、模块id,文件名
      const child = createAsset(absolutePath);

      //给子依赖项赋值,
      asset.mapping[relativePath] = child.id;

      //将子依赖也加入队列中,广度遍历
      queue.push(child);
    });
  }
  return queue;
}

在这里插入图片描述

  • mapping这个字段是把当前模块依赖的文件名称 和 模块的id 做一个映射,目的是为了更方便查找模块

生成浏览器可执行代码

  • 其实bundle函数就是返回我们构造的字符串,拿到字符串,我们把字符串导出成bundle.js。
//根据生成的依赖关系图,生成对应环境能执行的代码,目前是生产浏览器可以执行的
function bundle(graph) {
  let modules = "";

  console.log(graph);

  //循环依赖关系,并把每个模块中的代码存在function作用域里
  //转换依赖图格式 => {id:[fn,mapping],...}
  //会作为最后生成代码的入参
  graph.forEach((mod) => {
    modules += `${mod.id}:[
      function (require, module, exports){
        ${mod.code}
      },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });

  //require, module, exports 是 cjs的标准不能再浏览器中直接使用,所以这里模拟cjs模块加载,执行,导出操作。
  const result = `
    (function(modules){
      //创建require函数, 它接受一个模块ID(这个模块id是数字0,1,2) ,它会在我们上面定义 modules 中找到对应是模块.
      function require(id){
        const [fn, mapping] = modules[id];
        function localRequire(relativePath){
          //根据模块的路径在mapping中找到对应的模块id
          return require(mapping[relativePath]);
        }
        const module = {exports:{}};
        //执行每个模块的代码。
        fn(localRequire,module,module.exports);
        return module.exports;
      }
      //执行入口文件,
      require(0);
    })({${modules}})
  `;

  return result;
}

const graph = createGraph("./demo/entry.js");
const ret = bundle(graph);

// 打包生成文件
fs.writeFileSync("./bundle.js", ret);

最终生成的可执行内容:

(function (modules) {
  //创建require函数, 它接受一个模块ID(这个模块id是数字0,1,2) ,它会在我们上面定义 modules 中找到对应是模块.
  function require(id) {
    const [fn, mapping] = modules[id];
    function localRequire(relativePath) {
      //根据模块的路径在mapping中找到对应的模块id
      return require(mapping[relativePath]);
    }
    const module = { exports: {} };
    //执行每个模块的代码。
    fn(localRequire, module, module.exports);
    return module.exports;
  }
  //执行入口文件,
  require(0);
})({
  0: [
    function (require, module, exports) {
      "use strict";

      var _message = _interopRequireDefault(require("./message.js"));
      function _interopRequireDefault(obj) {
        return obj && obj.__esModule ? obj : { default: obj };
      }
      console.log(_message["default"]);
    },
    { "./message.js": 1 },
  ],
  1: [
    function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true,
      });
      exports["default"] = void 0;
      var _name = require("./name.js");
      var _default = "hello ".concat(_name.name, "!");
      exports["default"] = _default;
    },
    { "./name.js": 2 },
  ],
  2: [
    function (require, module, exports) {
      "use strict";

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

代码示例:

const fs = require("fs");
const path = require("path");
const babylon = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

let ID = 0;
//读取文件信息,并获得当前js文件的依赖关系
function createAsset(filename) {
  //获取文件,返回值是字符串
  const content = fs.readFileSync(filename, "utf-8");

  //讲字符串为ast(抽象语法树, 这个是编译原理的知识,说得简单一点就是,可以把js文件里的代码抽象成一个对象,代码的信息会存在对象中)
  //babylon 这个工具是是负责解析字符串并生产ast。
  const ast = babylon.parse(content, {
    sourceType: "module",
  });

  //用来存储 文件所依赖的模块,简单来说就是,当前js文件 import 了哪些文件,都会保存在这个数组里
  const dependencies = [];

  //遍历当前ast(抽象语法树)
  traverse(ast, {
    //找到有 import语法 的对应节点
    ImportDeclaration: ({ node }) => {
      //把当前依赖的模块加入到数组中,其实这存的是字符串,
      //例如 如果当前js文件 有一句 import message from './message.js',
      //'./message.js' === node.source.value
      dependencies.push(node.source.value);
    },
  });

  //模块的id 从0开始, 相当一个js文件 可以看成一个模块
  const id = ID++;

  //这边主要把ES6 的代码转成 ES5
  const { code } = babel.transformFromAstSync(ast, null, {
    presets: ["@babel/preset-env"],
  });

  return {
    id,
    filename,
    dependencies,
    code,
  };
}

//从入口开始分析所有依赖项,形成依赖图,采用广度遍历
function createGraph(entry) {
  const mainAsset = createAsset(entry);
  console.log(mainAsset);

  //既然要广度遍历肯定要有一个队列,第一个元素肯定是 从 "./demo/entry.js" 返回的信息
  const queue = [mainAsset];

  for (const asset of queue) {
    const dirname = path.dirname(asset.filename);

    //新增一个属性来保存子依赖项的数据
    //保存类似 这样的数据结构 --->  {"./message.js" : 1}
    asset.mapping = {};

    //采用广度遍历
    asset.dependencies.forEach((relativePath) => {
      const absolutePath = path.join(dirname, relativePath);

      //获得子依赖(子模块)的依赖项、代码、模块id,文件名
      const child = createAsset(absolutePath);

      //给子依赖项赋值,
      asset.mapping[relativePath] = child.id;

      //将子依赖也加入队列中,广度遍历
      queue.push(child);
    });
  }
  return queue;
}

//根据生成的依赖关系图,生成对应环境能执行的代码,目前是生产浏览器可以执行的
function bundle(graph) {
  let modules = "";

  console.log(graph);

  //循环依赖关系,并把每个模块中的代码存在function作用域里
  graph.forEach((mod) => {
    modules += `${mod.id}:[
      function (require, module, exports){
        ${mod.code}
      },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });

  //require, module, exports 是 cjs的标准不能再浏览器中直接使用,所以这里模拟cjs模块加载,执行,导出操作。
  const result = `
    (function(modules){
      //创建require函数, 它接受一个模块ID(这个模块id是数字0,1,2) ,它会在我们上面定义 modules 中找到对应是模块.
      function require(id){
        const [fn, mapping] = modules[id];
        function localRequire(relativePath){
          //根据模块的路径在mapping中找到对应的模块id
          return require(mapping[relativePath]);
        }
        const module = {exports:{}};
        //执行每个模块的代码。
        fn(localRequire,module,module.exports);
        return module.exports;
      }
      //执行入口文件,
      require(0);
    })({${modules}})
  `;

  return result;
}

const graph = createGraph("./demo/entry.js");
const ret = bundle(graph);

// 打包生成文件
fs.writeFileSync("./bundle.js", ret);

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值