经过紧张的学习,React 课程终于暂告一个段落了。本章将是一个相对轻松的内容,我们一起探讨一下 React 应用的 SEO 以及为什么需要服务端渲染,同时对 React 课程做个简单的小结。
为什么说 SEO 是 React 应用的最大挑战
我们来做个实验:将本课程 React 课程第一章最后使用 vite 生成的 React 进行构建,然后看一下构建后的内容。先来看一下我们的源码怎么写的:
import React from 'react';
function App() {
return <h1>你好,axum中文网 - 来自 react 的问候</h1>;
}
export default App;
构建好之后,会将目标文件放在 dist
目录下。打开 dist/index.html
,这是构建好的最终的 HTML 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<script type="module" crossorigin src="/assets/index.7b12dfb4.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
握了个大草,依然只有一个 <div id="root"></div>
。我们在组件里写的那个 <h1>你好,axum中文网 - 来自 react 的问候</h1>
跑去哪里了?
你会看到,在这个 HTML 里有一行引入 JS 代码的标签:<script type="module" crossorigin src="/assets/index.7b12dfb4.js"></script>
,是的,我们写的组件(包括本例这种硬编码进去的文本内容)全部被打包进了 JS 文件里。
原因也很简单,我们是用 JSX 来开发组件的,JSX 的本质是 React.createElement()
语法糖, 这是一个由 React 提供的 Javascript 函数,自然我们开发的组件全被视为对 React.createElement()
调用的 Javascript 代码了。
HTML 里空空如也,内容和数据全跑到 JS 里去了,这是对 SEO 最大的挑战——几乎不要奢望SEO了。
本节代码:axum-rs-react-no-seo
服务端渲染(Server-Side Rendering)
有没有可能把组件直接渲染成普通的HTML标签,以利于SEO?可以的,服务端渲染就行,而且 React 官方就提供了相应的功能。下面代码是我们一直在用的,一个典型的 React 入口文件:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
第2行:import ReactDOM from 'react-dom/client'
,我们是从 react-dom/client
导入的 ReactDOM
- 大胆猜测,是不是还有
react-dom/server
? - 小心求证:是的。官方文档在这里
本课程不使用 React 提供的服务端渲染,而是使用 NextJS 来更灵活的处理各种渲染模式。
惊天大BUG!我要上报给官方,少说能拿个几万美刀!
一位不愿透露姓名的学员(毕竟要为几万美刀的安全负责)表示,他使用 Context 制作了用户登录,但按F5刷新网页之后,又变成未登录了。这是一个BUG,上报给官方少说能拿几万美刀——他肆意地遐想着。
同学,醒醒!你缺的不仅仅是美刀,更是缺少对基础知识的理解!
我们知道,我们开发的组件其实是作为虚拟DOM存在的,由 React 决定什么时候、以什么方式转换成真实 DOM 并渲染到页面上。虚拟 DOM 是保存在内存里的,所以每次刷新页面都会导致组件重走一次生命周期——就是说,每次刷新页面,你看到的其实是不同的实例。连组件都重新走一遍整个生命周期了,你觉得由它维护的 state
、props
不会丢失吗?
要避免这位同学遇到的问题,那就需要将那些数据进行持久化。
前端数据的持久化
在后端,我们有很多种途径对数据进行持久化:写入到文件、保存到数据库等等。而在前端,我们可以利用 HTML 5 提供的 Storage
API实现。
有两种 Storage
:
localStorage
:本地存储sessionStorage
:会话存储
它们提供的 API 一样,只是存储数据的持续时间不同。会话存储基本上就是关闭浏览器,数据就会自动被清除;而本地存储则是,除非手动删除,否则它一直都在。
上面给出的文档链接里有示例代码,我们来看一下:
// 保存数据到 sessionStorage
sessionStorage.setItem('key', 'value');
// 从 sessionStorage 获取数据
let data = sessionStorage.getItem('key');
// 从 sessionStorage 删除保存的数据
sessionStorage.removeItem('key');
// 从 sessionStorage 删除所有保存的数据
sessionStorage.clear();
再说一遍,它们的API是一样的,把上面代码中的
sessionStorage
改成localStorage
就是本地存储
本地存储的 React hook
定义一个 hook
function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}
使用 hook
function App() {
// Similar to useState but first arg is key to the value in local storage.
const [name, setName] = useLocalStorage("name", "Bob");
return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
以上代码来自这里
你可以根据自己的需求把上面的 hook 进行优先、扩展,以期与你的需求更加契合。
是时候撒花了,恭喜你完成了我们这个专题的第一个子专题。下一章我们将开始 NextJS 的学习,准备好了吗?