目录
问题起因:
最近遇到了点问题,react项目的动态路由,逻辑这些都写好了,我把后台的数据复制到本地模拟动态路由是可以的,但是呢,当调用接口后数据能够拿到,尴尬的是动态路由渲染不出来,实际上是拿不到,也就是说,执行顺序的问题导致的,也就是环境问题。
下面是我的文件的位置:
注意这是文件的位置:顶级文件!
React项目在开发和生产过程中主要涉及到了两个环境:
一个是window(浏览器环境),一个是node环境。
判断是否浏览器环境的话可以通过 if (typeof window !== 'undefined') {}
先node ---> 后window
像node的话,主要是用于开发工具链和构建过程:
开发阶段(运行开发服务器、构建工具)和生产阶段(构建生产包)
执行构建工具(Webpack、Babel)
运行开发服务器
处理环境变量
执行测试
相关的文件比如:package.json, webpack.config.js, .env环境变量等
而浏览器环境,主要是用于:
运行实际React应用(打包后的代码在浏览器中执行)
渲染用户界面
执行React组件
处理用户交互
相关的文件比如:index.html ,src/index.js,src/App.js,路由配置文件,React组件文件,CSS/SCSS样式文件等
文件执行顺序
Node.js启动开发服务器(npm start)
加载构建配置(webpack, babel)
编译源代码(JSX转换、TypeScript编译等)
浏览器加载HTML入口文件(index.html)
浏览器加载并解析 public/index.html 文件,这是React应用的入口点。 <!-- public/index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>React App</title> </head> <body> <!-- 根DOM容器 --> <div id="root"></div> </body> </html>
加载并执行JavaScript入口文件(index.js)
览器解析到 index.js 文件后,下载并执行该脚本。 // src/index.js import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; // 定位根DOM节点 const rootElement = document.getElementById('root'); // 创建React根 const root = ReactDOM.createRoot(rootElement);
渲染根组件(App.js)
在入口文件中调用 root.render() 方法,开始渲染根组件。 // src/index.js // 渲染根组件 root.render( <React.StrictMode> <App /> </React.StrictMode> ); App.js 组件被加载和初始化,开始构建组件树。 // src/App.js import React from 'react'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <!-- 应用内容 --> </header> </div> ); } export default App;
初始化路由(React Router配置)
渲染匹配的路由组件
组件生命周期执行
对于上面我遇到的问题中:
1. Node.js环境(SSR)没有window对象(浏览器API(如 `localStorage`)只有在浏览器环境中才可用)
2. 即使没有SSR,在组件的顶层作用域直接访问 `localStorage` 也会在组件挂载前执行
3.在组件渲染的初始阶段(包括路由配置)执行时机过早
4..在客户端渲染时,只会在首次导入时执行一次,可能读取不到最新的值
node输出的路由只有本地的,没有从后台或者本地获取的:
我代码里面虽然判断了是否window环境,但是这里node就执行了,所以说下面这个动态路由直接为空数组了,也就是说,动态路由需要后台获取的或者调用api(这里调用接口是异步的,node环境使用axios调用也是一样的,数据是能够拿到的)的就不能放在顶级文件中,需要换位置,一般放在src文件中的某处
(我这里是登录后获取到的用户信息里面包含了,直接缓存到本地了)
可以在组件挂载后,使用useEffect+useState等调用浏览器的api这些
假如我调用接口,Umi是否会等待这些异步操作完成?
我关心的是在构建过程中,Umi是否会等待这些异步操作完成,然后再继续执行后续的静态路由和动态路由的导出操作?
我尝试使用过调用后台的api接口,并让等待其执行后,执行导出静态+动态路由。
但是,目前这个项目是基于(Ant Design Pro ) Umi 框架的,而不是 Next.js,比较遗憾,没啥用。
在Umi框架中,默认情况下,在构建时不会等待你在路由配置文件中进行的异步数据获取,要求是同步的,不能使用await!!!
1. 路由配置文件的作用:`config/routes.tsx`(或类似配置文件)的主要目的是定义路由结构,而不是执行数据获取。它通常是同步的。
2. 数据获取的时机:在Umi项目中,数据获取通常发生在以下环节:
2.1 - 页面组件内:使用`useEffect`(客户端获取)或通过Umi的服务器端渲染(SSR) 能 力 (在`getInitialProps`或类似方法中)。
2.2 - 服务端渲染(SSR):如果你启用了SSR,那么数据获取会在服务端进行,但这是在请求时(request time)而不是构建时(build time)。
3. 动态路由的生成:Umi支持动态路由(例如`/user/:id`),但动态路由的生成(即生成多个具体的路由路径)通常需要你在构建时通过插件或脚本预先确定这些路径。
Umi本身不会在构建时去调用API获取动态路由的路径列表,动态路由的生成必须在构建前通过脚本准备好
下面是流程图:
解决方案(针对 Ant Design Pro/Umi)
方案 1:运行时动态路由(推荐)
在页面组件内处理异步数据,而不是在路由配置中:
// config/routes.tsx (静态定义路径) export default [ { path: '/dynamic/:id', component: '@/pages/DynamicPage', } ]; // src/pages/DynamicPage.tsx import { useParams } from 'umi'; export default function DynamicPage() { const params = useParams<{ id: string }>(); const [data, setData] = useState(null); useEffect(() => { const fetchData = async () => { const response = await fetch(`/api/data/${params.id}`); setData(await response.json()); }; fetchData(); }, [params.id]); return data ? <div>{data.content}</div> : <Loading />; }
方案 2:构建时预取数据(需插件支持)
使用 Umi 插件实现类似 SSG 的功能:
安装数据获取插件:
npm install umi-plugin-static-props
配置
config/config.ts
:export default { plugins: ['umi-plugin-static-props'], staticProps: { dynamicPaths: async () => { const res = await fetch('https://api.example.com/dynamic-paths'); return res.json().map(id => ({ params: { id } })); }, getProps: async ({ params }) => { const res = await fetch(`https://api.example.com/data/${params.id}`); return { props: { data: await res.json() } }; } } };
- 3.修改页面组件:
// src/pages/DynamicPage.tsx export default function DynamicPage({ data }) { return <div>{data.content}</div>; } // 声明静态属性 DynamicPage.getStaticProps = async ({ params }) => { // 此函数在构建时由插件调用 };
方案 3:自定义构建脚本
在
package.json
中添加预构建脚本:{ "scripts": { "prebuild": "node ./scripts/fetchDynamicRoutes.js", "build": "umi build" } }
// scripts/fetchDynamicRoutes.js const fs = require('fs'); const fetch = require('node-fetch'); (async () => { // 1. 获取动态路由数据 const routes = await fetch('https://api.xxx.com/dynamic-paths').then(r => r.json()); // 2. 生成路由配置文件 const routeConfig = ` export default ${JSON.stringify( routes.map(id => ({ path: `/dynamic/${id}`, component: '@/pages/DynamicPage', // 注入预获取数据 data: ${JSON.stringify(await fetchData(id))} })), null, 2 )}; `; // 3. 写入临时路由文件 fs.writeFileSync('./src/.temp/routes.ts', routeConfig); })(); async function fetchData(id) { const res = await fetch(`https://api.xxx.com/data/${id}`); return res.json(); }
// config/routes.tsx import 'src/.temp/routes'; // 导入生成的配置
对于大多数 Ant Design Pro 项目,建议采用方案 1(运行时获取)结合 客户端缓存,既能保持开发简单性,又能提供良好用户体验。
如果需要 SEO 支持,可配合方案 2 或方案 3 实现部分路由的静态化。
如果我说:顶级文件执行环境为node,这句话对吗?
React应用的构建过程(编译、打包)通常在Node.js环境中进行。
// Webpack/Vite 等构建工具在 Node 环境中处理 React 文件 // 所有 import/require 语句由 Node 处理 import React from 'react';
React应用的运行环境主要是浏览器(客户端)。
如果使用服务端渲染(SSR),则部分代码(包括顶级文件)会在Node.js服务器环境中执行。
举例:
// 这个文件可能被 Node 和浏览器执行 (SSR 时) export default function Page({ data }) { // Node 获取 data (getServerSideProps) return ( <div> {/* 这部分 DOM 操作在浏览器执行 */} <button onClick={() => console.log('Click!')}> {data.title} {/* 文本由 SSR 注入 */} </button> </div> ) } // 这段只在 Node 执行 (Next.js) export async function getServerSideProps() { const res = await fetch('https://api.example.com/data'); // Node 环境请求 return { props: { data: await res.json() } }; }
因此,原话“react的顶级文件执行环境为node”是片面的。
正确的描述应该是:React 应用的源码处理和构建阶段在 Node 环境中运行,但运行时逻辑主要在浏览器环境执行。服务端渲染时组件代码会在 Node 环境先执行一次。
这里的话简单讲下node的这个package.json文件吧
package.json
是 Node 项目的 “配置中心”,统筹 元数据、脚本、依赖、环境约束。
简单介绍下下面这个文件吧
package.json里面的一些内容:
1. 基础元数据
{ "name": "ant-design-pro", "version": "6.0.0", "private": true, "description": "An out-of-box UI solution for enterprise applications" }
name
:项目名称,用于标识项目(若发布到 npm 仓库,名称需唯一)。version
:项目版本号,遵循 语义化版本规范(主版本.次版本.补丁版本
)。private
:设为true
时,项目不会被发布到 npm 公共仓库(避免私有项目误发布)。description
:项目功能描述,方便开发者理解项目定位。2.脚本命令(
scripts
)"scripts": { ... }
- 用于定义 npm 脚本,通过
npm run <脚本名>
运行命令(如npm run start
启动开发服务)。- 常见脚本:
start
(开发环境启动)、build
(生产打包)、test
(单元测试)等(具体内容因项目而异)。3. 浏览器兼容性(
browserslist
)"browserslist": [ "> 1%", "last 2 versions", "not ie <= 10" ]
- 定义项目 支持的浏览器范围,影响以下工具:
- Babel:决定哪些 ES6+ 语法需要转译为 ES5(兼容旧浏览器)。
- Autoprefixer:决定哪些 CSS 属性需要添加浏览器前缀(如
-webkit-
、-moz-
)。- 规则解析:
> 1%
:覆盖 全球市场份额超过 1% 的浏览器(数据来自 Can I Use)。last 2 versions
:每个浏览器的 最后两个正式版本。not ie <= 10
:排除 IE 10 及更早版本(即不支持 IE 10 以下浏览器)。4. 依赖管理
生产依赖(
dependencies
)"dependencies": { ... }
- 项目 运行时必须的依赖(如 React、Ant Design 组件库、业务逻辑库等),会被打包到生产环境。
开发依赖(
devDependencies
)"devDependencies": { ... }
- 仅 开发阶段需要的依赖(如 Webpack、Babel、ESLint、测试框架等),生产环境无需安装。
5. 环境约束(
engines
)"engines": { "node": ">=12.0.0" }
- 指定项目运行所需的 Node.js 版本范围(这里要求 Node.js ≥ 12.0.0)。
- 作用:确保团队成员 / CI 环境使用兼容的 Node 版本,避免因版本差异导致的问题。
举例:
{
"name": "my-react-app",
"version": "0.1.0",
"scripts": {
// Node.js环境下执行的脚本
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test"
},
"dependencies": {
// 运行时依赖(浏览器环境)
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
// 开发依赖(Node.js环境)
"webpack": "^5.75.0",
"babel-loader": "^9.1.2"
}
}
当然,除了上面这些还有其他的一些需要注意的:
其他常见环境问题与解决方案
问题1:服务器端渲染(SSR)中的window问题
// 错误:直接访问window对象
const isMobile = window.innerWidth < 768;
// 正确:使用useEffect和useState
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
handleResize(); // 初始调用
return () => window.removeEventListener('resize', handleResize);
}, []);
问题2:环境变量访问
// .env文件(Node.js环境)
API_URL=https://api.example.com
// React组件中访问(浏览器环境)
// 错误:process.env是Node.js环境变量
const apiUrl = process.env.API_URL;
// 正确:使用REACT_APP_前缀
// .env文件:REACT_APP_API_URL=https://api.example.com
const apiUrl = process.env.REACT_APP_API_URL;
最后,我们在使用过程中需要:
- 始终在useEffect中访问浏览器API(localStorage, window等)
- 为异步操作添加加载状态
- 使用环境变量时添加REACT_APP_前缀
- 避免在模块顶层作用域访问浏览器API
- 在SSR中使用动态导入(dynamic import)延迟加载浏览器相关组件
- 使用自定义hook封装环境相关逻辑
所有依赖浏览器环境的操作都应在组件挂载后执行(useEffect)
上面内容经供参考,包含个人看法,不是很准确,有点模糊,可能存在问题,有问题请指正,感谢!
----------到底啦----------