普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月27日首页

古法编程: SPA的路由浅思考

作者 Pkmer
2026年4月26日 18:22

在之前我所有的印象中,往浏览器输入URL,那么就会请求服务器获取资源。毕竟URL又叫统一资源定位器:标识了一个互联网资源的“住址”。

但是 SPA(Single Page Application)单页面应用程序,似乎打破了我这种固有的认知,让我陷入了盲区。我看到它的网络资源地址改变了,但是它并没有向服务器请求?

react-router这家伙就干这样的事情:

  1. 修改了应用程序员的位置URL
  2. 确定在指定的位置渲染哪些React组件

解决盲区痛点

问题1:URL变化了为什么没有请求服务器

URL 变化 ≠ 一定会向服务器发起请求

正常情况下:

用户点击链接 → 地址栏 URL 变化 → 浏览器向新 URL 发送请求 → 服务器返回页面 → 页面刷新

上面是常见的一个思维定式,因此让很多人觉得URL变化请求服务器是一个不可分割的原子操作。实际上:改变 URL 是一个动作,向服务器请求是另一个动作

打开浏览器,我们输入reactrouter.com敲回车来到react-router的官网,打开命令行窗口,输入下面的代码

window.history.pushState({},null,"/home")

结果发现浏览器中的URL已经变化了,但是并没有跳转到home页面

image.png

window.history.pushState

pushState 是 HTML5 提供的 History API 中的一个方法,用于在不刷新页面的情况下向浏览器的会话历史栈中添加一个新的历史记录条目。

问题2: URL变化了此时用户手动刷新为什么不404?

经过上面的案例了解到了window.history.pushState的作用。那么好,假如现在用户手动刷新了页面,此时是会向服务器请求资源的。那么为什么刷新之后看起来是正常页面,而不是404?

正常流程应该是这样会报404的:

1.  用户访问 `https://example.com/`
2.  服务器返回 `index.html`
3.  前端 JS 加载,使用 `pushState` 跳转到 `/user/profile`
4.  地址栏变成 `/user/profile`,页面正常显示 ✅

      **此时用户刷新页面(出问题了):**

5.  用户在 `/user/profile` 页面按 F5 刷新
6.  浏览器向服务器请求 `https://example.com/user/profile`
7.  服务器去找 `/user/profile` 这个路径对应的文件或接口
8.  找不到!返回 404 ❌

        **为什么会 404?**  
        
因为服务器上没有 `/user/profile` 这个真实的文件,它只是前端路由的一个虚拟路径。

但是我们看正常SPA应用程序不会出现404。原因是因为背后的nginx捣的鬼。

nginx配置

try_files 内部回退配置

内部回退的特点:

  • Nginx 内部尝试不同的文件路径
  • 不返回重定向状态码(返回 200)
  • 地址栏 URL 保持不变
  • 用户完全无感知
# Nginx 配置示例:SPA 所有路由都返回 index.html
location / {
  try_files $uri $uri/ /index.html;
}
参数 说明
$uri 用户请求的原始路径(如 /user/profile
$uri/ 尝试作为目录查找(如 /user/profile/
/index.html 前面都找不到,就返回 index.html

场景:用户刷新 /user/profile

1. 用户访问 https://example.com/user/profile
   ↓
2. Nginx 尝试找文件:$uri = /user/profile
   → 文件不存在
   ↓
3. Nginx 尝试找目录:$uri/ = /user/profile/
   → 目录不存在
   ↓
4. 最后返回 /index.html(不重定向,直接返回这个文件的内容)
   ↓
5. 浏览器收到 index.html
   ↓
6. 前端 JS 重新加载,读取当前 URL 是 /user/profile
   ↓
7. 前端路由匹配到 /user/profile,渲染对应页面
   ↓
8. 用户看到正确的页面 ✅

小结

以上就是关于react-router在单页面应用程序SPA中对我来说神奇的魔法。总结一下,我缺失的盲区

  1. window.history.pushStateAPI
  2. nginx的try_files 内部回退
昨天 — 2026年4月26日首页

古法编程: React思维模型快速建立

作者 Pkmer
2026年4月26日 16:54

d179110117a86708aa993eb40f13a3c4_720.jpg

使用React的方法论与思想,方便我这个古法编程者Vibe Coding的时候更能游刃有余。

从原始操作DOM API,编写每个操作步骤的命令式,到使用React负责处理DOM的细节,程序员只描述最终结果的声明式编程,最终实现DOM响应数据的变化而自发做出更改的响应式。其中声明式编程,以现代的我的视角了看很像Vibe Coding,我们只需要描述需求即可。

一层一层的抽象出来。命令式->声明式->Vibe Coding(氛围编程)

React思维模型

HTML上凿洞,动态数据露脸

就像旅游景区镂空的拍照墙,我们只需要露个头,一个完美的姿势拍照就好了。

import { useState } from "react";

export default function App() {
  let [who, setWho] = useState("Pkmer");
  return (
    <div className="icons">💪{who}👊</div>
  );
}

JSX是伪装成HTML的JavaScript代码

React开发工具将JSX标签自动转化为相应的JavaScript代码

function App(){
    let [username] = userState(() => "Pkmer")
    return <div className="container">
        <text className="name">💪{username}👊</text>
        <button />
    <div>
}

转化成的JavaScript

import {jsx as _jsx} from "react"

function App(){
    let [username] = userState(() => "Pkmer")
    return _jsx("div",{
        className: "container",
        children: [
            _jsx("text",{className: "name",children: ["💪",username,"👊"]}),
            _jsx("button")
        ]
    })
}

一次组件渲染,一页手翻书

组件每次渲染的时候,都会重新执行一次

function ComponentXxx(){
    console.log("run run run...")
    return // ...jsx...
}

不可变特性(快照)

组件的state数据是不可变的,每一次都只是一个快照,要想更新数据,需要推倒重建。一个组件就像一座大厦一样,就算只给窗户换一种颜色,那么这个大厦就得重建(重新渲染)

下面的代码第一种错误的示范中:在React看来house还是原来的那个house,同一个引用,React会不会重新渲染。第二种正确的方式: house已经不再是原来的house了,尽管大部分内容是一样的,但是它已经是一个新的house.

const [house,setHouse] = useState({windowColor: "蓝色",floors: 2});

// ❌️这样修改不会有效果,house只是快照
house.windowColor = "白色"
setHouse(house)  


// 正确✅️
const newHouse = [...hourse,windowColor: "白色"]
setHouse(newHouse)

可以这样理解:React为了性能考虑,没有进行深层比较,这里只是浅层的比较,发现house的引用变了,才更新。

组件的动态组合方式:children

相比搭积木一样一个一个组件固定的组合方式,children这个特殊的属性,提供了动态组件组合方式

function Dialog({children}){
    return <div>
        <div>{children}</div>
    </div>
}

// 使用
function App(){
    return <Dialog>
        <Heading>I Love Coding</Heading>
        <Slogon>此刻我在深圳图书馆-北馆,充电学习</Slogon>
    </Dialog>
}

传送工程师的接力:单向数据流

props层层传递

数据所有者要想将数据传递给消费者,需要进行层层传递,尽管中间传递者并不消费这个数据

image.png

context电梯,按需取货

在提供数据的楼层(上层)将包裹(数据),放入电梯(context)。电梯往下走,下面的哪个楼层需要这个包裹(数据),自己在对应的楼层打开电梯取走。

const ThemeContext = React.createContext("light")

// 上层数据所有者,提供数据,放入电梯
function Home(){
    const [theme,setTheme] = useState("light")
    
    return <ThemeContext.Provider value={theme}>
        <Page />
    </ThemeContext.Provider>
}

App->Page->Header->Logo,中间层Page,Header都不需要数据,只有Logo需要。

function Page(){
    return <div>
        <Header />
        <Content />
        <Footer />
    </div>
}

function Header(){
    return <div>
        <Logo />
        <Title />
    </div>
}

function Logo(){
    // 在我这层,打开电梯取出包裹(数据)
    const theme = useContext(ThemContext)
    
    return theme === "dark" ? <DarkLogo /> : <LightLogo />
}

便携式虫洞

由于数据是单向传递的,如果子组件要想改变数据,需要数据提供层进行修改。想要修改上层数据,上层提供则需要将权力下放。而这个下放的过程,就像虫洞一样,子组件员工可以将手伸向上层老板的办公室,直接进行签合同。

function BossOffice() {
  const [contract, setContract] = useState('初始合同');

  // 老板下放修改权限的方法
  const signContract = (newContract) => {
    setContract(newContract);
  };

  return (
    <div>
      <h1>老板办公室 - 当前合同: {contract}</h1>
      {/* 将修改权限通过 props 下放给子组件 */}
      <EmployeePortal onSignContract={signContract} />
    </div>
  );
}

function EmployeePortal({ onSignContract }) {
  const handleSign = () => {
    // 子组件员工直接调用上层传来的方法,就像穿过虫洞伸手改数据
    onSignContract('员工新签的合同');
  };

  return (
    <div>
      <h2>员工虫洞通道</h2>
      <button onClick={handleSign}>伸向老板办公室签合同</button>
    </div>
  );
}

Hook勾子将数据放入React大海又勾回来

Hook将函数组件内的数据保存到外部环境,以备下次渲染所用。

  • 保存只读数据: useMemo(保存函数的返回值),useCallback(保存的是回调函数本身)
  • 保存可变数据,更改时触发渲染: useState和useReducer(更底层)
  • 保存可变数据,更改时不触发渲染: useRef

useEffect与生命周期回调方法

useEffect完全可以代替类组件中的三个生命周期回调方法

class Xxx extends Component{
    // 挂载以后运行
    componentDidMount{}
    
    // 每次更新以后运行
    componentDidUpdate(prevProps){}
    
    // 将要卸载前运行
    componentWillUnmount(){}

}

useEffect对应的行为方式

useEffect(() => {
    // 数组参数为空,只在组件第一次渲染时调用
},[])

useEffect(() => {
    // 当数组中的元素变化更新时会执行
},[要变化的值,或者函数]) // 要诚实的告诉哪些值会变。


useEffect(() => {
    // 省略数组。将在组件每次渲染后运行此处的代码
})

useEffect的真正职责:管理组件副作用

药物的副作用并不是药物的目的,当然是越少越好。而程序里的副作用却是我们有意而为,是程序的功能之一

所谓副作用,是函数组件与其周边环境发生了交互的额外任务,比如操作window对象,访问网络请求后端api,读取本地文件等,这些作用都超出了当前函数组件的范围。函数组件关心的是state和props

function Boat(props){
    useEffect(() => {
        const listener = ...
        window.addEventListener('keydown',listener)
        ...
        return () => {
            window.removeListener('keydown',listener)
        }
    },[])

}
❌
❌