React: SEO挑战、服务端渲染及本地存储

18162
2022/11/17 16:30:49

经过紧张的学习,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 是保存在内存里的,所以每次刷新页面都会导致组件重走一次生命周期——就是说,每次刷新页面,你看到的其实是不同的实例。连组件都重新走一遍整个生命周期了,你觉得由它维护的 stateprops 不会丢失吗?

要避免这位同学遇到的问题,那就需要将那些数据进行持久化。

前端数据的持久化

在后端,我们有很多种途径对数据进行持久化:写入到文件、保存到数据库等等。而在前端,我们可以利用 HTML 5 提供的 Storage API实现。

有两种 Storage

它们提供的 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 的学习,准备好了吗?