Barrel Export、Jest、Babel 与依赖图

Barrel Export、Jest、Babel 与依赖图

学习开源项目的时候遇到的一个issue,ai的相关分析,在此做些记录。

目标:通过一个真实 Issue,系统理解 React/TypeScript 项目中的 barrel file、ES Module、module graph、Jest、Babel AST transform、Tree Shaking、side effects、feature boundary,并沉淀为可以用于实际项目和面试的判断框架。


0. 先说结论

这个 Issue 的核心并不是"React 有问题",而是一个更底层的模块系统问题:

当业务代码从一个 feature 的 index.ts barrel file 导入一个成员时,测试环境可能需要先加载这个 barrel file,而 barrel file 又声明了多个 re-export/import,因此其他模块可能进入测试运行时的模块依赖图;如果这些模块存在顶层副作用,就会出现"我没用这个组件,但它的代码却执行了"的现象。

Issue #120 的原始描述是:features/order/index.ts 导出 useCreateOrderMutationuseGetOrderQueryMyPage.tsx 只从 @/features/order 使用 useGetOrderQuery,但在 Jest 测试中,useCreateOrderMutation 内部代码也被执行。Issue 于 2023-04-05 创建,目前页面仍标记为 Open,且当前页面没有关联 PR/Development 项。

后续有人提供了一个专门针对 Bulletproof React feature barrel 的 Babel 插件:

pafry7/babel-plugin-bulletproof-features-import

它的核心思路不是取消 barrel API,而是:

text 复制代码
源码层:保留漂亮的 feature import
        ↓
Babel 编译阶段解析 feature/index 文件
        ↓
把 barrel import 改写为具体模块 import
        ↓
Jest / bundler 获得更精确的依赖图

这个方案可以理解成:

对开发者保留稳定、漂亮的 public API;对运行时/测试环境生成更精确的 dependency graph。

但是要注意一个非常重要的现实细节:当前插件仓库 README 的示例与源码实现之间存在表达上的不一致。README 展示的是 export { Foo } from './components/Foo' 风格,而当前 collect-esm-imports.js 的实现主要遍历 ImportDeclaration,也就是更直接地支持 import { Foo } from './components/Foo'; export { Foo }; 这种模式。实际采用之前应该以当前源码和项目测试为准,而不要只根据 2023 年的 Issue 评论判断能力边界。


1. Issue 背景:Bulletproof React 为什么会出现 barrel file

Bulletproof React 的一个核心思想是 feature-based architecture

典型结构类似:

text 复制代码
src/
├── app/
├── components/
├── features/
│   ├── auth/
│   ├── users/
│   └── orders/
├── hooks/
├── lib/
├── testing/
├── types/
└── utils/

一个 feature 内部又可能继续分:

text 复制代码
features/order/
├── api/
├── components/
├── hooks/
├── stores/
├── types/
├── utils/
└── index.ts

index.ts 往往承担 feature 的 public API:

ts 复制代码
// features/order/index.ts
export { useCreateOrderMutation } from './api/useCreateOrderMutation';
export { useGetOrderQuery } from './api/useGetOrderQuery';
export { OrderList } from './components/OrderList';
export type { Order } from './types/order';

业务代码可以写:

ts 复制代码
import { useGetOrderQuery } from '@/features/order';

而不是:

ts 复制代码
import { useGetOrderQuery } from '@/features/order/api/useGetOrderQuery';

这两种写法背后代表两个不同的架构理念:

深路径 import

text 复制代码
业务层
  ↓
feature 内部实现

调用方知道 feature 内部怎么组织文件。

Barrel import

text 复制代码
业务层
  ↓
feature public API
  ↓
feature 内部实现

调用方只依赖"这个 feature 提供了什么",而不是"它内部怎么实现"。

这就是 barrel 的架构价值。


2. Barrel file 是什么

Barrel file,也叫 barrel module / aggregator module,是一个专门负责聚合其他模块导出的文件。

例如:

ts 复制代码
// components/index.ts
export { Button } from './Button';
export { Dialog } from './Dialog';
export { Select } from './Select';

调用方:

ts 复制代码
import { Button } from './components';

MDN 也把这种"集中多个模块导出"的方式称为 barrel module / re-export aggregation。模块之间会形成一个依赖图(dependency graph)。

参考:

  • MDN JavaScript Modules
  • MDN export

3. 为什么"我只 import 一个东西",另一个模块仍然可能被加载?

这是理解 Issue #120 的最关键知识点。

假设:

ts 复制代码
// features/order/index.ts
export { useCreateOrderMutation } from './api/useCreateOrderMutation';
export { useGetOrderQuery } from './api/useGetOrderQuery';

业务代码:

ts 复制代码
import { useGetOrderQuery } from '@/features/order';

直觉上很多人会认为:

text 复制代码
我只用了 useGetOrderQuery
=> useCreateOrderMutation 不应该碰

但 ES Module 的依赖处理不是按"最终 JS 表达式有没有使用"直接决定"完全不接触另一个模块"。

可以把过程理解成:

text 复制代码
MyPage.tsx
  ↓
features/order/index.ts
  ↓
┌──────────────────────────────┐
│                              │
├── useCreateOrderMutation.ts  │
│                              │
└── useGetOrderQuery.ts        │

index.ts 本身就是一个模块,它声明了对两个模块的导出关系,因此这些模块可以进入宿主环境构建/加载的依赖图。

MDN 明确指出:模块及其依赖构成有向 dependency graph;模块求值时需要先处理其依赖。Node 的模块 API 也区分了 link / instantiate / evaluate 阶段。

更重要的是:

"模块被解析/加载/求值"和"最终 bundle 中是否保留某段未使用代码"是不同问题。


4. import、module loading、tree shaking 三件事情不要混为一谈

这是面试最容易被问到的地方。

4.1 Module graph

首先形成:

text 复制代码
A
├── B
├── C
└── D

表示 A 依赖 B/C/D。

4.2 Module evaluation

模块系统会处理模块依赖并执行模块顶层代码。

例如:

ts 复制代码
// foo.ts
console.log('foo module evaluated');
export const foo = 1;

只要 foo.ts 被执行环境加载并求值,它的顶层 console.log 就会发生。

这就是"模块副作用"。

4.3 Tree shaking

Tree shaking 是 bundler 做的 dead-code elimination,依赖 ESM import / export 的静态结构。webpack 文档把 tree shaking 定义为 dead-code elimination,并进一步区分了 usedExportssideEffects

所以:

text 复制代码
模块进入依赖图
        ≠
最终一定进入生产 bundle

生产 bundler 可能判断:

text 复制代码
这个 export 没有使用
        ↓
可被优化掉

但测试运行时并不一定有与生产构建完全相同的 tree shaking 能力。


5. 为什么 Jest 更容易暴露这个问题

Issue 发生在 Jest 测试环境。

Jest 的任务主要是:

text 复制代码
解析代码
→ 转换代码
→ 建立测试运行环境
→ 执行模块

而生产 bundler 的重点往往是:

text 复制代码
解析
→ 构建依赖图
→ 分析 export 使用情况
→ tree shaking
→ code splitting
→ minify
→ 输出 bundle

所以不要把:

"webpack 生产环境不会把它带进去"

误推导成:

"Jest 运行的时候这个 module 一定不会被 evaluate"。

Jest 的 ESM/CJS 与 transform 机制本身也存在差异,Jest 文档明确区分 ESM 与默认的 CommonJS transform 路径。

参考:Jest ECMAScript Modules 文档。


6. 真正触发问题的通常是"顶层副作用"

如果 useCreateOrderMutation.ts 只是:

ts 复制代码
export function useCreateOrderMutation() {
  // hook logic
}

那么它被加载通常没有太大的可观察影响。

真正危险的是:

ts 复制代码
// useCreateOrderMutation.ts
initializeSomething();
registerGlobalEvent();
connectSomeService();
console.log('module loaded');

export function useCreateOrderMutation() {}

此时:

text 复制代码
加载 module
   ↓
执行顶层代码
   ↓
副作用发生

所以 Issue 中"unused component executed"的本质更准确地描述应该是:

unused export 对应的 module 被纳入了执行路径,而该 module 的顶层行为产生了可观察副作用。

这比简单说"unused import 被执行了"更准确。


7. 这个问题和 side effects 有什么关系

webpack 文档对 side effect 的定义很重要:

除了暴露 export 之外,import 一个文件时还能产生特殊行为,这就是 side effect。

典型例子:

ts 复制代码
import './polyfill';

它可能什么都不导出,但会修改全局环境。

再比如:

ts 复制代码
window.foo = 'bar';

或者:

ts 复制代码
registerPlugin();

都属于典型副作用。

如果项目尽可能保持 module side-effect free,Tree Shaking、测试隔离、SSR、HMR 往往更容易管理。

webpack 也支持通过 package.json 中的 sideEffects 属性向 bundler 提供信息,例如:

json 复制代码
{
  "sideEffects": false
}

但注意:不要因为"优化"就机械地设置 sideEffects: false。如果项目确实存在 CSS、polyfill、全局注册等副作用,错误标记可能让生产构建错误地把这些文件裁掉。


8. Issue #120 的核心代码路径

把原 Issue 简化成:

ts 复制代码
// features/order/index.ts
export { useCreateOrderMutation } from './api/useCreateOrderMutation';
export { useGetOrderQuery } from './api/useGetOrderQuery';
tsx 复制代码
// pages/MyPage.tsx
import { useGetOrderQuery } from '@/features/order';
tsx 复制代码
// pages/MyPage.test.tsx
// 测试 MyPage,并 mock / spyOn useGetOrderQuery

问题:

text 复制代码
MyPage.test
    ↓
MyPage
    ↓
@/features/order
    ↓
index.ts
    ├── useCreateOrderMutation.ts  ← 也进入模块依赖图
    └── useGetOrderQuery.ts

如果 useCreateOrderMutation.ts 有顶层副作用,就可以被观察到。


9. 第一类解决方案:放弃 barrel,全部使用 deep import

最直接:

ts 复制代码
import { useGetOrderQuery } from '@/features/order/api/useGetOrderQuery';

而不是:

ts 复制代码
import { useGetOrderQuery } from '@/features/order';

优点

  • dependency 非常明确
  • 测试环境容易预测
  • 不容易因为聚合文件把无关模块带进来
  • 对构建器来说依赖路径非常直接

缺点

  • 暴露 feature 内部目录结构
  • 调用方和内部实现耦合
  • 文件重构成本提高
  • import path 变长
  • public API 不够干净

例如今天:

text 复制代码
features/order/api/useGetOrderQuery.ts

明天重构成:

text 复制代码
features/order/data/useGetOrderQuery.ts

全项目调用方可能都要改。

结论

这是简单可靠的方案,但会牺牲 feature encapsulation。


10. 第二类解决方案:保留 barrel,但减少 barrel 的职责

这是更值得推荐的工程实践。

不要:

ts 复制代码
// ❌ 巨型 barrel
export * from './api';
export * from './components';
export * from './hooks';
export * from './stores';
export * from './utils';
export * from './types';

而是只暴露真正的 public API:

ts 复制代码
// features/order/index.ts
export { OrderList } from './components/OrderList';
export { useGetOrderQuery } from './api/useGetOrderQuery';
export type { Order } from './types/order';

内部模块之间则可以使用明确路径。

这时 index.ts 真正承担的是:

Public API boundary

而不是:

"所有东西都从这里导出"


11. 第三类解决方案:让 module 尽量 side-effect free

这是最通用的原则。

推荐:

ts 复制代码
export function createOrder() {
  // 调用时才产生行为
}

谨慎:

ts 复制代码
initializeOrderSystem();
registerGlobalHandler();
export function createOrder() {}

如果确实需要初始化:

ts 复制代码
export function initOrderSystem() {
  initializeOrderSystem();
}

把"什么时候发生行为"从 module load 阶段推迟到显式调用阶段。

这有助于:

  • Jest 测试隔离
  • Tree Shaking
  • SSR
  • HMR
  • 依赖图可预测性
  • 代码初始化顺序
  • 减少循环依赖影响

12. 第四类解决方案:测试中 mock 整个 module

如果一个测试的目标是:

测试 MyPage,不关心 order feature 的真正实现

可以在测试中 mock:

ts 复制代码
jest.mock('@/features/order', () => ({
  useGetOrderQuery: jest.fn(),
}));

这种方式的价值在于 test isolation

但是:

mock 不能替代良好的模块设计。

如果原模块有大量不必要的顶层副作用,mock 只是把测试环境的表现压住了。


13. Issue 提出的核心 workaround:Babel 自动改写 import

社区后来有人发布:

pafry7/babel-plugin-bulletproof-features-import

仓库 README 明确说明,这个插件就是针对 Bulletproof React 的 feature barrel + Jest 场景设计的,并引用了 Issue #120 和 Jest #11234。

它的目标是:

确保测试期间只 import relevant files。

同时 README 提到它也可以用于本地开发或 production bundle 前的 import transform,并声称可以移除 circular dependencies、减少 bundle size,但可能影响 Fast Refresh 和 startup time。


14. 插件的核心思路

假设源码:

ts 复制代码
import { LoginView, LoginButton } from '@/features/login';

Feature barrel 描述了这些 export 到哪里。

插件在 Babel 阶段做静态分析,然后改写为:

ts 复制代码
import { LoginView } from '/absolute/path/to/features/login/views/LoginView';
import { LoginButton } from '/absolute/path/to/features/login/views/components/LoginButton';

最终变成:

text 复制代码
开发者写
    ↓
@/features/login
    ↓
Babel plugin
    ↓
具体模块路径
    ↓
Jest / bundler

这样就同时获得了:

对开发者

ts 复制代码
import { LoginView } from '@/features/login';

漂亮、稳定、隐藏内部结构。

对构建/测试环境

ts 复制代码
import { LoginView } from '@/features/login/views/LoginView';

依赖更精确。


15. 为什么要求相对路径

讨论里用户追问:

如果 feature 里有 typesscreenscomponentshooksutils 等多层目录呢?

作者回答:

只要 index 文件使用 relative imports,就应该支持 subfolders。

这个思路很好理解:

ts 复制代码
export { Foo } from './components/Foo';
export { bar } from './utils/bar';
export { useBaz } from './hooks/useBaz';

相对于:

text 复制代码
features/foo/index.ts

都能确定具体文件位置。

而 alias:

ts 复制代码
export { Foo } from '@/shared/foo';

则需要额外知道 @/ 的解析规则。


16. 重要:当前插件源码与讨论/README 存在一个值得注意的不一致

这是阅读开源项目时非常值得学习的一点:

不要只看 Issue 评论,要继续看实现。

当前插件源码的 src/collect-esm-imports.js 中,核心逻辑是用 TypeScript parser 解析 index 文件,并遍历:

text 复制代码
ImportDeclaration

然后把 import name 映射到 module path。

也就是说,它明确处理这种思路:

ts 复制代码
import { Foo } from './components/Foo';
export { Foo };

而不是简单地直接 AST 处理:

ts 复制代码
export { Foo } from './components/Foo';

另一方面,当前 README 的示例却直接展示了:

ts 复制代码
export { LoginView } from './views/LoginView';
export { LoginButton } from './views/components/LoginButton';

这说明:

  1. 文档和当前实现可能没有完全同步;
  2. 讨论里的"应该支持 subfolders"不能自动等价于"当前版本支持所有 re-export 语法";
  3. 真正采用之前一定要用当前版本的实际测试覆盖自己的 index.ts 写法。

这是分析开源项目时非常重要的工程习惯:

text 复制代码
Issue discussion
       ↓
README
       ↓
源码
       ↓
测试
       ↓
实际验证

而不是停留在第一层。


17. 插件源码是怎么工作的

当前 index.js 的整体流程大致是:

text 复制代码
Babel visitor: ImportDeclaration
            ↓
读取 import source
            ↓
判断是不是 features/<featureName>
            ↓
找到 feature/index.js 或 index.ts
            ↓
解析 barrel 文件
            ↓
得到 { exportName -> relativePath }
            ↓
遍历当前 import 的 named specifiers
            ↓
把每一个 import 改写到具体路径
            ↓
replaceWithMultiple()

源码中可以看到它:

  • 读取 featuresPath
  • 从 import module name 中识别 feature
  • feature/index.js / index.ts
  • 使用 collectEsmImports() 解析 barrel
  • ImportSpecifier 逐个生成新的 Babel ImportDeclaration
  • 最后用 replaceWithMultiple() 替换原 import

这正是一个非常典型的 Babel AST transform。


18. 这个插件为什么是 Babel Plugin,而不是普通 util

因为它要改写:

ts 复制代码
import { Foo } from '@/features/foo';

这种 语法级结构

Babel plugin 可以直接访问 AST:

text 复制代码
Program
 └── ImportDeclaration
      ├── source
      └── specifiers

然后把 AST:

text 复制代码
ImportDeclaration('@features/foo')

改成:

text 复制代码
ImportDeclaration('/features/foo/components/Foo')

Babel 官方文档把 plugin 机制描述为:通过 plugin/preset 对代码进行 transformation;@babel/core 可以返回 transform 后的 code / source map / AST。

这个案例非常适合学习 Babel AST。


19. 为什么不用运行时 resolver

因为这个问题最好在 compile time 解决。

如果运行时再做:

text 复制代码
运行时 import feature
     ↓
动态判断实际文件

就会增加:

  • 运行时复杂度
  • bundle 分析难度
  • 测试复杂度
  • 调试成本

而在 Babel 阶段:

text 复制代码
源码
 ↓
静态分析
 ↓
确定具体路径
 ↓
生成普通 import

最终产物不需要知道"曾经有一个 barrel import"。

这是非常典型的:

把运行时工作提前到编译期。


20. 为什么这可能减少 bundle size

如果一个大 barrel 聚合很多模块:

text 复制代码
feature/index
 ├── A
 ├── B
 ├── C
 ├── D
 ├── E
 └── F

调用方只需要:

text 复制代码
B

直接 import 会让 dependency graph 更明确。

当然,真正优秀的生产 bundler 本来就应该通过 ESM 静态分析 + tree shaking 处理很多这类情况,因此不能简单说:

"所有 barrel 都一定导致生产 bundle 变大。"

更准确的说法是:

barrel 会增加中间模块层级,而是否最终导致额外 bundle 内容,取决于 module format、side effects、bundler 能力、配置以及代码结构。

webpack 明确说明 Tree Shaking 基于 ESM 的静态结构,并进一步区分 usedExportssideEffects


21. 为什么插件可能降低 Fast Refresh / startup

因为插件本身不是免费的。

每次 transform 都要做额外工作:

text 复制代码
ImportDeclaration
   ↓
识别 feature
   ↓
读 index 文件
   ↓
parse
   ↓
建立 export map
   ↓
重写 AST

即使插件做了 cache,也增加了编译阶段复杂度。

因此 README 自己也明确提示:

可能降低 Fast Refresh 和 startup time。

这就是典型的工程 trade-off:

text 复制代码
Runtime module graph 更精准
             ↑
             │
      编译时多做工作

22. 最佳实践:到底该不该用 barrel?

不要回答:

barrel 好 / barrel 坏。

更好的判断方式是:

22.1 对外 API:可以用 barrel

例如:

ts 复制代码
// features/auth/index.ts
export { LoginForm } from './components/LoginForm';
export { useAuth } from './hooks/useAuth';
export type { User } from './types';

这是 public API。

22.2 feature 内部:不要强迫所有代码走 barrel

feature 内部模块可以直接:

ts 复制代码
import { getUser } from '@/features/auth/api/getUser';

这能减少循环依赖和隐式耦合。

22.3 共享 UI library:barrel 通常更自然

例如:

ts 复制代码
import { Button, Dialog, Select } from '@/components/ui';

因为这里的 ui 本身就是 public package-like boundary。

22.4 大型 monorepo:尤其注意跨 package barrel

跨 package 的巨大 barrel:

text 复制代码
@company/ui
@company/core
@company/data

如果所有东西都从一个入口导出,很容易形成:

  • 巨大依赖图
  • circular dependency
  • 较差的开发体验
  • tree-shaking 边界不理想

所以 package exports 应该经过设计,而不是无限聚合。


23. 最佳实践:把 index.ts 当作"API contract"

一个非常好的代码品味原则:

只有真正想让外部依赖的东西,才进入 public barrel。

比如:

ts 复制代码
// ✅ public
export { UserList } from './components/UserList';
export { useCurrentUser } from './hooks/useCurrentUser';
export type { User } from './types';

不要:

ts 复制代码
// ❌ 把内部所有细节全部暴露
export * from './api';
export * from './components';
export * from './hooks';
export * from './utils';
export * from './stores';
export * from './internal';

前者是 API design。

后者更接近"文件索引"。

这两件事不是同一个东西。


24. 最佳实践:减少 module-level side effects

推荐:

ts 复制代码
export function registerOrder() {
  registerSomething();
}

谨慎:

ts 复制代码
registerSomething();

export function registerOrder() {}

如果必须存在:

text 复制代码
side-effects.ts

可以考虑明确隔离,并在 package/bundler 层面正确描述 sideEffects。


25. 最佳实践:依赖方向应该清晰

对于 feature-based architecture,推荐:

text 复制代码
shared / common
       ↑
     features
       ↑
       app

更直观地说:

text 复制代码
app
 ↓
features
 ↓
shared/lib/utils

而不是:

text 复制代码
feature A
 ↕
feature B
 ↕
feature C

因为循环依赖和跨 feature 隐式耦合会显著增加调试难度。

一个好的 index.ts 应该帮助你形成边界,而不是破坏边界。


26. 最佳实践:不要把 Tree Shaking 当成所有问题的万能药

错误思路:

"Webpack 会 tree shake,所以随便 barrel、随便副作用都没关系。"

正确思路:

text 复制代码
先保证 dependency graph 合理
        ↓
保证 module 尽量 side-effect free
        ↓
让 ESM 静态分析有机会工作
        ↓
再让 bundler 做 tree shaking

Tree Shaking 是优化机制,不是架构机制。


27. 什么时候值得引入这个 Babel 插件

比较适合:

text 复制代码
✅ React Native + Jest
✅ Bulletproof React 风格的 feature barrel
✅ 现有代码量很大,不想改所有 import
✅ 测试中频繁遇到无关模块加载/副作用
✅ 希望继续保留 feature public API

不应该因为:

text 复制代码
"我看到了这个 Issue"

就直接加入每个 Next.js / React 项目。

先确认:

  1. 是否真的存在 barrel 导致的实际问题;
  2. 是否是 module side effect 导致;
  3. 生产 bundler 是否已经很好地 tree-shake;
  4. 是否可以通过缩小 barrel public API 解决;
  5. 是否应该改变模块结构,而不是加工具补洞。

28. 一个推荐的决策树

遇到:

"我只 import A,但 B 似乎也执行了。"

按照下面顺序排查。

text 复制代码
Q1: B 是通过 barrel/index.ts 间接进入依赖图的吗?
        │
        ├── 否 → 看动态 import、测试 setup、全局初始化等
        │
        └── 是
             ↓
Q2: B 是否存在 module-level side effect?
        │
        ├── 否 → 大概率只是 module 被加载,不一定是业务 bug
        │
        └── 是
             ↓
Q3: 可以把副作用移出 module top-level 吗?
        │
        ├── 可以 → 优先重构
        │
        └── 不行
             ↓
Q4: barrel 是否真的需要暴露 B?
        │
        ├── 不需要 → 缩小 public API
        │
        └── 需要
             ↓
Q5: 测试是否应该 isolate?
        │
        ├── 是 → module mock
        │
        └── 否
             ↓
Q6: 是否值得做 compile-time import transform?
        │
        └── 根据项目规模/构建链决定

29. 推荐的项目写法

方案 A:普通 feature

text 复制代码
features/order/
├── api/
│   ├── createOrder.ts
│   └── getOrder.ts
├── components/
│   └── OrderList.tsx
├── hooks/
│   └── useOrder.ts
├── types/
│   └── order.ts
└── index.ts
ts 复制代码
// index.ts
export { OrderList } from './components/OrderList';
export { useOrder } from './hooks/useOrder';
export type { Order } from './types/order';

业务侧:

ts 复制代码
import { OrderList, useOrder } from '@/features/order';

feature 内部:

ts 复制代码
import { getOrder } from '@/features/order/api/getOrder';

这是一个很好理解的 public/internal 边界。


30. 需要特别警惕的写法

ts 复制代码
// index.ts
export * from './api';
export * from './components';
export * from './hooks';
export * from './stores';
export * from './utils';
export * from './constants';
export * from './internal';

这种写法短期很爽:

ts 复制代码
import { A, B, C, D, E, F } from '@/features/order';

但是长期容易出现:

text 复制代码
feature/order
     ↓
巨型 public API
     ↓
任何东西都能被依赖
     ↓
内部重构越来越困难
     ↓
circular dependency / module graph 膨胀

相关推荐
l1m0_2 小时前
告别反复修改Prompt:AI生成React应用并落地开发的实战复盘
前端·react.js·ui·ai·设计
xexpertS2 小时前
前端工程转型实践:从 Ember 迁移到 React,提升构建速度与研发效能
前端·react.js·前端框架
张元清3 小时前
React useFocus Hook:追踪并控制元素焦点状态 (2026)
javascript·react.js
一朵好运莲4 小时前
智能体使用 Chrome DevTools MCP 调试浏览器
开发语言·javascript·react.js
触底反弹4 小时前
🔥 从 SPA 到 SSR:5 个核心差异搞懂 Next.js 为什么是前端 SEO 的终极方案
前端·react.js·next.js
BreezeJiang5 小时前
为什么 Next.js 要把组件切成两半?从一个 SEO 死穴说起
javascript·react.js
小林ixn5 小时前
深入理解 React 水合(Hydration):从静态 HTML 到可交互页面
react.js·next.js
小林ixn5 小时前
Next.js 全栈实战:从 SPA 到 SSR,SEO 友好到底怎么落地?
react.js·前端框架·next.js
烬羽5 小时前
彻底搞懂 App Router 约定式路由:文件放对位置,路由就自动生成了
react.js·next.js·前端工程化