mekefly / blog Goto Github PK
View Code? Open in Web Editor NEWblog
blog
这是一个迷你的打包工具,是为了用少量的代码理解打包等工具的核心原理
想直接看源码可以看这个路径
mini-webpack: https://github.com/mekefly/mini-wabpack
本质上,webpack
是一个用于现代 JavaScript
应用程序的 静态模块打包工具。当 webpack
处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
本质上,这样的事只是为了更加理解 webpack
的工作方式,更加理解 node
的工作原理,为以后的工作扑平道路
我们将会使用小步骤的开发**完成这个项目
我们如果想要完成这个项目需要从小到大的解决若干问题
这个就不多说了我们只需要获取 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)
...
...
错误写的很明白,只有 sourceType
为 module
时才能使用 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/
我们看到上面有个 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
可以使用 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,
};
}
要制作依赖图我们需要解决以下几个问题
//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的作用
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去寻找对应的模块
我们可以使用缓存把模块运行产生的结果给放到缓存数组里,如果再次执行, 从数组里拿值
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
。- 那么最终结果就是
a
为10
和b
为0
。
像这样的,搭建你**的框架的模型,就叫做心智模型。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.