Giter Club home page Giter Club logo

blog's People

Watchers

 avatar

blog's Issues

mini-webpack

mini-webpack

这是一个迷你的打包工具,是为了用少量的代码理解打包等工具的核心原理

想直接看源码可以看这个路径
mini-webpack: https://github.com/mekefly/mini-wabpack

webpack 是什么?

本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

为什么要做这个呢?

本质上,这样的事只是为了更加理解 webpack 的工作方式,更加理解 node 的工作原理,为以后的工作扑平道路

我们要做到的几个任物点

  • 制作依赖图
    • 提取依赖
      • 获取文件内容
      • 生成抽象语法树
      • 通过抽象语法树来生成依赖信息
      • 对代码进行 import 转 require
    • 对依赖进行平铺
  • 合成结果
    • 模板
      • 模板的包装方案
      • require 的手动实现
  • 写入文件

那么这就开始吧

我们将会使用小步骤的开发**完成这个项目

我们如果想要完成这个项目需要从小到大的解决若干问题

获取文件内容

这个就不多说了我们只需要获取 fs 包然后再请求对应的文件内容就可以了

import { readFileSync } from "fs";

export function getFileContent(filePath: string) {
  return readFileSync(filePath, { encoding: "utf-8" });
}

要注意的是,我们需要传入 encoding来设置转换的文件格式,防止乱码

生成抽象语法树

抽象语法树是一个非常庞大的概念,将文本解析成机器可阅读的一种操作,本项目暂时不手写它了,可能会在其他项目手写 mini 语法树欢迎继续关注

获取抽象语法树时有别人已经写好的项目@babel/parser,我们可以直接使用这个项目来完成语法树的生成

pnpm install @babel/parser -D

我们可以使用它导出的 parse 方法来导出语法树

例如

import { parse } from "@babel/parser";
parse("console.log('hello')");

但是,当我们使用 import {foo} from 'foo' 类似的语法时会报一个错误

SyntaxError: 'import' and 'export' may appear only with 'sourceType: "module"' (1:0)

...
...

错误写的很明白,只有 sourceTypemodule 时才能使用 import 我们给它加上就好了

import { parse } from "@babel/parser";

export function genAst(text: string) {
  return parse(text, { sourceType: "module" });
}

通过抽象语法树来生成依赖信息

获取依赖信息可以通过遍历语法树上的节点来找到对应的 import 节点

如果要遍历 ast 语法树我们可以使用 babel 为我们提供的工具 @babel/traverse

我们可以下载它

pnpm install @babel/traverse -D

我们可以使用一些小工具来查看语法树 https://astexplorer.net/

image

我们看到上面有个 ImportDeclaration 对的这个节点就是我们需要的包含 import 的语法树节点

我们可以通过下面的方式来获取这个节点

如果需要获取其他的节点也是一样的道理

import traverse from "@babel/traverse";
traverse(ast, {
  ImportDeclaration(node) {
    console.log(node);
  },
});

我们可以把代码完善一下

import traverse from "@babel/traverse";
import { genAst } from "./genAst";
export function dependencyAnalysis(ast: ReturnType<typeof genAst>) {
  //用与存储依赖的依赖图
  const dep: string[] = [];
  traverse(ast, {
    ImportDeclaration(node) {
      //获取到依赖的路径,可能是相对或绝对,至于如何处理就是 `path` 工具的事了
      const depPath = node.node.source.value;
      dep.push(depPath);
    },
  });
  //最终返回了依赖的列表
  return dep;
}

对代码进行 import 转 require

想要将 import 转为 require 可以使用 babel-core 来完成

pnpm install babel-core -D

使用也非常简单,我们直接使用它提供的 transform 函数来完成转换

function importToRequire(file: string) {
  return transform(file, { presets: ["env"] }).code ?? "";
}

提取依赖信息

我们将上面提取到的有用信息进行导出合并就可以里,为下一步做准备

export let id = 0;
export type Dependencies = ReturnType<typeof getDependencies>;
export function getDependencies(fileFullPath: string, path: string) {
  const file = getFileContent(fileFullPath);
  const ast = genAst(file);
  const dep = dependencyAnalysis(ast);
  const code = importToRequire(file);

  return {
    //为什么需要id? 这样来标识模块的唯一性,fullPath也可以,但不你要想到代码可能是运行在别人电脑上的,路径中有很多隐私信息会暴露,并且全路径是不需要的
    id: id++,
    dep,
    fullPath: fileFullPath,
    path,
    code,
    //mapping是局部模块名id映射例如 {"./foo.js":1}
    //当这个代码内执行require时将会从mapping中找到需要请求的模块的唯一id,然后根据id找到模块
    mapping: {} as any,
  };
}

为什么需要mapping见下面

那么如何找到目标函数呢?

制作依赖图

要制作依赖图我们需要解决以下几个问题

  • 遇到递归依赖该怎么办?
//foo.js
const bar = require("bar.js")
//bar.js
const foo = require("foo.js")

这种情况要怎么阻止呢

我们可以提供一个已完成加载的列表
const set = []
set.add("foo.js")
set.add("bar.js")

如果 在寻找依赖时发现 bar.js 已经做过处理了,这时候我们直接停止不在处理当前依赖,转而处理下一个依赖

  • 如何将依赖摊平?

这里我使用的是广度优先搜索

例如

i => [a,b]
a => [c,d]
b => [e,f]

那么我们便历 i 添加到图表中这时

graph:[a,b]

然后便历数组继续平铺

i: 0
索引为 0 然后检查 a 的依赖便利添加到图表中
[a,b,c,d]

i:1
索引为 1 然后检查 b 的依赖便利添加到图表中
[a,b,c,d,e,f]

....

这样运行下去,所有依赖都将平铺

解决完这些问题后,
这时候我们就得到了下面代码

import { resolve, dirname } from "path";
import { getDependencies, id } from "./getDependencies";
import { mainPath } from "./index";

//对模块id做一个提前记录,当循环依赖时可能无法获取到id,提前记录以代需要时拿到,然后再对相对路径与模块id做一个映射
const modulesMapping: any = {};
export function genGraph() {
  const fullPath = resolve(mainPath);

  //已完成平铺的列表
  const dependencies = getDependencies(fullPath, mainPath);
  modulesMapping[fullPath] = 0;

  //已完成平铺的路径
  const completedPath: Set<string> = new Set();

  //添加到已完成平铺的列表中
  completedPath.add(fullPath);
  const graph: Array<ReturnType<typeof getDependencies>> = [dependencies];

  //便历图表
  for (const { dep, fullPath: filePath, mapping } of graph) {
    //对图表中的依赖进行平铺
    flattening(dep, filePath, mapping, completedPath, graph);
  }
  return graph;
}
function flattening(
  dep: string[],
  filePath: string,
  mapping: any,
  completedPath: Set<string>,
  graph: {
    id: number;
    dep: string[];
    fullPath: string;
    path: string;
    code: string;
    mapping: any;
  }[]
) {
  dep.forEach((path) => {
    //全路径
    const fullPath = resolve(dirname(filePath), path);

    //相对路径与模块id映射
    mappingId(fullPath, mapping, path);

    //如果平铺完成就停止运行
    if (loaded(completedPath, fullPath)) {
      return;
    }

    //获取依赖
    const dependencies = getDependencies(fullPath, path);
    //平铺到图表中
    graph.push(dependencies);

    //对已完成依赖解析的文件进行记录
    completedPath.add(filePath);
  });
}
function loaded(completedPath: Set<unknown>, fullPath: string) {
  return completedPath.has(fullPath);
}

//将依赖的相对路径与模块id完成映射
function mappingId(fullPath: string, mapping: any, path: string) {
  if (typeof modulesMapping[fullPath] === "undefined") {
    mapping[path] = modulesMapping[fullPath] = id;
  } else {
    mapping[path] = modulesMapping[fullPath];
  }
}

模板的包装方案

我想到的方案是 require 的方案
例如

// index.js
const {foo} = require("./foo.js")

// foo.js
exports.foo = function () {
  console.log("This is Foo");
}
  • 我们可以将他们直接合并吗?

当然不可以,代码中是会有可能有重复的变量的

  • 那应该如何合并?

我们要做的是让下面代码正常运行在同一个文件里

但是如果拥有同样的变量,它们将相互影响

所以我们需要使用函数包裹起来

function () {
  const {foo} = require("./foo.js")
}
function () {
  exports.foo = function () {
    console.log("This is Foo");
  }
}

然后为它提供一些必要的参数

function (require,module,exports) {
  const {foo} = require("./foo.js")
}
function (require,module,exports) {
  exports.foo = function () {
    console.log("This is Foo");
  }
}

这样就可以同时支持像下面这种写法了,这个就是 cjs 模块规范

function (require,module,exports) {
  function foo() {
    console.log("This is Foo");
  }
  module.exports = {
    foo
  }
}
  • 那么如何调用呢?

那就要看下面了

require 的手动实现

我们先回想一下require的作用

  • 参数 传入路径
  • 结果 目标路径模块运行产生的结果( exports )
function require(path) {
  //找到目标函数
  //生成 module
  const module = {exports:{}}
  //执行
  target(require,module,module.exports)

  //返回exports
  return module.exports
}

那么如何找到目标函数呢?

我们可以用 mapping

我们设想一下 一个 map 的 key 是路径,然后值是对应的函数,不就可以通过 path 获取到 模块 并执行了吗

const modules = {
  "foo.js": function (require,module,exports) {
    exports.foo = function () {
      console.log("This is Foo");
    }
  },
  "index.js": function (require,module,exports) {
    const {foo} = require("./foo.js")
  }
}
  • 但是这样就完美了吗?

这样的情况下可能会依赖冲突

请注意,这里使用的都是相对路径

我们可以设想一下,如a目录有 index.js,foo.js; b 目录也有 index.js,foo.js,他们都是一样的 index.js 引用 foo.js

这时候跟目录的index.js 同时引用/a/index.js和/b/index.js

依赖扁平后就会同时有两个foo.js

/index.js, /a/index.js ,/b/index.js ,foo.js ,foo.js

  • 这该怎么办呢?

我们就可以使用局部映射这就是上文提到模块 id 的作用了,唯一指定对应的模块

const modules = {
  0: [function (require,module,exports) {
    const {foo} = require("./foo.js")
  },
  {"./foo.js": 1}
  ],
  1: [function (require,module,exports) {
    exports.foo = function () {
      console.log("This is Foo");
    }
  },{}
  ],
}

当执行 require 时我们就去自己的映射表中找到平铺的模块 id,
然后通过唯一id去寻找对应的模块

  • 还有一些情况,比如 require 被执行多次如何处理呢?

我们可以使用缓存把模块运行产生的结果给放到缓存数组里,如果再次执行, 从数组里拿值

  var cache = {};

  function require(id){
    if(cache[id]) return cache[id].exports;
    ......
    const module = cache[id] = {......}
    ......
  }

这时候我们得到了完整代码

(function (modules) {
  var cache = {};
  require(0);
  function require(id) {
    //缓存
    if (cache[id]) return cache[id].exports;

    //找到目标函数

    var m = modules[id];
    var fn = m[0];
    var mapping = m[1];

    //本地 mapping映射
    function localRequire(path) {
      return require(mapping[path]);
    }

    //生成 module

    var module = (cache[id] = { exports: {} });

    //执行
    fn(localRequire, module, module.exports);

    //返回
    return module.exports;
  }
})({
  0: [
    function (require, module, exports) {
      const {foo} = require("./foo.js")
    },
    { "foo.js": 1 },
  ],
  1: [
    function (require, module, exports) {
      exports.foo = function () {
        console.log("This is Foo");
      }
    },
    {},
  ],
});

合成结果

通过模板生成代码

import { Dependencies } from "./getDependencies";

export function generateCode(graph: Dependencies[]) {
  return `
(function (modules) {
  var cache = {};
  require(0);
  function require(id) {
    if (cache[id]) {
      return cache[id].exports;
    }
    var m = modules[id];
    var fn = m[0];
    var mapping = m[1];
    function localRequire(path) {
      return require(mapping[path]);
    }
    var module = (cache[id] = { exports: {} });
    fn(localRequire, module, module.exports);
    return module.exports;
  }
})({
  ${graph
    .map((d) => {
      return `
    ${d.id}:[
      function (require, module, exports) {
        ${d.code}
      },
      ${JSON.stringify(d.mapping)}
    ]
    `;
    })
    .join()}
});
  `;
}

写入文件

import { genGraph } from "./genGraph";
import { generateCode } from "./template";
import { writeIn } from "./index";
import { writeFileSync } from "fs";
import { resolve } from "path";
import { builder } from "./builder";

const buildPath = "build/index.js";
export const mainPath = "./example/index.js";

export function builder() {
  const graph = genGraph();
  const code = generateCode(graph);
  writeIn(code);

}

export function writeIn(text: string) {
  writeFileSync(resolve(buildPath), text);
}

好了终于完成了,如果需要详细信息的话,以看原码仓库,仓库地址应该在文件的最上方,喜欢的话可以加个star一起讨论哦!

什么是心智模型

什么是心智模型

我们来看这样的一段代码

let a = 0;
let b = a;
a = 10;

请问你的思考方式是怎么样的呢?

你可能是这样思考的

  • let a = 0;
    • 声明了一个变量 a ,并赋值为 0
  • let b = a;
    • 声明变量 b ,并赋值为 a
    • a 是一个变量,a 的值是 0 ,所以 b 的值也为 0
  • a = 10
    • a 的值赋为 10
  • 那么最终结果就是 a10b0

像这样的,搭建你**的框架的模型,就叫做心智模型。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.