Tech-Paul

work hard, play hard

What’s Fiber?

React 的基本组成:当我们写 React 组件并使用 JSX 时,React 在底层会将 JSX 转换为元素的对象结构。例如:

1
const element = <div>Hello, world!</div>

React 会将其转换为以下对象结构:

1
const element = React.createElement("h1", null, "Hello, world")

为了将这个元素渲染到 DOM 上,React 需要创建一种内部实例,用来追踪该组件的所有信息和状态。在早期版本的 React 中,我们称之为“实例”或“虚拟 DOM 对象”。但在 Fiber 架构中,这个新的工作单元就叫做 Fiber。
所以,在本质上,Fiber 是一个 JavaScript 对象,代表 React 的一个工作单元,它包含了与组件相关的信息。一个简化的 Fiber 对象长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fiber = {
type: "div",
props: {
children: "Hello, world",
},
child: null,
sibling: null,
return: null,
stateNode: null,
alternate: null,
pendingProps: null,
memoizedProps: null,
memoizedState: null,
dependencies: null,
}

当 React 开始工作时,它会沿着 Fiber 树形结构进行,试图完成每个 Fiber 的工作(例如,比较新旧 props,确定是否需要更新组件等)。如果主线程有更重要的工作(例如,响应用户输入),则 React 可以中断当前工作并返回执行主线程上的任务。

因此,Fiber 不仅仅是代表组件的一个内部对象,它还是 React 的调度和更新机制的核心组成部分。

Why do we need Fiber?

在 React 16 之前的版本中,是使用递归的方式处理组件树更新,称为堆栈调和(Stack Reconciliation),这种方法一旦开始就不能中断,直到整个组件树都被遍历完。这种机制在处理大量数据或复杂视图时可能导致主线程被阻塞,从而使应用无法及时响应用户的输入或其他高优先级任务。

Fiber 的引入改变了这一情况。Fiber 可以理解为是 React 自定义的一个带有链接关系的 DOM 树,每个 Fiber 都代表了一个工作单元,React 可以在处理任何 Fiber 之前判断是否有足够的时间完成该工作,并在必要时中断和恢复工作。

Fiber structure

源码里 FiberNode 的结构:

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
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode
) {
// 基本属性
this.tag = tag // 描述此Fiber的启动模式的值(LegacyRoot = 0; ConcurrentRoot = 1)
this.key = key // React key
this.elementType = null // 描述React元素的类型。例如,对于JSX<App />,elementType是App
this.type = null // 组件类型
this.stateNode = null // 对于类组件,这是类的实例;对于DOM元素,它是对应的DOM节点。

// Fiber链接
this.return = null // 指向父Fiber
this.child = null // 指向第一个子Fiber
this.sibling = null // 指向其兄弟Fiber
this.index = 0 // 子Fiber中的索引位置

this.ref = null // 如果组件上有ref属性,则该属性指向它
this.refCleanup = null // 如果组件上的ref属性在更新中被删除或更改,此字段会用于追踪需要清理的旧ref

// Props & State
this.pendingProps = pendingProps // 正在等待处理的新props
this.memoizedProps = null // 上一次渲染时的props
this.updateQueue = null // 一个队列,包含了该Fiber上的状态更新和副作用
this.memoizedState = null // 上一次渲染时的state
this.dependencies = null // 该Fiber订阅的上下文或其他资源的描述

// 工作模式
this.mode = mode // 描述Fiber工作模式的标志(例如Concurrent模式、Blocking模式等)。

// Effects
this.flags = NoFlags // 描述该Fiber发生的副作用的标志(十六进制的标识)
this.subtreeFlags = NoFlags // 描述该Fiber子树中发生的副作用的标志(十六进制的标识)
this.deletions = null // 在commit阶段要删除的子Fiber数组

this.lanes = NoLanes // 与React的并发模式有关的调度概念。
this.childLanes = NoLanes // 与React的并发模式有关的调度概念。

this.alternate = null // Current Tree和Work-in-progress (WIP) Tree的互相指向对方tree里的对应单元

// 如果启用了性能分析
if (enableProfilerTimer) {
// ……
}

// 开发模式中
if (__DEV__) {
// ……
}
}

其实可以理解为是一个更强大的虚拟 DOM。

Fiber 工作原理

Fiber 工作原理中最核心的点就是:可以中断和恢复,这个特性增强了 React 的并发性和响应性。

实现可中断和恢复的原因就在于:Fiber 的数据结构里提供的信息让 React 可以追踪工作进度、管理调度和同步更新到 DOM

Fiber 工作原理中的几个关键点:

  • 单元工作:每个 Fiber 节点代表一个单元,所有 Fiber 节点共同组成一个 Fiber 链表树(有链接属性,同时又有树的结构),这种结构让 React 可以细粒度控制节点的行为。

  • 链接属性:child、sibling 和 return 字段构成了 Fiber 之间的链接关系,使 React 能够遍历组件树并知道从哪里开始、继续或停止工作。
    fiber work

  • 双缓冲技术: React 在更新时,会根据现有的 Fiber 树(Current Tree)创建一个新的临时树(Work-in-progress (WIP) Tree),WIP-Tree 包含了当前更新受影响的最高节点直至其所有子孙节点。Current Tree 是当前显示在页面上的视图,WIP-Tree 则是在后台进行更新,WIP-Tree 更新完成后会复制其它节点,并最终替换掉 Current Tree,成为新的 Current Tree。因为 React 在更新时总是维护了两个 Fiber 树,所以可以随时进行比较、中断或恢复等操作,而且这种机制让 React 能够同时具备拥有优秀的渲染性能和 UI 的稳定性。

  • State 和 Props:memoizedProps、pendingProps 和 memoizedState 字段让 React 知道组件的上一个状态和即将应用的状态。通过比较这些值,React 可以决定组件是否需要更新,从而避免不必要的渲染,提高性能。

  • 副作用的追踪:flags 和 subtreeFlags 字段标识 Fiber 及其子树中需要执行的副作用,例如 DOM 更新、生命周期方法调用等。React 会积累这些副作用,然后在 Commit 阶段一次性执行,从而提高效率。

Fiber 工作流程

  1. 初始化:React 会创建一个 Fiber 树,其中包含了所有的组件节点。
  2. 构建:React 会遍历 Fiber 树,根据组件的生命周期方法和 props 等信息,构建出对应的 Fiber 节点。
  3. 调度:React 会根据优先级和时间等因素,将 Fiber 节点分配给不同的工作线程。
  4. 执行:工作线程会从 Fiber 树中取出需要执行的 Fiber 节点,并执行其对应的生命周期方法和 props 等信息。
  5. 渲染:工作线程会将执行结果渲染到 DOM 上。
  6. 提交:React 会将 DOM 上的变化提交到浏览器中,触发浏览器的重新渲染。
  7. 循环:React 会重复上述步骤,直到整个 Fiber 树都被遍历完。

第一阶段:Reconciliation(调和)

  • 目标: 确定哪些部分的 UI 需要更新。
  • 原理: 这是 React 构建工作进度树的阶段,会比较新的 props 和旧的 Fiber 树来确定哪些部分需要更新。
    1、遍历 Fiber 树:BeforeMutation
    2、构建更新队列:UpdateQueue
    3、构建副作用列表:CommitMutation
    4、处理 layout effects:commitLayout
    5、构建新的 Fiber 树:CommitLayout
    6、构建新的 Fiber 树:CommitRoot
    7、构建新的 Fiber 树:CommitWork
    调和阶段又分为三个小阶段:
    • 创建与标记更新节点:beginWork
      • 判断 Fiber 节点是否要更新
      • 判断 Fiber 子节点是更新还是复用
      • mountChildFibers 和 reconcileChildFibers 最终会进入同一个方法 createChildReconciler,执行 Fiber 节点的调和(处理诸如新的 Fiber 创建、旧 Fiber 删除或现有 Fiber 更新等操作)。而整个 beginWork 完成后,就会进入 completeWork 流程。
    • 收集副作用列表:completeUnitOfWork 和 completeWork - completeUnitOfWork 负责遍历 Fiber 节点,同时记录了有副作用节点的关系。
      bubbleProperties 为 completeWork 完成了两个工作:

记录 Fiber 的副作用标志
为子 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
// packages/react-reconciler/src/ReactFiberCompleteWork.js
// 以下只是核心逻辑的代码,不是bubbleProperties的完整源码
function bubbleProperties(completedWork: Fiber) {
const didBailout =
completedWork.alternate !== null &&
completedWork.alternate.child === completedWork.child // 当前的Fiber与其alternate(备用/上一次的Fiber)有相同的子节点,则跳过更新

let newChildLanes = NoLanes // 合并后的子Fiber的lanes
let subtreeFlags = NoFlags // 子树的flags。

if (!didBailout) {
// 没有bailout,需要冒泡子Fiber的属性到父Fiber
let child = completedWork.child
// 遍历子Fiber,并合并它们的lanes和flags
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
)

subtreeFlags |= child.subtreeFlags
subtreeFlags |= child.flags

child.return = completedWork // Fiber的return指向父Fiber,确保整个Fiber树的一致性
child = child.sibling
}
completedWork.subtreeFlags |= subtreeFlags // 合并所有flags(副作用)
} else {
// 有bailout,只冒泡那些具有“静态”生命周期的flags
let child = completedWork.child
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
)

subtreeFlags |= child.subtreeFlags & StaticMask // 不同
subtreeFlags |= child.flags & StaticMask // 不同

child.return = completedWork
child = child.sibling
}
completedWork.subtreeFlags |= subtreeFlags
}
completedWork.childLanes = newChildLanes // 获取所有子Fiber的lanes。
return didBailout
}

调和阶段知识拓展

1、为什么 Fiber 架构更快?
在上面这段代码里,我们还可以看出来为什么 Fiber 架构比以前的递归 DOM 计算要快:flags 或 subtreeFlags 是 16 进制的标识,在这里进行按位或(|)运算后,可以记录当前节点本身和子树的副作用类型,通过这个运算结果可以减少节点的遍历,从而提高性能。
2、调和过程可中断

前面我们提到,调和过程可以被中断,现在我们就看看源码里是怎么进行中断和恢复的。首先,我们要明确可中断的能力是 React 并发模式(Concurrent Mode)的核心,这种能力使得 React 可以优先处理高优先级的更新,而推迟低优先级的更新。

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
// packages/react-reconciler/src/ReactFiberWorkLoop.js
// 以下只是核心逻辑的代码,不是renderRootConcurrent的完整源码
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// 保存当前的执行上下文和 dispatcher
const prevExecutionContext = executionContext
executionContext |= RenderContext
const prevDispatcher = pushDispatcher(root.containerInfo)
const prevCacheDispatcher = pushCacheDispatcher()

if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// 如果当前的工作进度树与传入的 root 或 lanes 不匹配,我们需要为新的渲染任务准备一个新的堆栈。
// ……
}

// 持续的工作循环,除非中断发生,否则会一直尝试完成渲染工作
outer: do {
try {
if (
workInProgressSuspendedReason !== NotSuspended &&
workInProgress !== null
) {
// 如果当前的工作进度是由于某种原因而被挂起的,并且仍然有工作待处理,那么会处理它
const unitOfWork = workInProgress
const thrownValue = workInProgressThrownValue

// 根据不同挂起原因,进行中断、恢复等计算
resumeOrUnwind: switch (workInProgressSuspendedReason) {
case SuspendedOnError: {
// 如果工作因错误被挂起,那么工作会被中断,并从最后一个已知的稳定点继续
// ……省略逻辑
break
}
case SuspendedOnData: {
// 工作因等待数据(通常是一个异步请求的结果)而被挂起,
// ……省略逻辑
break outer
}
case SuspendedOnInstance: {
// 将挂起的原因更新为SuspendedOnInstanceAndReadyToContinue并中断工作循环,标记为稍后准备好继续执行
workInProgressSuspendedReason =
SuspendedOnInstanceAndReadyToContinue
break outer
}
case SuspendedAndReadyToContinue: {
// 表示之前的挂起工作现在已经准备好继续执行
if (isThenableResolved(thenable)) {
// 如果已解析,这意味着需要的数据现在已经可用
workInProgressSuspendedReason = NotSuspended
workInProgressThrownValue = null
replaySuspendedUnitOfWork(unitOfWork) // 恢复执行被挂起的工作
} else {
workInProgressSuspendedReason = NotSuspended
workInProgressThrownValue = null
throwAndUnwindWorkLoop(unitOfWork, thrownValue) // 继续循环
}
break
}
case SuspendedOnInstanceAndReadyToContinue: {
// ……省略部分逻辑
const isReady = preloadInstance(type, props)
if (isReady) {
// 实例已经准备好
workInProgressSuspendedReason = NotSuspended // 该fiber已完成,不需要再挂起
workInProgressThrownValue = null
const sibling = hostFiber.sibling
if (sibling !== null) {
workInProgress = sibling // 有兄弟节点,开始处理兄弟节点
} else {
// 没有兄弟节点,回到父节点
const returnFiber = hostFiber.return
if (returnFiber !== null) {
workInProgress = returnFiber
completeUnitOfWork(returnFiber) // 收集副作用,前面有详细介绍
} else {
workInProgress = null
}
}
break resumeOrUnwind
}
}
// 还有其它case
}
}

workLoopConcurrent() // 如果没有任何工作被挂起,那么就会继续处理工作循环。
break
} catch (thrownValue) {
handleThrow(root, thrownValue)
}
} while (true)

// 重置了之前保存的执行上下文和dispatcher,确保后续的代码不会受到这个函数的影响
resetContextDependencies()
popDispatcher(prevDispatcher)
popCacheDispatcher(prevCacheDispatcher)
executionContext = prevExecutionContext

// 检查调和是否已完成
if (workInProgress !== null) {
// 未完成
return RootInProgress // 返回一个状态值,表示还有未完成
} else {
// 已完成
workInProgressRoot = null // 重置root
workInProgressRootRenderLanes = NoLanes // 重置Lane
finishQueueingConcurrentUpdates() // 处理队列中的并发更新
return workInProgressRootExitStatus // 返回当前渲染root的最终退出状态
}
}

第二阶段:Commit(提交)

  • 目标: 更新 DOM 并执行任何副作用。

  • 原理: 遍历在 Reconciliation 阶段创建的副作用列表进行更新。
    1、遍历副作用列表:BeforeMutation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // packages/react-reconciler/src/ReactFiberCommitWork.js
    // 以下只是核心逻辑的代码,不是commitBeforeMutationEffects的完整源码
    export function commitBeforeMutationEffects(
    root: FiberRoot,
    firstChild: Fiber
    ): boolean {
    nextEffect = firstChild // nextEffect是遍历此链表时的当前fiber
    commitBeforeMutationEffects_begin() // 遍历fiber,处理节点删除和确认节点在before mutation阶段是否有要处理的副作用

    const shouldFire = shouldFireAfterActiveInstanceBlur // 当一个焦点元素被删除或隐藏时,它会被设置为 true
    shouldFireAfterActiveInstanceBlur = false
    focusedInstanceHandle = null

    return shouldFire
    }

    2、正式提交:CommitMutation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // packages/react-reconciler/src/ReactFiberCommitWork.js
    // 以下只是核心逻辑的代码,不是commitMutationEffects的完整源码
    export function commitMutationEffects(
    root: FiberRoot,
    finishedWork: Fiber,
    committedLanes: Lanes
    ) {
    // lanes和root被设置为"in progress"状态,表示它们正在被处理
    inProgressLanes = committedLanes
    inProgressRoot = root

    // 递归遍历Fiber,更新副作用节点
    commitMutationEffectsOnFiber(finishedWork, root, committedLanes)

    // 重置进行中的lanes和root
    inProgressLanes = null
    inProgressRoot = null
    }

    3、处理 layout effects:commitLayout

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // packages/react-reconciler/src/ReactFiberCommitWork.js
    export function commitLayoutEffects(
    finishedWork: Fiber,
    root: FiberRoot,
    committedLanes: Lanes
    ): void {
    inProgressLanes = committedLanes
    inProgressRoot = root

    // 创建一个current指向就Fiber树的alternate
    const current = finishedWork.alternate
    // 处理那些由useLayoutEffect创建的layout effects
    commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes)

    inProgressLanes = null
    inProgressRoot = null
    }

    一旦进入提交阶段后,React 是无法中断的。
    react-fiber 参考链接

微前端架构 是前端开发中的一种设计模式,它借鉴了 微服务架构 的思想,用于构建可扩展、可维护的前端应用程序。它将单一的前端应用拆分为多个独立的、松散耦合的小型前端应用,每个应用可以独立开发、部署和维护。这种架构特别适合大型前端项目,能够提升开发效率和灵活性。

核心概念

  1. 独立性:每个微前端应用可以被认为是独立的前端模块,通常由不同的开发团队独立开发。这些应用可以有自己的技术栈、生命周期和路由,且可以独立于其他应用进行部署。

  2. 模块化:微前端通过拆分应用,促使模块化开发。每个模块(微前端)处理应用的一部分功能,例如不同的页面、业务逻辑等。

  3. 技术栈无关:每个微前端应用可以使用不同的技术栈来开发(React、Vue、Angular 等),只需遵循一定的接口协议进行通信和集成。这种松散耦合的设计方式使得新旧技术可以共存。

  4. 独立部署:每个微前端应用可以独立构建和部署。你可以在不影响其他部分的情况下更新某个微前端应用,降低了发布和更新的风险。

  5. 团队自治:由于微前端可以分割为多个小团队负责开发,这样的架构更利于大团队协作开发,不同团队可以并行开发各自的模块,互不干扰。

微前端架构的优势

  1. 独立开发和部署:各个团队可以独立开发并将微前端部分独立部署,而不会影响其他团队的开发工作和部署流程。

  2. 技术栈灵活:不同的团队可以自由选择自己擅长或适合的前端框架和工具,比如某个模块可以使用 React,另一个模块则可以使用 Vue。

  3. 渐进式迁移:微前端架构允许在同一个项目中逐步引入新技术,而不需要一次性重构整个项目。你可以逐步将旧技术替换为新技术。

  4. 提升应用可维护性:通过将大型单体应用拆分成多个小的微前端模块,代码更加模块化,维护和扩展变得更容易。

  5. 更快的开发周期:由于每个微前端都是独立的模块,开发团队可以并行工作,缩短整体项目的开发周期。

微前端架构的挑战

  1. 复杂的集成和通信:多个独立的微前端应用需要整合在一起,这涉及到应用间的通信、共享状态管理和 UI 协同等复杂问题。

  2. 性能问题:多个微前端应用需要加载不同的资源(JavaScript、CSS 等),如果没有优化得当,可能会导致性能问题。

  3. 样式隔离和冲突:每个微前端应用可能使用不同的样式,如何有效地避免样式冲突、保证样式隔离是一个挑战。通常可以使用 CSS Modules、Shadow DOM 或其他样式隔离技术。

  4. 公共依赖的管理:多个微前端应用可能会依赖同一个库(如 React 或 Lodash),如果不统一管理这些依赖,可能会导致冗余的依赖项加载,影响应用性能。

微前端的实现方式

  1. 基于 iframe:通过 iframe 来加载各个微前端应用,确保隔离性。iframe 天然具备隔离功能,但会带来性能问题和跨应用的通信复杂性。

  2. 基于 JavaScript 加载:通过动态加载 JavaScript 文件,将各个微前端应用组合在一起。这是较为灵活的一种方式,但需要处理应用之间的样式和状态隔离问题。

  3. 基于路由的方式:通过路由来加载不同的微前端应用。例如,在主应用中根据不同的 URL 加载不同的子应用。这种方式常用于单页面应用(SPA)的场景。

  4. Web Components:通过使用 Web Components 技术(如 Custom ElementsShadow DOM),实现各个微前端模块的隔离和复用。Web Components 自带的样式隔离特性使得这种方式较为流行。

常见的微前端架构框架和工具

  1. single-spa:一个流行的微前端框架,提供了将多个前端应用集成在一起的解决方案,支持不同框架共存(如 React、Vue、Angular 等)。

  2. Qiankun:基于 single-spa,由蚂蚁金服开发的微前端框架,专注于解决微前端的加载、路由、状态管理等问题,提供了更简单的 API 和集成方案。

  3. Module Federation:Webpack 5 提供的模块联邦功能允许多个应用共享代码和依赖,适合微前端架构中的模块复用。

微前端架构的应用场景

  • 大型企业项目:大型企业往往有多个业务模块或子系统,微前端架构允许不同业务模块独立开发和维护。
  • 渐进式重构:旧的单体应用可以逐步拆分为多个独立的微前端应用,实现技术栈的升级和架构的重构。
  • 团队分工明确的项目:多个团队可以并行开发,独立负责各自的微前端模块,提升开发效率。

总结

微前端架构通过将前端应用拆分为多个独立的模块,使得大型项目的开发、部署和维护变得更加灵活和高效。尽管微前端架构带来了诸多好处,但也需要面对模块间通信、样式隔离等技术挑战。对于大型复杂的前端项目,微前端架构是一种非常有价值的解决方案。

Package bug

1. 使用补丁工具

patch-package

  1. 安装:npm install patch-package postinstall
  2. 添加脚本:
1
2
3
4
5
6
{
"scripts": {
"postinstall": "patch-package"
}
}

  1. 修改 node_modules 中的代码
  2. 生成补丁文件
    修改完代码后,你需要使用 patch-package 生成一个补丁文件来记录你的修改。补丁文件会记录你对 node_modules 中依赖包的更改,并保存为 .patch 文件。
1
2
npx patch-package some-package

这将生成一个补丁文件,并将其保存在项目根目录下的 patches/ 文件夹中。补丁文件的命名格式通常是:+.patch

  1. 确保补丁文件生效
    在补丁文件生成后,你不需要做任何手动的应用操作,因为 postinstall 脚本已经配置好了,它会确保每次执行 npm install 时自动应用补丁。

生成新文件:<package-name>+<version>.patch,保存在patches目录下
在项目的package.json文件中,在scripts部分添加一个postinstall脚本,例如:"postinstall": "patch-package"。这样每次安装依赖时,都会自动应用补丁

2. 手动修改并使用本地版本

  1. 找到有问题的包在项目的node_modules目录中的位置。
  2. 复制该包的整个目录到项目中的其他位置,比如src/vendorPatched
  3. 对复制出来的版本进行手动修改以修复 bug。
  4. 在项目代码中,使用相对路径导入修改后的本地版本,而不是从node_modules中导入原始有问题的版本。例如,如果是一个 JavaScript 模块,可以使用import { someFunction } from '../vendorPatched/some-package/some-file.js';

3. 使用 fork 和替代源

如果 package 是开源的,可以在代码平台进行 fork

对 fork 后的项目进行 bug 修复,推送上去

在 package.json 中将有问题的 package 依赖地址指向 fork 后的地址,例如:

1
"some-package": "git+https://github.com/your-username/forked-package.git"

4. 通过脚本 copy 覆盖

还是用postinstall这个勾子,在这个勾子执行cp修改过的文件  ./node_modules/包名/原始文件拷贝过去,最终node_modules下的文件就变成了修改后的文件了,比如:

1
2
3
"scripts": {
"postinstall": "cp ./patches/upload/* ./node_modules/antd/lib/"
}

修改引用
配置一个webpack alias别名,如'原始文件的引用路径': '修改后文件的引用路径',使得最终修改后的文件被引用,如:

1
2
3
4
5
resolve: {
alias: {
'antd/upload': path.resolve(__dirname, './patched/upload/*'),
}
}
0%