FiberNode

V16

每一个ReactElement对应一个Fiber对象,记录各个节点的状态,由于Fiber对象并不是绑定在组件的实例上,这也给hooks实现时,拿到每个节点的状态提供了方便

Fiber 记录了整个应用的树型结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// Fiber对应一个组件需要被处理或者已经处理了,一个组件可以有一个或者多个Fiber
type Fiber = {|
// 标记不同的组件类型
tag: WorkTag,

// ReactElement里面的key
key: null | string,

// ReactElement.type,也就是我们调用`createElement`的第一个参数
elementType: any,

// The resolved function/class/ associated with this fiber.
// 异步组件resolved之后返回的内容,一般是`function`或者`class`
type: any,

// The local state associated with this fiber.
// 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
stateNode: any,

// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
return: Fiber | null,

// 单链表树结构
// 指向自己的第一个子节点
child: Fiber | null,
// 指向自己的兄弟结构
// 兄弟节点的return指向同一个父节点
sibling: Fiber | null,
index: number,

// ref属性
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

// 新的变动带来的新的props
pendingProps: any,
// 上一次渲染完成之后的props
memoizedProps: any,

// 该Fiber对应的组件产生的Update会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,

// 上一次渲染的时候的state
memoizedState: any,

// 一个列表,存放这个Fiber依赖的context
firstContextDependency: ContextDependency<mixed> | null,

// 用来描述当前Fiber和他子树的`Bitfield`
// 共存的模式表示这个子树是否默认是异步渲染的
// Fiber被创建的时候他会继承父Fiber
// 其他的标识也可以在创建的时候被设置
// 但是在创建之后不应该再被修改,特别是他的子Fiber创建之前
mode: TypeOfMode,

// Effect
// 用来记录Side Effect
effectTag: SideEffectTag,

// 单链表用来快速查找下一个side effect
nextEffect: Fiber | null,

// 子树中第一个side effect
firstEffect: Fiber | null,
// 子树中最后一个side effect
lastEffect: Fiber | null,

// 代表任务在未来的哪个时间点应该被完成
// 不包括他的子树产生的任务
expirationTime: ExpirationTime,

// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,

// 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber
// 镜像Fiber
// 我们称他为`current <==> workInProgress`
// 在渲染完成之后他们会交换位置
alternate: Fiber | null,

// 下面是调试相关的,收集每个Fiber和子树渲染时间的

actualDuration?: number,

// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,

// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,

// Sum of base times for all descedents of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,

// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only
_debugID?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,

V17

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// A Fiber is work on a Component that needs to be done or was done. There can
// be more than one per component.

// Fiber 是组件的中的工作内容,需要完成或已经完成,每个组件可以个有多个。

export type Fiber = {|
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.

// 第一个字段概念上是实例的一部分,它过去被拆分成单独的类型并且存在于其他Fiber节点的字段中。
// 直到Flow修复了交叉错误,我们把它合并成单一类型

// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.

// 实例在组件的所有的版本中共享,可以很容易的拆分成单个对象避免复制太多内容到镜像树上,
// 现在我们把它放在单独的对象上,为了初始化渲染期间最小化创建对象的数量

// Tag identifying the type of fiber.
// fiber类型, 标签标识

tag: WorkTag,

// Unique identifier of this child.
// 这个子元素的唯一标识
key: null | string,

// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
// element.type的值,用于在调和子节点期间保留标识。
elementType: any,

// The resolved function/class/ associated with this fiber.
// 函数组件或类组件通过type链接,type为函数组件或类组件的引用
type: any,

// The local state associated with this fiber.
// 保存实例化之后的节点对象
stateNode: any,

// Conceptual aliases
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.

// Remaining fields belong to Fiber

// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
return: Fiber | null,

// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,

// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,

// Input is the data coming into process this fiber. Arguments. Props.
pendingProps: any, // This type will be more specific once we overload the tag.
memoizedProps: any, // The props used to create the output.

// A queue of state updates and callbacks.
updateQueue: mixed,

// The state used to create the output
memoizedState: any,

// Dependencies (contexts, events) for this fiber, if it has any
dependencies: Dependencies | null,

// Bitfield that describes properties about the fiber and its subtree. E.g.
// the ConcurrentMode flag indicates whether the subtree should be async-by-
// default. When a fiber is created, it inherits the mode of its
// parent. Additional flags can be set at creation time, but after that the
// value should remain unchanged throughout the fiber's lifetime, particularly
// before its child fibers are created.
mode: TypeOfMode,

// Effect
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,

// Singly linked list fast path to the next fiber with side-effects.
nextEffect: Fiber | null,

// The first and last fiber with side-effect within this subtree. This allows
// us to reuse a slice of the linked list when we reuse the work done within
// this fiber.
firstEffect: Fiber | null,
lastEffect: Fiber | null,

lanes: Lanes,
childLanes: Lanes,

// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: Fiber | null,

// Time spent rendering this Fiber and its descendants for the current update.
// This tells us how well the tree makes use of sCU for memoization.
// It is reset to 0 each time we render and only updated when we don't bailout.
// This field is only set when the enableProfilerTimer flag is enabled.
actualDuration?: number,

// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,

// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,

// Sum of base times for all descendants of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,

// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only

_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
_debugNeedsRemount?: boolean,

// Used to verify that the order of hooks does not change between renders.
_debugHookTypes?: Array<HookType> | null,
|};

FiberRoot

FiberRoot

  • 整个应用的起点
  • 包含应用挂载的目标节点
  • 记录整个应用更新过程的各种信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
type BaseFiberRootProperties = {|
// root节点,render方法接收的第二个参数
containerInfo: any,
// 只有在持久更新中会用到,也就是不支持增量更新的平台,react-dom不会用到
pendingChildren: any,
// 当前应用对应的Fiber对象,是Root Fiber
current: Fiber,

// 一下的优先级是用来区分
// 1) 没有提交(committed)的任务
// 2) 没有提交的挂起任务
// 3) 没有提交的可能被挂起的任务
// 我们选择不追踪每个单独的阻塞登记,为了兼顾性能
// The earliest and latest priority levels that are suspended from committing.
// 最老和新的在提交的时候被挂起的任务
earliestSuspendedTime: ExpirationTime,
latestSuspendedTime: ExpirationTime,
// The earliest and latest priority levels that are not known to be suspended.
// 最老和最新的不确定是否会挂起的优先级(所有任务进来一开始都是这个状态)
earliestPendingTime: ExpirationTime,
latestPendingTime: ExpirationTime,
// The latest priority level that was pinged by a resolved promise and can
// be retried.
// 最新的通过一个promise被reslove并且可以重新尝试的优先级
latestPingedTime: ExpirationTime,

// 如果有错误被抛出并且没有更多的更新存在,我们尝试在处理错误前同步重新从头渲染
// 在`renderRoot`出现无法处理的错误时会被设置为`true`
didError: boolean,

// 正在等待提交的任务的`expirationTime`
pendingCommitExpirationTime: ExpirationTime,
// 已经完成的任务的FiberRoot对象,如果你只有一个Root,那他永远只可能是这个Root对应的Fiber,或者是null
// 在commit阶段只会处理这个值对应的任务
finishedWork: Fiber | null,
// 在任务被挂起的时候通过setTimeout设置的返回内容,用来下一次如果有新的任务挂起时清理还没触发的timeout
timeoutHandle: TimeoutHandle | NoTimeout,
// 顶层context对象,只有主动调用`renderSubtreeIntoContainer`时才会有用
context: Object | null,
pendingContext: Object | null,
// 用来确定第一次渲染的时候是否需要融合
+hydrate: boolean,
// 当前root上剩余的过期时间
// TODO: 提到renderer里面区处理
nextExpirationTimeToWorkOn: ExpirationTime,
// 当前更新对应的过期时间
expirationTime: ExpirationTime,
// List of top-level batches. This list indicates whether a commit should be
// deferred. Also contains completion callbacks.
// TODO: Lift this into the renderer
// 顶层批次(批处理任务?)这个变量指明一个commit是否应该被推迟
// 同时包括完成之后的回调
// 貌似用在测试的时候?
firstBatch: Batch | null,
// root之间关联的链表结构
nextScheduledRoot: FiberRoot | null,
|};

mode

创建跟节点RootFiber时,在current属性上挂载了通过createHostRootFiber创建的fiber对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function createHostRootFiber(tag: RootTag): Fiber {
let mode;
if (tag === ConcurrentRoot) {
mode = ConcurrentMode | BlockingMode | StrictMode;
} else if (tag === BlockingRoot) {
mode = BlockingMode | StrictMode;
} else {
// tag 为0
mode = NoMode;
}

// 开发时mode为8
if (enableProfilerTimer && isDevToolsPresent) {
// Always collect profile timings when DevTools are present.
// This enables DevTools to start capturing timing at any point–
// Without some nodes in the tree having empty base times.
mode |= ProfileMode;
}

return createFiber(HostRoot, null, null, mode);
}

ReactDOM.render

ReactDOM.render

packages/react-dom/src/client/ReactDOMLegacy.js

  • 创建ReactRoot
  • 创将FiberRoot和RootFiber
  • 创将更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function render(
element: React$Element<any>,
container: Container,
callback: ?Function,
) {
// ...
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>, // 父组件
children: ReactNodeList, // 渲染组件
container: Container, // 容器
forceHydrate: boolean, // 是否强制注入
callback: ?Function, // 回调函数
) {
//...
// 查看是否有复用的节点
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// 初始化加载,在root上挂载创建的ReactRooter
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
// _internalRoot 也就是内部使用的fiberRoot
fiberRoot = root._internalRoot;
//...
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
//...
updateContainer(children, fiberRoot, parentComponent, callback);
}
// 最终都是调用了 updateContainer,并把创将的fiberRoot传入
return getPublicRootInstance(fiberRoot);
}
legacyCreateRootFromDOMContainer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 直接返回原生节点
function getReactRootElementInContainer(container: any) {
if (!container) {
return null;
}

if (container.nodeType === DOCUMENT_NODE) {
return container.documentElement;
} else {
return container.firstChild;
}
}

// 如果原生节点存在 shouldHydrate = true
function shouldHydrateDueToLegacyHeuristic(container) {
const rootElement = getReactRootElementInContainer(container);
return !!(
rootElement &&
rootElement.nodeType === ELEMENT_NODE &&
rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
);
}

function legacyCreateRootFromDOMContainer(
container: Container,
forceHydrate: boolean,
): RootType {
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
if (!shouldHydrate) {
let warned = false;
let rootSibling;
// 清空容器的子节点
while ((rootSibling = container.lastChild)) {
if (__DEV__) {
if (
!warned &&
rootSibling.nodeType === ELEMENT_NODE &&
// ROOT_ATTRIBUTE_NAME 用于服务端渲染的判断,是否需要合并节点
(rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
) {
warned = true;
console.error(
'render(): Target node has markup rendered by React, but there ' +
'are unrelated nodes as well. This is most commonly caused by ' +
'white-space inserted around server-rendered markup.',
);
}
}
container.removeChild(rootSibling);
}
}
// ...
return createLegacyRoot(
container,
shouldHydrate
? {
hydrate: true,
}
: undefined,
);
}
createLegacyRoot 创建ReactRoot
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export function createLegacyRoot(
container: Container,
options?: RootOptions,
): RootType {
//
return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}

function ReactDOMBlockingRoot(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
this._internalRoot = createRootImpl(container, tag, options);
}

function createRootImpl(
container: Container,
// 常量0,标识根节点
tag: RootTag,
options: void | RootOptions,
) {
// createContainer 最终调用了createFiberRoot
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
//...
return root;
}
createFiberRoot
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
// 容器节点本身是FiberRoot对象
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
// 创建的Fiber对象
const uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// 为原始的fiber节点添加updateQueue信息
// function initializeUpdateQueue(fiber) {
// var queue = {
// baseState: fiber.memoizedState,
// firstBaseUpdate: null,
// lastBaseUpdate: null,
// shared: {
// pending: null
// },
// effects: null
// };
// fiber.updateQueue = queue;
// }
initializeUpdateQueue(uninitializedFiber);
return root;
}
updateContainer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// container = container._reactRootContainer = legacyCreateRootFromDOMContainer(container,forceHydrate)._internalRoot;
const current = container.current;
const eventTime = requestEventTime();

//创将事件优先级
const lane = requestUpdateLane(current);

// 创将更新对象
const update = createUpdate(eventTime, lane);
update.payload = {element};

// 加入到更新队列中
enqueueUpdate(current, update);
// 调度更新
scheduleUpdateOnFiber(current, lane, eventTime);

return lane;
}

Express实践 ② 完善核心功能

密码加密/校验

  • 密码不可以明文存储,需要生成 hash,在模型文件中,可以使用 Getters, Setters & Virtuals 方法并配合 bcrypt 模块 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // models\user.js

    const { Model, Op } = require("sequelize");
    const bcrypt = require("bcrypt");
    module.exports = (sequelize, DataTypes) => {
    class User extends Model {}
    User.init(
    {
    password: {
    type: DataTypes.STRING,
    set(value) {
    // 在设置值或更新值得时候会执行这个方法
    this.setDataValue("password", bcrypt.hashSync(value, 10));
    },
    },
    },
    {
    sequelize,
    modelName: "User",
    }
    );
    return User;
    };
  • 密码校验,模型校验方法只有设置或更新数据时触发,所以可以考虑将验证方法写在模型的类上,或是在路由中实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // routes\user.js
    // 路由中校验数据

    router.post("/login", async function (req, res, next) {
    try {
    const data = await User.findOne({
    where: {
    username: req.body.username,
    email: req.body.email,
    },
    });

    if (!bcrypt.compareSync(req.body.password, data.password)) {
    throw new Error("密码错误");

    res.status(401);
    return;
    }

    res.end();
    } catch (err) {}
    });
  • 生成 jwt,用户保存用户的登录状态, 需要依赖 jsonwebtoken

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // routes\user.js

    const bcrypt = require("bcrypt");
    const jwt = require("jsonwebtoken");

    router.post("/login", async function (req, res, next) {
    try {
    const data = await User.findOne({
    // ...
    });
    const token = jwt.sign(
    {
    data: "foobar",
    },
    process.env.SECRET,
    { expiresIn: "30d" }
    );
    res.end(token);
    } catch (err) {}
    });
  • SECRET 通常作为环境变量,且禁止提交到仓库, 可以使用 crypto 生成随机密钥

    1
    crypto.randomBytes(64).toString("hex");

中间件/校验 jwt

对接口的校验权限的校验不止一个,所以不会再每个路由文件中实现一边逻辑,这就是用到了 中间件

下面是一个路由级别的中间件,用于处理用户的 token 是否有效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// middleware\auth.js

const jwt = require("jsonwebtoken");
const { User } = require("../models");

module.exports = async function (req, res, next) {
try {
const { token } = req.headers;
if (!token) {
throw new Error("用户未登录");
}

const { userId } = jwt.verify(token, process.env.SECRET);

const user = await User.findByPk(userId);

if (!user) throw new Error("用户不存在");

if (user.role === "[some role]") throw new Error("用户权限错误");

// 可以直接再路由的req上获取用户信息
req.user = user;
next();
} catch (err) {
// 客户端需要处理token失效的情况

res.status(500).json({
err: err.message,
});
}
};

嵌套关联查询

上一章讲解了一对多的关联查询,但是关联字段中可能还需要关联另一张表,也就是嵌套关联查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
router.get("/", async function (req, res, next) {
try {
const condition = {
order: [["id", "DESC"]],
// 全局过滤 两个关联的外键
attributes: {
exclude: ["categoryId", "userId"],
},
include: [
// as 关联字段别名,需要和model中的对应
// attributes 用于过滤关联模型中的字段
{ model: User, as: "user", attributes: ["id", "username"] },
{
model: Category,
as: "category",
attributes: ["id", "name"],

// 通过嵌套 include 关联查询
include: {
model: Article,
as: "article",
attributes: ["id", "title"],
},
},
],
};
const page = req.query.page || 1;
const pageSize = req.query.pageSize || 10;

condition.limit = pageSize;
condition.offset = (page - 1) * pageSize;

const { count, rows } = await Course.findAndCountAll(condition);
res.json(rows);
} catch (err) {}
});

这会查出来如下嵌套的返回格式, 他们的关系是

  • 一个课程数据多个用户,也属于多个分类。或者说一个用户有多个课程,一个分类包含多个课程。
  • 一个分类属于多篇文章。或者说一个文章有多个分类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"status": true,
"message": null,
"data": {
"course": [
{
"id": 12,
"name": "后端课程2",
"image": "xxx",
"recommended": true,
"introductory": false,
"content": "后端课程1,描述",
"likesCount": 10,
"chaptersCount": 10,
"createdAt": "2025-01-17T12:48:54.000Z",
"updatedAt": "2025-01-17T12:48:54.000Z",
"user": {
"id": 7,
"username": "user1"
},
"category": {
"id": 2,
"name": "后端课程",
"article": {
"id": 16,
"title": "标题100"
}
}
}
// ...剩余7条数据
],
"pagination": {
"total": 8,
"page": 1,
"pageSize": 10
}
}
}

但是其我们是以课程的视角去查询,用户,分类,以及文章都属于当前课程的附属属性,并不需要用层级关系去体现。会希望他们以同级的字段展示。这也是 sequelize 中 Eager Loading vs Lazy Loading 的概念。

include 关联字段表示 Eager Loading, 会直接把数据返回。而 Lazy Loading 允许用方法调用的方式,选择哪些数据需要,而这种方式正好处理数据的层级问题。

上面的 findAndCountAll 是无法使用 Lazy 关联查询的,因为它查询的是整个列表,必须明确用数据结构说面关联关系,并不是针对某个课程的关联查询

下面是查询单独一个课程的关联信息, 需要查询出关联的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
router.get("/:id", async function (req, res, next) {
try {
const condition = {
order: [["id", "DESC"]],
// 注意不要排除了关联 ID, 否则查询不到
// attributes: {
// exclude: ["categoryId", "userId"],
// },
};

const course = await Course.findByPk(req.params.id, condition);

// 通过课程查询分类,注意要带上关联的 articleId
const category = await course.getCategory({
attributes: ["id", "name", "articleId"],
});

// 通过分类查询文章
const article = await category.getArticle({
attributes: ["id", "title"],
});

res.json({
status: true,
message: null,
data: {
course,
category,
article,
},
});
} catch (err) {}
});

多对多的查询

实现一个点赞的功能:

  • 一个用户可以可以给多个课程点赞
  • 一个课程也可以被多个用户点赞

虽然可以直接在用户表中储存点赞课程,但不是一个好的做法, 存在以下问题:

无法保证数据一致性:难以通过外键验证课程是否存在。
查询复杂:查找特定课程的所有学生需要额外的解析逻辑。
扩展性差:不适合大规模、多条件查询的场景。

最佳实践是使用中间表:

数据一致性:中间表中的外键可以确保关联数据的完整性。
灵活性:中间表允许添加额外的信息(如时间戳、状态等)。
查询效率:数据库的索引可以优化多对多查询性能。
扩展性:关系较复杂时,可以轻松扩展中间表的结构。

修改模型,通过点赞中间表建立课程表和用户之间的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// models\user.js

class User extends Model {
static associate(models) {
models.User.belongsToMany(models.Course, {
through: models.Like,
as: "likeCourse",
});
}
}

// models\course.js
class User extends Model {
static associate(models) {
models.Course.belongsToMany(models.User, {
through: models.Like,
as: "courseWithUsers",
});
}
}

查询某个用户喜欢的课程, 如果使用 include 做查询在多对多的关系中无法实现分页,在 include 中无法对关联查询的数据分页,且模型中的 foreignKey 是必填的,否则会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
router.get("/:id", async function (req, res, next) {
try {
// 获取user实例
const user = await User.findByPk(req.params.id, {
include: [
{
model: Course,
as: "likeCourses",
// 无法使用分页
// limit: pageSize,
// offset: (page - 1) * pageSize,
},
],
});

res.json({
//...
});
} catch (err) {}
});

所以可以使用 Lazy Loading 手动请求数据, 模型中提供了内置的方法,可以用于获取分页数据

另外需要控制 不显示中间表的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
router.get("/:id", async function (req, res, next) {
try {
const page = req.query.page || 1;
const pageSize = req.query.pageSize || 10;

const limit = pageSize;
const offset = (page - 1) * pageSize;
const user = await User.findByPk(req.params.id);

// getLikeCourses 是模型内部映射的方法,通过 get + as别名 命名
const course = await user.getLikeCourses({
// 不显示中间表的数据
joinTableAttributes: [],
attributes: ["id", "name"],
limit,
offset,
});

// 另一个映射方法用于获取总条数
const total = await user.countLikeCourses();

res.json({
//...
});
} catch (err) {}
});

处理跨域

  • nginx
  • cors

Express实践 ① 用 sequelize 实现 CRUD

安装

初始化项目 express-generator

1
2
# --no-view 表示不生成视图文件
npx express-generator --no-view [项目名称]

安装 ORM 相关依赖,并初始化

1
2
3
4
npm install -D sequelize mysql2

# 初始化ORM
npx sequelize-cli init

修改 sequelize 配置文件

所有的参数都需要使用字符串格式

1
2
3
4
5
6
7
8
9
10
11
12
13
// config/config.js

{
"development": {
"username": "用户名",
"password": "密码",
"port": "端口号",
"database": "数据库名称",
"host": "127.0.0.1",
"dialect": "mysql",
"timezone": "+8:00"
}
}

创建数据库

字符集 utf8mb4
utf8mb4 支持 Unicode 字符集中的所有字符,包括表情符号(emoji)。一些其他语言字符,utf8 字符集无法处理的字符。

**字符集排序规则 utf8mb4_general_ci **

是一种常见的 不区分大小写(case-insensitive, CI) 排序规则,ci 代表 Case Insensitive,即不区分字母的大小写。
它会将大写字母和小写字母视为相同的字符进行排序。例如,字母 a 和 A 会被视为相同,而在某些排序规则下它们可能会被视为不同。

utf8mb4_unicode_ci — 精确排序

适用场景:多语言支持、需要精确排序的应用。
优点:符合 Unicode 排序规则,处理复杂字符(如带有重音符号的字母)时表现更好,能够准确地处理所有 Unicode 字符,适用于全球多种语言的应用。
缺点:与 utf8mb4_general_ci 相比,性能略差一些,因为它会考虑更多的字符特性进行排序。

创建模型

Creating the first Model (and Migration), 模型的名字用单数命名,生成的表名是复数

1
npx sequelize-cli model:generate --name User --attributes firstName:string,lastName:string,email:string

可以为模型添加校验条件 Validations & Constraints

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// models\user.js

"use strict";
const { Model } = require("sequelize");

module.exports = (sequelize, DataTypes) => {
class User extends Model {
static associate(models) {
// define association here
}
}
User.init(
{
email: DataTypes.STRING,
username: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: "用户名不能为空",
},
notEmpty: {
msg: "用户名不能为空",
},
async isUnique(value) {
const user = await User.findOne({ where: { username: value } });
if (user) {
throw new Error("用户名已存在");
}
},
len: {
args: [2, 20],
msg: "用户名长度在 2 到 20 之间",
},
},
},
password: DataTypes.STRING,
nickname: DataTypes.STRING,
sex: DataTypes.TINYINT,
company: DataTypes.STRING,
introduce: DataTypes.TEXT,
role: DataTypes.TINYINT,
avatar: DataTypes.STRING,
},
{
sequelize,
modelName: "User",
}
);
return User;
};

还可以通过 mysql-workbench 创建 ER 图,其中实线表示关联表没有自己的主键。

如果数据库不是在 Docker 环境,而是使用集成环境运行,迁移后表名可能是小写的,这会导致部署到服务器上之后 Mysql 严格区分大小写导致报错,可以在服务器上重新运行迁移

对于已经存在的表,如果可以在模型中对表名做映射的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class User extends Model {
static associate(models) {
models.User.belongsToMany(models.Course, {
through: models.Like,
// 指明特殊的外键字段
foreignKey: "user_name",
as: "likeCourses",
});
}
}
User.init(
{
user_name: DataTypes.STRING,
},
{
sequelize,
modelName: "User",
//指定模型和表名的对应关系
tableName: "user_table",
// 不需要时间字段
timestamps: false,
}
);

执行迁移文件

Running Migrations, 创建模型会自动生成迁移文件,迁移文件需要按情况手动修改,修改字段类型,添加索引,等操作都是在迁移文件中完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// migrations\20250117092337-create-user.js

module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
await queryInterface.createTable("Users", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER.UNSIGNED,
},
email: {
allowNull: false,
type: Sequelize.STRING,
},
username: {
allowNull: false,

type: Sequelize.STRING,
},
password: {
allowNull: false,
type: Sequelize.STRING,
},
nickname: {
allowNull: false,
type: Sequelize.STRING,
},
sex: {
type: Sequelize.TINYINT,
},
company: {
type: Sequelize.STRING,
},
introduce: {
type: Sequelize.TEXT,
},
role: {
type: Sequelize.TINYINT,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});

//添加索引
await queryInterface.addIndex("Users", ["email"], {
unique: true,
});

await queryInterface.addIndex("Users", ["username"], {
unique: true,
});
},
async down(queryInterface, Sequelize) {
// 回滚
await queryInterface.dropTable("Users");
},
};

修改完成后,执行迁移文件,这会在数据库中创建真正的数据表。

1
npx sequelize-cli db:migrate

如果数据表已经存在数据可以使用一下命令回归,这会执行迁移文件中的 down 方法,删除数据表

1
npx sequelize-cli db:migrate:undo  --name [迁移文件名称]

如果数据表数据有效不能删除,还需要对字段修改,就需要额外创建一个迁移文件, 并单独执行这个迁移文件, 并同步修改 model 文件新增或修改字段属性

1
2
3
npx sequelize-cli migration:generate --name add-avatar-to-user

npx sequelize-cli db:migrate --name xxxx-add-avatar-to-user

创建种子文件

Creating the first Seed 用于快速生成初始化的测试数据。

seed:all 会执行所有的种子文件,因此已经用 seed 初始化的表,还会再插入一遍数据

种子文件字段校验受到模型(索引字段),迁移文件(校验字段,类型)影响,需要注意字段是否匹配,否则命令执行可能报错

1
npx sequelize-cli seed:generate --name demo-user

需要手动修改种子文件,添加新增数据和删除数据的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// seeders\20250117101059-user.js

module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.bulkInsert("Users", [
{
email: "admin@live.com",
username: "admin",
password: "12345",
nickname: "admin",
sex: 1,
role: 0,
createdAt: new Date(),
updatedAt: new Date(),
},
]);
},

async down(queryInterface, Sequelize) {
await queryInterface.bulkDelete("Users", null);
},
};

开发路由

  • 在 router 文件夹下添加一个新的路由文件,并注册到 express 中

    1
    2
    3
    4
    // app.js
    var app = express();
    var courseRouter = require("./routes/course");
    app.use("/course", courseRouter);
  • 使用 模糊查询

  • 常用 查询方法

  • 201 响应码表示请求成功,且修改或创建了资源

  • 必须对用户提交的数据过滤,字段验证可以用模型实现,而且可以自定义异步方法,从数据库中查询校验

  • 避免孤儿数据
    外键约束影响性能,一般不允许使用。
    删除关联数据有风险,有些关联数据是用户数据不能删除
    可以检测只有没有关联信息的数据才能被删除

  • 查询 关联字段,文档只有语法,但是没有自动生成文件中如何添加的示例。

    在 model 文件中添加关联信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // models\course.js

    const { Model } = require("sequelize");
    module.exports = (sequelize, DataTypes) => {
    class Course extends Model {
    static associate(models) {
    // as 关联字段的别名, 在路由中使用的代码也需要改
    models.Course.belongsTo(models.User, { as: "user" });
    models.Course.belongsTo(models.Category, { as: "category" });
    }
    }
    // ...
    return Course;
    };

    查询条件中需要添加关联字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const condition = {
    // 排序字段
    order: [["id", "DESC"]],
    // 全局过滤 两个关联的外键
    attributes: {
    exclude: ["categoryId", "userId"],
    },
    include: [
    // as 关联字段别名,需要和model中的对应
    // attributes 用于过滤关联模型中的字段
    { model: User, as: "user", attributes: ["id", "username"] },
    { model: Category, as: "category", attributes: ["id", "name"] },
    ],
    };
  • 路由代码如下, 可以优化统一的错误处理以及参数的解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    // routes\course.js

    var express = require("express");
    var router = express.Router();
    const { Course, User, Category } = require("../../models");
    const { Op } = require("sequelize");

    router.get("/", async function (req, res, next) {
    try {
    const condition = {
    order: [["id", "DESC"]],
    // 全局过滤 两个关联的外键
    attributes: {
    exclude: ["categoryId", "userId"],
    },
    include: [
    // as 关联字段别名,需要和model中的对应
    // attributes 用于过滤关联模型中的字段
    { model: User, as: "user", attributes: ["id", "username"] },
    { model: Category, as: "category", attributes: ["id", "name"] },
    ],
    };

    const page = req.query.page || 1;
    const pageSize = req.query.pageSize || 10;

    condition.limit = pageSize;
    condition.offset = (page - 1) * pageSize;

    if (req.query.title) {
    condition.where = {
    title: {
    [Op.like]: `%${req.query.title}%`,
    },
    };
    }

    const { count, rows } = await Course.findAndCountAll(condition);

    res.json({
    status: true,
    message: null,
    data: {
    course: rows,
    pagination: {
    total: count,
    page,
    pageSize,
    },
    },
    });
    } catch (err) {
    console.log(err);
    res.status(500).json({
    status: false,
    message: err,
    });
    }
    });

    router.get("/:id", async function (req, res, next) {
    try {
    const { id } = req.params;
    const data = await Article.findByPk(id);

    if (data) {
    res.json({
    status: true,
    message: null,
    data,
    });
    } else {
    res.status(404).json({
    status: false,
    });
    }
    } catch {
    res.status(500).json({
    status: false,
    });
    }
    });

    router.post("/", async function (req, res, next) {
    try {
    const data = await Article.create(req.body);
    res.status(201).json({
    status: true,
    });
    } catch (err) {
    res.status(500).json({
    err: err,
    });
    }
    });

    router.delete("/:id", async function (req, res, next) {
    try {
    const data = await Article.destroy({
    where: {
    id: req.params.id,
    },
    });
    console.log(data);
    res.status(201).json({
    status: true,
    });
    } catch {
    res.status(500).json({
    status: false,
    });
    }
    });

    router.put("/:id", async function (req, res, next) {
    try {
    const count = await Article.count({
    where: { id: req.params.id },
    });
    if (count == 0) {
    res.status(404).json({
    status: false,
    message: "文章不存在",
    });
    } else {
    await Article.update(req.body, {
    where: { id: req.params.id },
    });
    res.status(201).json({
    status: true,
    message: "更新成功",
    });
    }
    } catch {
    res.status(500).json({
    status: false,
    });
    }
    });
    module.exports = router;

集合

Set集合的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Set {
constructor() {
this.set = { "a": 1, __proto__: { b: 1 } };
}
has(element) {
return Object.prototype.hasOwnProperty.call(this.set, element)
}
add(element) {
if (!this.has(element)) {
this.set[element] = element;
return true;
}
return false;
}
delete(element) {
if (this.has(element)) {
Reflect.deleteProperty(this.set, element)
return true;
}
return false;
}
clear() {
this.items = {};
}
size() {
return Reflect.ownKeys(this.set).length
}
values() {
return Object.values(this.items);
}
}

集合运算

  • 并集
1
2
3
4
5
6
7
8
9
10
function union(A, B) {
const set = new Set();
A.values().forEach(v => {
set.add(v)
});
B.values().forEach(v => {
set.add(v)
});
return set;
}
  • 交集
1
2
3
4
5
6
7
8
9
function intersection(A, B) {
const set = new Set();
A.values().forEach(item => {
if (B.has(item)) {
set.add(item);
}
});
return set;
}
  • 差集

A与B的差集表示存在于A,但是不存在于B

B与A的差集表示存在于B,但是不存在于A

1
2
3
4
5
6
7
8
9
function differenceSet(A, B) {
const set = new Set();
A.values().forEach(item => {
if (!B.has(item)) {
set.add(item);
}
});
return set;
}

只需要遍历集合长度较小的一个即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function differenceSet(A, B) {
const set = new Set();
let smallSet = A;
let bigSet = B;
if (smallSet.values().length > bigSet.values().length) {
smallSet = B;
bigSet = A;
}
smallSet.values().forEach(item => {
if (!bigSet.has(item)) {
set.add(item);
}
});
return set;
}
  • 子集
1
2
3
function childSet(A, B) {
return B.values().every(item => A.has(item))
}

Nodejs 基础 API

Buffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 20个字节
const b1 = Buffer.alloc(20);

// 直接去一块连续内存,可能有没有回收的垃圾数据
const b2 = Buffer.allocUnsafe(20);

// 为什么是 31 而不是01, 因为uft8 编码下并不是1
const b3 = Buffer.from("1");

// 无法保存文字的信息, 数组中的每一个必须是16进制 8 或2
const b4 = Buffer.from([1, "维护"]);

// 两块内存不会相同,内存上是分开的
const b5 = Buffer.alloc(3);
const b6 = Buffer.from(b5);

// 填充

const b7 = Buffer.alloc(5);
//会把整个buffer填满,如果长度不够,会截断
// 开始字节位置,结束字节位置
console.log(b7.fill("中", 1, 2));
console.log(b7.toString());

// 写入
const b8 = Buffer.alloc(5);
console.log(b8.write("中", 1, 4));

// toString 可以选择从哪一个字节下标开始解析

console.log(b7.toString("utf-8", 3, 6));

// slice 截取操作
const b9 = Buffer.from("为各位");
console.log(b9.slice());

// 查找可以指定从哪一位后面开始查找
console.log(b9.indexOf("为", 2));

// copy
const b10 = Buffer.from("为各位");

const b11 = Buffer.alloc(39);
// 开始结束位置
b10.copy(b11, 3, 6);

// concat

const b12 = Buffer.from("1");
const b13 = Buffer.from("2");
const b14 = Buffer.concat([b12, b13]);

// slice 方法已经被标记为过时,使用 subarray 从 Buffer 中截取一段数据
// start end
const b15 = b14.subarray(02);

// 判断
Buffer.isBuffer;

// 实现对 Buffer 的split
Buffer.prototype.split = function (sep) {
const length = Buffer.from(sep).length;
let ret = [];
let start = 0;
let offset = 0;
while ((offset = this.indexOf(sep, start)) !== -1) {
ret.push(this.subarray(start, offset).toString());
start = offset + length;
}
ret.push(this.subarray(start).toString());
return ret;
};

// 向buffer中写入数据
// value 必须是指定进制的整数
// offset从哪个字节开始写入
// LE 表示按小端字节序写入 1 => [01,00,00,00]
// LB 表示按大端字节序写入 1 => [00,00,00,01]
buf.writeInt32LE(value,offset);

// 从buffer中读取数据
// offset 表示从哪个位置开始读取
// 注意: 不能设置读取长度,他读取的长度是固定的,4个字节32位
buf.readInt32LE(offset)

Event Loop

Node.js 的事件循环基于 libuv,用于管理异步操作。事件循环分为多个阶段,每个阶段负责处理特定类型的回调。

1. Timers 阶段

  • 作用:处理由 setTimeout()setInterval() 创建的回调。
  • 行为
    • 检查到期的定时器,并执行其回调。
    • 如果没有到期的定时器,则直接跳到下一个阶段。

2. I/O Callbacks 阶段

  • 作用:处理几乎所有延迟的 I/O 回调(除了 close 事件、setImmediate 和定时器回调)。
  • 行为
    • 处理如网络请求错误等 I/O 回调。
    • 较少用在用户代码中,因为大多数 I/O 会在 Poll 阶段完成。

3. Idle, Prepare 阶段

  • 作用:供内部使用,用于准备事件循环和一些内部操作。
  • 行为
    • 通常不处理用户定义的回调。
    • 主要为 Node.js 引擎的内部运行服务。

4. Poll 阶段

  • 作用:处理新到达的 I/O 事件并执行相关回调。
  • 行为
    • 如果没有到期的定时器,这个阶段会阻塞,等待新的 I/O 事件。
    • 如果有定时器到期,会直接跳到 Timers 阶段。

5. Check 阶段

  • 作用:执行 setImmediate() 的回调。
  • 行为
    • 优先于下一次 Poll 阶段任务执行。
    • Poll 阶段结束后立即执行。

6. Close Callbacks 阶段

  • 作用:处理关闭事件的回调。
  • 行为
    • 例如处理 socket.on('close')fs.close() 的回调。
    • 用于释放资源时执行相关任务。

特殊任务:微任务队列(Microtask Queue)

  • 并不属于事件循环的某个阶段,而是会在每个阶段完成后执行。

  • 包括以下两种任务:

    • process.nextTick() 回调。
    • Promise.then().catch().finally()
  • 微任务优先于下一个事件循环阶段执行。

  • 例如,在 Timers 阶段执行完所有回调后,会立即执行微任务队列中的任务。

1
2
3
4
5
6
7
8
9
10
// setTimeout  setImmediate 执行时机并不确定
setTimeout(() => console.log("Timers"), 0);
setImmediate(() => console.log("Check"));
Promise.resolve().then(() => console.log("Microtask"));
process.nextTick(() => console.log("Next Tick"));

// 1. Next Tick
// 2. Microtask
// 3. Timers
// 4. Check
1
2
3
4
5
6
fs.readFileSync(() => {
setTimeout(() => console.log("Timers"), 0);
// setImmediate 永远先执行,因为 fs队列是在poll中
// 下一步会执行 check 队列中的 setImmediate
setImmediate(() => console.log("Check"));
});

模拟一个模块加载函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
const path = require("path");
const fs = require("fs");
const vm = require("vm");

class Module {
// 模块的导出对象,类似于 CommonJS 中的 `module.exports`
exports = {};

// 模块缓存,用于存储已加载的模块,避免重复加载
static cache = {};

// 文件扩展名及对应的加载器方法
static extensions = {
".js": (module) => {
// 读取 JavaScript 文件内容
let content = fs.readFileSync(module.id, "utf-8");

// 将文件内容包装成 CommonJS 模块的函数形式
content = Module.wrap(content);

// 将包装后的代码编译为可执行的函数
const script = new vm.Script(content);
const fn = script.runInThisContext();

// 调用包装函数,将 `exports`、`require` 等注入到模块上下文
fn(module.exports, myRequire, module, module.id, path.dirname(module.id));
},
".json": (module) => {
// 读取 JSON 文件内容
const content = fs.readFileSync(module.id, "utf-8");

// 将 JSON 内容解析为对象,并赋值给模块的 `exports`
module.exports = JSON.parse(content);
},
};

constructor(id) {
// 模块的唯一标识符,通常为文件的绝对路径
this.id = id;
}

// 包装模块代码,模拟 CommonJS 的运行环境
static wrap(content) {
return `(function(exports, require, module, __filename, __dirname) { ${content} })`;
}

// 解析文件名,包括支持自动添加扩展名
static resolveFileName(filePath) {
// 将文件路径转换为绝对路径
const absolutePath = path.resolve(filePath);

// 检查文件是否存在
if (fs.existsSync(absolutePath)) {
return absolutePath;
}

// 检查支持的文件扩展名
for (const ext of Object.keys(Module.extensions)) {
const fullPath = absolutePath + ext;
if (fs.existsSync(fullPath)) {
return fullPath;
}
}

// 如果文件未找到,抛出错误
throw new Error(`无法找到模块 '${filePath}'`);
}

// 加载模块,根据扩展名选择合适的加载器
load() {
const ext = path.extname(this.id); // 获取文件扩展名
const loader = Module.extensions[ext]; // 根据扩展名找到对应的加载器

if (loader) {
loader(this); // 执行加载器
} else {
throw new Error(`不支持的文件扩展名 '${ext}'`);
}
}
}

// 自定义的 `require` 方法,用于加载模块
function myRequire(filePath) {
// 解析文件名,确保获取的是有效路径
const resolvedPath = Module.resolveFileName(filePath);

// 如果模块已缓存,直接返回缓存中的 `exports`
if (Module.cache[resolvedPath]) {
return Module.cache[resolvedPath].exports;
}

// 创建一个新的模块实例
const module = new Module(resolvedPath);

// 将模块实例存入缓存
Module.cache[resolvedPath] = module;

// 加载模块内容
module.load();

// 返回模块的导出对象
return module.exports;
}

// 示例:加载一个模块
const res = myRequire("./path");

模拟一个 WriteStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
const fs = require("fs");
const EventEmitter = require("events");

function createWriteStream(path, options = {}) {
// 解构并设置默认参数
const {
flags = "w", // 文件打开模式(默认为写入模式)
encoding = "utf8", // 默认编码
autoClose = true, // 是否自动关闭文件描述符
emitClose = true, // 是否在关闭时触发 "close" 事件
start = 0, // 文件写入的起始位置
highWaterMark = 16 * 1024, // 写入的高水位标记(默认 16KB)
} = options;

// 内部状态变量
let offset = start; // 文件写入偏移量
let written = 0; // 当前已写入的字节数
let writing = false; // 是否正在写入
let cache = []; // 缓存区,用于存储等待写入的数据

// 定义一个可写流类
class WriteStream extends EventEmitter {
fd = null; // 文件描述符

// 打开文件
open() {
fs.open(path, flags, (err, fd) => {
if (err) {
this.emit("error", err); // 打开文件失败,触发错误事件
if (autoClose) {
this.destroy(); // 发生错误时自动关闭
}
return;
}
this.fd = fd; // 保存文件描述符
this.emit("open", fd); // 触发 open 事件
});
}

// 写入数据
write(chunk, encoding = "utf8", callback = () => {}) {
const buffer = Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk, encoding);

const canWrite = written < highWaterMark; // 判断当前写入是否超出高水位标记

// writeStream 首次会直接写入数据,后面的数据会写入缓存
if (writing) {
// 如果正在写入,缓存数据并延迟写入
cache.push({ buffer, callback });
} else {
writing = true;
this._write(buffer, callback);
}

return canWrite; // 返回是否可以继续写入
}

// 实际写入操作
_write(buffer, callback) {
if (this.fd === null) {
// 如果文件描述符尚未打开,等待 open 事件后再写入
return this.once("open", () => this._write(buffer, callback));
}

fs.write(
this.fd,
buffer,
0,
buffer.length,
offset,
(err, bytesWritten) => {
if (err) {
this.emit("error", err); // 写入失败,触发错误事件
if (autoClose) {
this.destroy();
}
return;
}

offset += bytesWritten; // 更新写入偏移量
written -= bytesWritten; // 更新已写入的字节数

// 检查缓存区中是否还有待写入的数据
const next = cache.shift();
if (next) {
this._write(next.buffer, next.callback);
} else {
writing = false; // 当前写入完成
callback(); // 执行回调
if (written < highWaterMark) {
this.emit("drain"); // 缓存区清空,触发 drain 事件
}
}
}
);
}

// 销毁流(关闭文件描述符)
destroy() {
if (this.fd !== null) {
fs.close(this.fd, (err) => {
if (emitClose) {
this.emit("close"); // 触发 close 事件
}
});
this.fd = null;
}
}
}

// 创建流实例并打开文件
const stream = new WriteStream();
stream.open();

return stream;
}

// 使用示例
const writeStream = createWriteStream("./package.txt");

// 监听事件
writeStream.on("open", (fd) => console.log("文件已打开,描述符:", fd));
writeStream.on("error", (err) => console.error("发生错误:", err));
writeStream.on("drain", () => console.log("缓存已清空,可以继续写入"));

// 写入数据
writeStream.write("Hello, World!", "utf-8", (err) => {
if (err) console.error("写入失败:", err);
else console.log("写入成功");
});

writeStream.write("Another line.", "utf-8", (err) => {
if (err) console.error("写入失败:", err);
else console.log("写入成功");
});

模拟 TCP 粘包的解决方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// util.js
const Buffer = require("buffer").Buffer;

// 获取数据包长度
const getLength = (chunk) => {
if (chunk.length < 12) return 0; // 不满足最小长度的包直接返回 0
return 12 + chunk.readInt32LE(4); // 长度字段存储在偏移量为 4 的位置
};

// 编码消息为特定格式的 Buffer
const encode = (string, id = 1) => {
const strBuffer = Buffer.from(string); // 将字符串转为 Buffer
const headerBuffer = Buffer.allocUnsafe(12); // 创建 12 字节的头部
headerBuffer.writeInt32LE(id, 0); // 写入消息 ID (4 字节)
headerBuffer.writeInt32LE(strBuffer.length, 4); // 写入消息长度 (4 字节)
return Buffer.concat([headerBuffer, strBuffer]); // 合并头部和消息
};

// 解码 Buffer 为字符串
const decode = (chunk) => {
const bodyLength = chunk.readInt32LE(4); // 获取消息体的长度
return chunk.subarray(12, 12 + bodyLength).toString(); // 提取消息体并转为字符串
};

module.exports = { getLength, encode, decode };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// server.js
const net = require("net");
const { getLength, encode, decode } = require("./utils");

// 创建服务端
const server = net.createServer();

server.listen(8080, "127.0.0.1", () => {
console.log("服务端启动,监听端口 8080");
});

server.on("connection", (socket) => {
console.log("客户端连接成功");

let unReadChunks = Buffer.alloc(0); // 存储未处理的缓冲区数据

socket.on("data", (chunk) => {
unReadChunks = Buffer.concat([unReadChunks, chunk]); // 拼接收到的数据
let len;

// 循环处理完整的数据包
while ((len = getLength(unReadChunks))) {
const cur = unReadChunks.subarray(0, len); // 提取当前完整数据包
unReadChunks = unReadChunks.subarray(len); // 更新未处理数据

// 解码消息,生成回复
const response = encode(decode(cur) + " 服务端恢复", cur.readInt32LE(0));
socket.write(response); // 发送回复消息
}
});

socket.on("close", () => console.log("客户端连接关闭"));
socket.on("error", (err) => console.error("服务端错误:", err));
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// client.js
const net = require("net");
const { getLength, encode, decode } = require("./utils");

// 创建客户端
const client = net.createConnection(8080, "127.0.0.1", () => {
console.log("客户端连接成功");

// 发送多条消息
client.write(encode("消息1"));
client.write(encode("消息2"));
client.write(encode("消息3"));
client.write(encode("消息4"));
client.write(encode("消息5"));
});

let unReadChunks = Buffer.alloc(0); // 存储未处理的缓冲区数据

client.on("data", (chunk) => {
unReadChunks = Buffer.concat([unReadChunks, chunk]); // 拼接收到的数据
let len;

// 循环处理完整的数据包
while ((len = getLength(unReadChunks))) {
const cur = unReadChunks.subarray(0, len); // 提取当前完整数据包
unReadChunks = unReadChunks.subarray(len); // 更新未处理数据

// 解码消息并输出
console.log("收到服务端回复:", decode(cur));
}
});

client.on("close", () => console.log("客户端连接关闭"));
client.on("error", (err) => console.error("客户端错误:", err));

链表

单向链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
class CreateNode {
constructor(value) {
this.value = value;
this.next = null;
}
}

class LinkedList {
constructor() {
// 内部使用头节点便于控制
this._head = { next: null };
// 对外头节点
this.count = 0;
}
push(node) {
let cur = this._head;
// 找到链表中的最后一个
while (cur.next !== null) {
cur = cur.next;
}
this.count += 1;
cur.next = node;
}
// 把指定位置元素移除
removeAt(index) {
if (index < 0 || index >= this.count) return undefined;
let cur = this._head;
let res = null;
let count = 0;
// 拿到目标节点的前一个节点
while (index !== count) {
cur = cur.next;
count++;
}
// 保存目标节点
res = cur.next;
// 拼接后面的节点
cur.next = cur.next.next;
this.count -= 1;

return res;
}
// 查找指定位置元素
getItem(index) {
let count = 0;
if (index < 0 || index >= this.count) return undefined;
let cur = this._head;
while (count !== index) {
cur = cur.next;
count++;
}
return cur.next;
}
// 在任意位置插入元素
insert(index, node) {
// 边界处理,因为可以在最有一个节点之后插入,所以不需要判断index和长度项等的情况
if (index < 0 || index > this.count) return;
//插入元素是在指定位置元素的前面插入
let cur = this._head;
let count = 0;
// 找到插入位置的节点,把新节点链接到当前节点
while (index !== count) {
cur = cur.next;
count += 1;
}
// 保存下一个节点
const next = cur.next;
cur.next = node;
node.next = next;
this.count += 1;

}
// 返回一个元素的位置
indexOf(node) {
let count = 0;
let cur = this.head;
while (cur !== null) {
if (node === cur) return count;
cur = cur.next;
count += 1;
}
return -1;
}
// 移除某个元素
remove(node) {
const index = this.insert(node);
return this.removeAt(index);
}
// 获取头节点
getHead() {
return this._head.next;
}
}

const list = new LinkedList();
list.push(new CreateNode(1))
list.push(new CreateNode(2))
list.push(new CreateNode(3))
list.insert(3, new CreateNode(4))
console.log(list.getHead())

双向链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
class CreateNode {
constructor(value) {
this.value = value;
this.next = null;
this.prev = null;
}
}

class DoublyLinkedList {
constructor() {
// 添加头节点信息
this._head = {
next: null,
prev: null
};
this.count = 0;
}
push(node) {
let cur = this._head;
// 找到链表中的最后一个
while (cur.next !== null) {
cur = cur.next;
}
// 没有初始化头节点的时候不需要指明prev
if (this._head.next !== null) {
node.prev = cur;
}
cur.next = node;
this.count += 1;
return this.count;
}
// 把指定位置元素移除
removeAt(index) {
if (index < 0 || index >= this.count) return undefined;
let cur = this._head;
let res = null;
let count = 0;
// 拿到目标节点的前一个节点
while (index !== count) {
cur = cur.next;
count++;
}
// 保存需要返回的目标节点
res = cur.next;
// 移除之后要修复后一个节点的prev指针
// 需要把修复prev的操作放在前面,因为下一步保存的时候仍然保留了,后一个节点的错误引用
cur.next.next.prev = cur
// 拼接后面的节点
cur.next = cur.next.next;
this.count -= 1;

return res;
}
// 查找指定位置元素
getItem(index) {
let count = 0;
if (index < 0 || index >= this.count) return undefined;
let cur = this._head;
while (count !== index) {
cur = cur.next;
count++;
}
return cur.next;
}
// 在任意位置插入元素
insert(index, node) {
// 边界处理,因为可以在最有一个节点之后插入,所以不需要判断index和长度项等的情况
if (index < 0 || index > this.count) return;
//插入元素是在指定位置元素的前面插入
let cur = this._head;
let count = 0;
// 找到插入位置的节点,把新节点链接到当前节点
while (index !== count) {
cur = cur.next;
count += 1;
}
// 对节点的引用应该是先修复指针,在赋值
// 保存下一个节点
const next = cur.next;
// 下一个节点的prev;
next.prev = node;
// 新节点的next
node.next = next;
// 新节点的prev
node.prev = cur;
// 前一个节点的next
cur.next = node;
this.count += 1;
}
// 返回一个元素的位置
indexOf(node) {
let count = 0;
let cur = this.head;
while (cur !== null) {
if (node === cur) return count;
cur = cur.next;
count += 1;
}
return -1;
}
// 移除某个元素
remove(node) {
const index = this.insert(node);
return this.removeAt(index);
}
// 获取头节点
getHead() {
return this._head.next;
}
}

循环链表

和前面两种链表相比,循环链表需要拿到,返回的那个头节点,用于判断是否是最后一个节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
class CreateNode {
constructor(value) {
this.value = value;
this.next = null;
}
}
class CircularLinkedList {
constructor() {
this._head = {
next: null,
};
this.count = 0;
}
push(node) {
let cur = null;
// 没有初始化头节点则
if (this.getHead() == null) {
cur = this._head;
// 最有一个节点指向自己
node.next = node;
} else {
// 如果已经有了头节点,保存头节点用于修复指针
const head = this.getHead();
cur = head;
while (cur.next !== head) {
cur = cur.next;
}
node.next = head;
}
cur.next = node;
this.count += 1;
return this.count;
}
// 把指定位置元素移除
removeAt(index) {
if (index < 0 || index >= this.count) return undefined;
// 保存下真实的头节点
const head = this.getHead()
let cur = this._head;
let res = null;
let count = 0;
// 拿到目标节点的前一个节点
while (index !== count) {
cur = cur.next;
count++;
}

// 保存需要返回的目标节点
res = cur.next;
// 如果是头节点
if (index === 0) {
// 拿到最后一个节点,修复next指针
const tail = this.getItem(this.count - 1);
tail.next = cur.next.next;
cur.next = cur.next.next;
}
// 如果被移除的节点是最后一个节点
else if (index === this.count - 1) {
cur.next = head;
} else {
cur.next = cur.next.next;
}
this.count -= 1;

return res;
}
// 查找指定位置元素
getItem(index) {
let count = 0;
if (index < 0 || index >= this.count) return undefined;
let cur = this._head;
while (count !== index) {
cur = cur.next;
count++;
}
return cur.next;
}
// 在任意位置插入元素
insert(index, node) {
// 边界处理,因为可以在最有一个节点之后插入,所以不需要判断index和长度项等的情况
if (index < 0 || index > this.count) return;
//插入元素是在指定位置元素的前面插入
const head = this.getHead();
let cur = this._head;
let count = 0;
// 找到插入位置的节点,把新节点链接到当前节点
while (index !== count) {
cur = cur.next;
count += 1;
}
// 末尾插入要修复next
if (index === this.count - 1) {
node.next = head;
}
// 头部插入
if (index === 0) {
const tail = this.getItem(this.count - 1);
tail.next = node;
}
const next = cur.next;
cur.next = node;
node.next = next;
this.count += 1;
}
// 返回一个元素的位置
indexOf(node) {
let count = 0;
let cur = this.head;
while (count < this.count) {
if (node === cur) return count;
cur = cur.next;
count += 1;
}
return -1;
}
// 移除某个元素
remove(node) {
const index = this.insert(node);
return this.removeAt(index);
}
// 获取头节点
getHead() {
return this._head.next;
}
}

配置优化

HMR 模块热替换

当一个模块改变时避免所有的模块都被重新编译一次,应该只更新修改的模块

通过 hot:true 开启hmr,此时样式文件(.css .scss) 可以进行热模块替换,style-loader内部的实现,但是js文件没有开启热替换,而且html的文件也不能更新了

因为热替换阻止了刷新,通过修改webpack.config.js 入口配置entry: ['./src/index.js','./src/index.html'],,可以重新开启index.html的刷新功能

另外 html文件不需要热替换的功能,因为每个入口只对应一个文件,一定要重新加载

.js文件的热替换不能是入口文件

1
2
3
4
5
6
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
printMe();
})
}
source-map

提供源代码到编译后代码映射的方案,可以追踪源代码的位置 通过devtool配置

[inline-|hidden-|eval][nosources-][cheap-[module-]]source-map

inline 构建速度快

source-map 能提示错误代码准确信息,和源代码中准确位置,会单独生成一个文件

inline-source-map source-map会内嵌到生成的js文件中,只生成一个内联的source-map ,能提示错误代码准确信息,和源代码中准确位置

eval-source-map source-map会内嵌到生成的js文件中,每个文件都会生成对应的source-map,可以提示错误原因,但不能追踪到源代码位置,只会定位到编译后的错误位置

hidden-source-map source-map文件会单独生成,可以提示错误原因,但不能追踪到源代码位置,只会定位到编译后的错误位置

cheap-source-map 在外部单独生成,只能提示到行,如果代码在一行中,不能准确的定位

cheap-module-source-map 在外部单独生成 与 cheap-source-map 类似,会将loader的source-map加入

nosources-source-map 可以提供作物信息,但是不能定位到错误位置,源代码和编译后代码都不可以

数度快慢: eval=> inline => cheap

开发环境:cheap-source-map
速度快
eval-cheap-source-map
eval-source-map 🆚
调试友好
source-map
cheap-module-source-map
cheap-source-map

生产环境
简单调试,
内联会让体积变大
nosources-source-map 全部隐藏代码,在线上环境使用🆚
hidden-source-map 只隐藏源代码

source-map 🆚,单独生成文件且便于调试

oneOf

用于提升构建的速度,只要有一个loader匹配到就不会继续匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
module: {
rules: [
{
//...
// 指定优先级,都会先被这个loader处理
enforce:'pre'
},
{
oneOf:[
// 其他的loader
]
}
]
}
}

缓存

在编译文件的时候,如果依赖文件没有改变,则直接使用编译好的缓存文件,无需所有文件都重新编译

1
2
3
4
5
6
7
8
9
{
test: /\.js$/i,
use: [{
loader: 'babel-loader',
options:{
cacheDirectory:true
}
}],
},

文件资源的缓存

  • hash 如果都使用hash的话,即每次修改任何一个文件,所有文件名的hash至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效.

  • chunkhash chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。动态import也受chunkhash的影响.

因为我们是将样式作为模块import到JavaScript文件中的,所以它们的chunkhash是一致的,这样就会有个问题,只要对应css或则js改变,与其关联的文件hash值也会改变,但其内容并没有改变呢,所以没有达到缓存意义。固contenthash的用途随之而来。

  • contenthash是针对文件内容级别的,只有你自己模块的内容变了,那么hash值才改变

tree-shaking

使用es6模块化规范,开启production模式,webpack会自动启用tree-shaking

webpack.config.js 中添加sideEffects:false表示所有的代码都没有副作用,如果标记为false, 全局引入的文件(polyfile),或没有通过模块化使用的css,都会被删除

可以通过一个数组来标记不需要处理的资源 sideEffects:['*.css']

代码分割 code-split

生成chunk的几种方式

  • 多页面entry生成多个chunk
  • 异步组件生成chunk
  • code split
1
2
3
4
5
6
7
// 将node-modules 中代码单独打包成
// 分析多入口文件中有没有公共的依赖,会把依赖单独打包
optimization: {
splitChunks: {
chunks: 'all'
}
},

懒加载 预加载

dynamic-imports

babel-loader会自动处理 dynamic-imports语法, 如果eslint提示错误,在.eslintrc中添加"parser": "babel-eslint"

PWA

work-box -> workbox-webpack-plugin

1
yarn add -D workbox-webpack-plugin

webpack.config.js 中添加插件和配置项

1
2
3
4
5
6
7
8
9
10
11
{
plugins:[
//...
new GenerateSW({
// 帮助serviceWork快速启动,
// 删除旧的serverwork
clientsClaim:true,
skipWaiting:true
})
]
}

注册servicework

在入口文件index.js

1
2
3
4
5
6
7
8
9
10
11
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./service-worker.js')
.then(() => {
console.log('注册成功');
})
.catch(() => {
console.log('注册失败');
});
});
}

如果eslint不支持全局变量,在.eslinrc添加{env:browser: true,}

多进程打包

thread-loader

进程的启用会占用时间(大约600ms),只有复杂的任务处理的时候才会有明显的效果

externals 忽略某些资源

webpack.config.js中添加externals

1
2
3
4
5
6
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};

和dll的区别是,externals并没有打包文件,需要通过cdn的方式引入进来,dll只是把指定的包单独打包,并通过插件把单独打包的文件重新引入

dll

对第三方的库,进行单独打包 webpack5不适用

通过两份配置,可以避免每次对第三方的库重新打包

webpack.dll.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const path = require('path');
const webpack = require('webpack');

module.exports = {
entry:{
lodash:['lodash'],
jquery:['jquery'],
moment:["moment"]
},
output:{
// 生成文件的名称
filename:'[name]_[contenthash:8].js',
path:path.resolve(__dirname,'dll'),
// 单独打包的库对外暴露的名称
library: "[name]_[fullhash]"
},
plugins:[
new webpack.DllPlugin({
context: __dirname,
// 映射单独打包的库的名称
name: '[name]_[fullhash]',
// 生成的manifest文件
path:path.resolve(__dirname,'dll/[name]_manifest.json')
})
],
mode:'production'
}

webpack.config.js

使用 add-asset-html-webpack-plugin把单独打包的资源引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.export = {
plugins:[
new webpack.DllReferencePlugin({
// 存在于manifest文件中的包,不会被打包
manifest: require(path.resolve(__dirname,'dll/moment_manifest.json'))
}),
new webpack.DllReferencePlugin({
manifest: require(path.resolve(__dirname,'dll/lodash_manifest.json'))
}),
new webpack.DllReferencePlugin({
manifest: require(path.resolve(__dirname,'dll/jquery_manifest.json'))
}),
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, './dll/*.js'),
})
]
}

optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
optimization: {
minimize: true,
// https://webpack.docschina.org/plugins/terser-webpack-plugin/
minimizer: [new TerserPlugin({
cache:true,// 开启缓存
parallel:true,//多进程打包
sourceMap:true //启用source-map
})],
splitChunks: {
// async表示只从异步加载得模块(动态加载import())里面进行拆分
// initial表示只从入口模块进行拆分
// all表示以上两者都包括
chunks: 'async', //all
minSize: 1024 * 30 , //分割chunk的最小大小,大于30kb才会被提取
minRemainingSize: 0,
maxSize: 0,// 没有最大限制
minChunks: 1, // 至少被引用一次
// 按需加载的时候并行加载文件的最大个数
// 可以理解为针对一个模块拆分后的个数,包括模块本身
// import()文件本身算一个请求
// 并不算js以外的公共资源请求比如css
// 如果同时有两个模块满足cacheGroup的规则要进行拆分,但是maxInitialRequests的值只能允许再拆分一个模块,那尺寸更大的模块会被拆分出来
// 一个按需加载的包最终被拆分成 n 个包,maxAsyncRequests 就是用来限制 n 的最大值。
maxAsyncRequests: 30,
maxInitialRequests: 30, //入口js文件最大请求数量
enforceSizeThreshold: 50000,
name: true, // 可以使用命名规则
cacheGroups: {
// node_module中的包会被引入到defaultVendors包中
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
// 如果当前打包的模块和之前的是同一个就会复用,而不是重复打包
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
// 将当前模块中记录的其他模块的hash单独打包成一个文件
// 因为如果main.js 引用 a.js,main.js中会保存a.js打包时生成的cotenthash
// 如果a.js发生改变则contenthash发生改变,那么main.js的contenthash也会改变
// 代码分割的时候一定要加
runtimeChunk:()=> {
name:entrypoint=>`runtime-${entrypoint.name}`
}
},

},
}

LeetCode 填充每个节点的下一个右侧节点指针

填充每个节点的下一个右侧节点指针

思考

相同层级的操作可以想到的是层序遍,但是题目中有一个问题没有说清楚,下一个右侧节点表示紧邻的右侧节点,空节点也算是一个节点,换句话说空节点不能跳过

在思考一下其中的细节,需要清楚一层有多少个节点,这样才能准确遍历到一层的末尾, 链接的时候空的节点不能跳过,因为这也是合法的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const connect = (root) => {
if (root === null) return null;

const queue = [root];
while (queue.length) {
// 记录每一层的长度
let len = queue.length;

while (len--) {
const node = queue.shift();
if (len && node) node.next = queue[0];

// 不需要判断空节点
if (node) queue.push(node.left, node.right);
}
}
return root;
};

这个问题也可以用,递归来解决,因为它可以抽象成更小的子问题,也就是链接两个节点,需要知道的是哪些节点是需要链接的。

如果盲目的套用递归框架就会困惑,在另外一个子树的节点,怎么能获取到呢。

1
2
3
4
5
6
7
8
9
const connect = (root) => {
if (root === null) return root;

if (root.left) root.next = root.right;

// 无法获取另外子树的节点

return root;
};

所以需要指明,那两个节点需要链接,这也是一个先序遍历的模型,但是子问题需要传递更多的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const dig = (left, right) => {
// 链接两个节点
if (left) left.next = right;
if (left) dig(left.left, left.right);
if (right) dig(right.left, right.right);
// 不同子树中需要连接的节点
if (left && right) dig(left.right, right.left);
};

const connect = (root) => {
if (root === null) return root;
dig(root.left, root.right);
return root;
};
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信