文章目录
1. ReactSSR
1.1 服务器端渲染
1.1.1 实现React SSR
- 引入要渲染的 React 组件
- 通过 renderToString 方法 将 React 组件转换为 HTML字符串
- 将结果 HTML 字符串响应到客户端
renderToString
方法用于将 React 组件转换为 HTML 字符串,通过react-dom/server
导入.
1.1.2 webpack 打包配置
问题: Node 环境不支持 ESModule 模块系统,不支持 JSX 语法
1.1.3项目启动命令配置
配置服务器端打包命令: "dev:server-build": "webpack --config webpack.server.js --watch"
配置服务端启动命令: "dev:server-run": "nodemon --watch build --exec\"node build/bundler.js\""
1.2 客户端 React 附加事件
1.2.1 实现思路分析
在客户端对组件进行二次“渲染”,为组件元素附加事件
1.2.2 客户端二次“渲染” hydrate
使用 hydrate 方法对组件进行渲染,为组件元素附加事件。
hydrate 方法在实现渲染的时候,会复用原本已经存在的 DOM 节点,减少重新生成节点以及删除原本 DOM 节点的开销。
通过 react-dom 导入 hydrate
ReactDOM.hydrate(<Home/>, document.getElementById('#root'))
1.2.3 客户端 React 打包配置
- webpack 配置
打包目的:转换 JSX 语法,转换浏览器不识别的高级 JavaScript 语法
打包目标位置:public文件夹 - 打包启动命令配置
"dev:client-build": "webpack --config webpack.client.js --watch"
1.2.4 添加客户端包文件请求链接
在响应给客户端的 HTML 代码中添加 script 标签,请求客户端 JavaScript 打包文件
<html>
<head>
<title> React SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script src="bundle.js"></script>
</body>
</html>
1.2.5 服务器端实现静态资源访问
服务器端程序实现静态资源访问功能,客户端 JavaScript 打包文件会被作为静态资源使用
app.use(express.static('public'))
1.3 优化
1.3.1 合并 webpack 配置
服务器端 webpack 配置和客户端 webpack 配置存在重复,将重复配置抽象到 webpack.base.js 配置文件中
1.3.2合并项目启动命令
目的:使用一个命令启动项目,解决多个命令启动的繁琐问题,通过 npm-run-all 工具实现。
"dev": "npm-run-all --parallel dev:*"
1.3.3 服务器端打包文件体积优化
问题:在服务器端打包文件中,包含了 Node 系统模块,导致打包文件本身体积庞大。
解决方案:通过 webpack 配置剔除打包文件中的 Node 模块。
const nodeExternals = require('webpack-node-externals')
const config = {
externals: [nodeExternals()]
}
module.exports = merge(baseConfig, config)
1.3.4 将启动服务器代码和渲染代码进行模块化拆分
优化代码组织方式,渲染 React 组件代码是独立功能,所以把它从服务器端入口文件中进行抽离。
1.4 实现路由
1.4.1 实现思路分析
- 在 React SSR 项目中需要实现两端路由。
- 客户端路由是用于支持用户通过点击链接的形式跳转页面。
- 服务器端路由是用于支持用户直接从浏览器地址栏中访问页面。
- 客户端和服务器端共用一套路由规则。
1.4.2 编写路由规则
share/routes.js
import Home from './pages/Home'
import List from './pages/List'
export default [
{
path: '/',
component: Home,
exact: true
}, {
path: '/list',
component: List,
}
]
1.4.3 实现服务器端路由
- Express 路由接受任何请求
Express 路由接受所有 Get 请求,服务器端 React 路由通过请求路径匹配要进行渲染的组件 - 服务器端路由配置
import React from 'react'
import {renderToString} from 'react-dom/server'
import { StaticRouter } from "react-router-dom";
import routes from '../share/routes'
import { renderRoutes } from "react-router-config";
export default (req) => {
const content = renderToString(
<StaticRouter location={req.path}>
{renderRoutes(routes)}
</StaticRouter>
)
return `
<html>
<head>
<title> React SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script src="bundle.js"></script>
</body>
</html>
`
}
1.4.4 实现客户端路由
添加客户端路由配置
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from "react-router-dom"
import { renderRoutes } from "react-router-config";
import routes from '../share/routes'
ReactDOM.hydrate(
<BrowserRouter>
{renderRoutes(routes)}
</BrowserRouter>
, document.getElementById('root'))
1.5 Redux
1.5.1 实现思路分析
- 在实现了React SSR 的项目中需要实现两端 Redux.
- 客户端 Redux 就是通过客户端 JavaScript 管理 Store 中的数据.
- 服务器端 Redux 就是在服务器端搭建一套 Redux 代码,用于管理组件中的数据.
- 客户端和服务器端共用一套 Reducer 代码.
- 创建 Store 的代码由于参数传递不同所以不可以共用
1.5.2 实现客户端 Redux
1.5.3 实现服务器端 Redux
1.6 防止 XSS 攻击
转移状态中的恶意代码
let response = {
data: [{id: 1, name: '<script>alert(1)</script>'}]
}
import serialize from 'serialize-javascript'
const initialState = serialize(store.getState())
2 Next
2.1 Next介绍
- Next.js是React服务端渲染应用框架.用于构建SEO友好的SPA应用.
- 支持两种预渲染方式,静态生成和服务器端渲染.
- 基于页面的路由系统,路由零配置
- 自动代码拆分.优化页面加载速度.
- 支持静态导出,可将应用导出为静态网站.
- 内置CSS-in-JS库styled-jsx
- 方案成熟,可用于生产环境,世界许多公司都在使用
- 应用部署简单,拥有专属部署环境Vercel,也可以部署在其他环境.
2.2 创建 Next.js 项目
- 安装一个 next-guide 项目, 该命令会零时调用npx create-next-app ,保证没事都是最新版本
- 创建:
npm init next-app next-guide
- 创建:
- 运行:
npm run dev
- 访问: localhost:3000
2.3 基于页面的路由系统
2.3.1 创建页面
- 在next.js中, 页面是被放置在pages文件夹中的React组件
- 组件需要被默认导出
- 组件文件中不需要引入 React
- 页面地址与文件地址是对应关系
创建页面:
pages/list.js文件
export default function List () {
return (
<div>List Page</div>
)
}
可访问http://localhost:3000/list查看效果
文件与路由的关系:
pages/index.js => /
pages/list.js => /list
pages/post/first.js => /post/first
2.3.2 页面跳转
- Link 组件默认使用JavaScript进行页面跳转, 即SPA形式的跳转
- 如果浏览器中JavaScript被禁用,则使用链接跳转
- Link 组件中不应添加 href 属性以外的的属性, 其他属性添加到 a 标签上
- Link 组件通过预取(生成产境)功能自动优化应用程序以获取最佳性能
试着修改:index.js
import Link from 'next/link'
export default function Home() {
return <div>
Index Page works
<Link href="/list"><a>Jump to List Page</a></Link>
</div>
}
2.4 静态资源、元数据和 CSS
2.4.1 静态资源
应用程序根目录中的 public 文件夹用于提供静态资源。
访问形式可参照
public/images/1.jpg --> /images/1.jpg
public/css/base.css --> /css/base.css
2.4.2 修改页面元数据
通过 Head 组件修改元数据
import Head from ‘next/head’
<>
2.4.3 CSS 样式
2.4.3.1 内置 styled-jsx
在 Next.js 中 内置了 styled-jsx, 它是一个 CSS-in-JS 库,允许在 React 组件中编写 CSS, CSS 仅作用于组件内部。
import Head from 'next/head'
import Link from 'next/link'
export default function Home() {
return <>
<Head>
<title>Index Page</title>
</Head>
<div>
Index Page works
<Link href="/list"><a className="demo">Jump to List Page</a></Link>
<img src="/images/1.jpeg" height="100" />
</div>
<style jsx>{`
.demo {
color: red
}
`}</style>
</>
}
2.4.3.2 CSS 模块
通过使用 CSS 模块功能,允许将组件的 CSS 样式编写在单独的 CSS 文件中.
CSS 模块约定样式文件的名称必须为"组件文件名称.module.css"
pages/list.js
import Head from "next/head";
import style from './list.module.css'
export default function List () {
return (
<>
<Head>
<title>List Page</title>
</Head>
<div className={style.demo}>List Page</div>
</>
)
}
page/list.module.css
.demo {
color: green;
font-size: xx-large;
}
2.4.3.3 全局样式文件
- 在pages文件夹下新建 _app.js文件,并加入如下代码
- 在项目根目录下创建 styles文件夹,并在其中创建 global.css
- 在 _app.js 中 通过 import 引入 global.css
- 重新启动开发服务器
pages/_app.js
import '../styles/globals.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
styles/globals.css
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
background: tomato;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
2.5 预渲染
2.5.1 预渲染概述
预渲染是指数据和 HTML 的拼接在服务器端提前完成。
预渲染可以使 SEO 更加友好。
预渲染会带来更好的用户体验,可以无需运行 JavaScript 即可查看应用程序 UI 。
2.5.2 两种形式
在 Next.js 中支持两种形式的预渲染:静态生成和服务器端渲染。
静态生成和服务器端渲染是生成 HTML 的时机不同。
静态生成:静态生成是在构建时生成 HTML 。以后的每个请求都共用构建时生成好的 HTML 。
服务器端渲染:服务器端渲染是在请求时生成 HTML 。每个请求都会重新生成 HTML
2.5.3如何做出选择
next.js 允许开发者为每个页面选择不同的渲染方式,不同的预渲染方式用不同的特点,应该根据场景进行渲染
但是大多建议页面使用静态生成
静态生成一次构建,反复使用,访问速度快,因为页面都是事先生成好的
使用场景: 营销页面、博客页面、电子商务产品列表、帮助和文档
服务端渲染访问速度不如静态生成快,但是由于每个请求都会重新渲染,适用于数据频繁更新的页面或者页面内容随请求的变化而变化的页面
2.5.4 实现静态生成
无数据和有数据的静态生成
如果组件不需要再其他地方获取数据,直接进行静态生成
- npm run build
如果组件需要在其他地方获取数据,在构建时next.js会预先获取组件需要的数据,然后对组件进行静态生成
2.5.4.1有数据的静态生成
getStaticProps 方法的作用是获取组件静态生成需要的数据。并通过 props 的方式将数据传递给组件。
该方法是一个异步函数,需要在组件内部进行导出。
在开发模式下, getStaticProps 改为在每个请求上运行。
在生产模式下, getStaticProps 只会在构建的时候执行,而每次访问 /list 页面时不会再执行 getStaticProps 方法。
pages/list.js
import Head from "next/head";
import style from './list.module.css'
import { readFile } from "fs";
import { promisify } from "util";
import { join } from "path";
const read = promisify(readFile)
export default function List ({data}) {
return (
<>
<Head>
<title>List Page</title>
</Head>
<div className={style.demo}>List Page</div>
<div>{data}</div>
</>
)
}
export async function getStaticProps () {
let data = await read(join(process.cwd(), 'pages', '_app.js'), 'utf-8')
console.log(data) // 会在 node 环境下输出,这个函数会在构建时运行
return {
props: {
data
}
}
}
2.5.4.2 服务器端渲染 getServerSideProps
如果采用服务端渲染,需要在组件中导出 getServerSideProps 方法
将 list.js 中的 getStaticProps 方法 改成 getServerSideProps 方法,其中 getServerSideProps 还有个参数为 context.
-
开发模式下不执行 getServerSideProps 方法。
-
生产模式下:
- 运行npm run build生成 .next 文件夹,可以看到 list 页面不会生成 HTML 页面。
- 运行npm start 启动生产环境的代码,访问 /list 页面, node 控制台会输出 getServerSideProps 方法中的打印语句。
因为使用了 getServerSideProps 则表示采用服务端渲染,而不是静态生成,所以每次访问都会执行 getServerSideProps 方法
list.js
import Head from "next/head";
import { readFile } from "fs";
import { promisify } from "util";
import { join } from "path";
// 将 回调函数形式的方法转成 promise形式
const read = promisify(readFile);
export default function List({ data }) {
return (
<div>
<h1>list page working</h1>
<p>
{data}
</p>
</div>
);
}
// 这里服务端渲染
export async function getServerSideProps() {
const data = await read(join(process.cwd(), "pages", "_app.js"), "utf-8");
console.log(data);
// 返回 props
return {
props: {
data,
},
};
}
2.5.4.3 基于动态路由的静态生成
- 基于参数为页面组件生成HTML页面,有多少参数就生成多少HTML页面
- 在构建应用时,先获取用户可以访问的所有路由参数,再根据路由参数获取具体数据,然后根据数据生成静态HTML.
实现:
创建基于动态路由的页面组件文件,命名时在文件名称外面加上 [],比如 [id].js
导出异步函数 getStaticPaths, 用于获取所有用户可以访问的路由参数
export async function getStaticPaths () {
// 此处获取所有用户可以访问的路由参数
return {
// 返回固定合适的路由参数
paths: [{params: {id: 1}}, {params: {id: 2}}],
// 当用户访问的路由有参数没有在当前函数中返回时,是否显示 404 页面, false 表示显示, true 表示不显示
fallback: false
}
}
导出异步函数 getStaticProps, 用于根据路由参数获取具体的数据
export async function getStaticProps ({params}) {
// params -> {id: 1}
// 此处根据路由参数获取具体数据
return {
// 将数据传递到组件中进行静态页面的生成
props: {}
}
}
注意: getStaticPaths 和 getStaticProps 只运行在服务器端,永远不会运行在客户端,甚至不会被打包到客户端 JavaScript 中,意味着这里可以随意写服务器端代码,比如查询数据库
2.5.4.4 fallback
- false: 参数不存在就 返回404页面
- true: 服务器要对这个不存在的页面进行静态生成,生成过程还要给用户一个等待的状态,不展示等待状态就无法成功构建。
- 该参数不存在=> 服务器端会再次执行getStaticProps 方法, 用不存在参数。
- 记住,此情况一定要展示不存在的状态
[id].js
import { route } from 'next/dist/next-server/server/router';
import { useRouter } from 'next/router'
export default function Post({ data }) {
const router = useRouter()
if(router.isFallback) return <div>loading.............</div>
return (
<div>
<h1>{data.id}</h1>
<h2>{data.title}</h2>
</div>
);
}
export async function getStaticPaths() {
return {
paths: [
{
params: { id: "1" },
},
{
params: { id: "2" },
},
],
fallback: true,
};
}
export async function getStaticProps({ params }) {
const { id } = params;
let data = {};
switch (id) {
case "1":
data = { id: "1", title: "文章一" };
break;
case "2":
data = { id: "2", title: "文章二二二" };
break;
case "3":
data = { id: "3", title: "文章33333" };
break;
default:
break;
}
return {
props: {
data
}
}
}
- 会基于已知的参数构建2个页面
- 其他页面会在传入不存在参数的时候,才会静态生成一个静态文件
2.5.4.5 自定义 404页面
要创建自定义 404 页面,需要在 pages 文件夹中创建 404.js 文件
export default function Error () {
return <div>404 ~</div>
}
2.6 API Routes
API Routes 可以理解为接口, 客户端向服务器端发送接口请求获取接口数据
Next.js 应用允许 React 开发者编写服务端代码创建数据接口
2.6.1 如何实现 API Routes
- 在 pages/api 文件中创建 API Routes 文件, 比如 user.js
- 在文件中默认导出请求处理函数,函数有2个参数, req为请求参数, res 为响应对象
a. get post 都可以
export default function (req, res) {
res.status(200).send({id: 1, name: 'TOM'})
}
注意:当前 API Routes 可以接受任何 Http 请求方法
- 访问API Routes : localhost:3000/api/user
不要在 getStaticPaths 或 getStaticProps 函数中访问 API Routes, 因为这两个函数就是在服务器端运行的,可以直接写服务器端代码
2.7 movie 项目
2.7.1 创建项目
npm init next-app movie
cd movie
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion
npm install react-icons
npm install @emotion/babel-preset-css-prop -D
npm install @babel/core
npm run dev
访问: localhost:3000
pages/_app.js
// import '../styles/globals.css'
import { ChakraProvider } from '@chakra-ui/react'
import theme from '@chakra-ui/theme'
function MyApp({ Component, pageProps }) {
return <ChakraProvider theme={theme}>
<Component {...pageProps} />
</ChakraProvider>
}
export default MyApp
2.7.2 启动数据服务
npm install
npm run dev
数据服务地址IP: localhost:3005
在 axiosConfig.js 文件中导出 baseURL = ‘http://localhost:3005’
2.7.3 代码
2.7.4 生成静态文件
npm run export
serve out
2.7.5 自定义 next 服务
package.json
"mydev": "nodemon server/index.js",
server/index.js
const express = require('express')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({dev})
const handler = app.getRequestHandler()
// prepare 方法是准备一下 next 应用
app.prepare().then(() => {
const server = express()
server.get('/hello', (req, res) => {
res.send('Hello Next.js')
})
server.get('*', (req, res) => {
handler(req, res)
})
server.listen(3000, () => console.log('服务器启动成功,请访问: http://localhost:3000'))
})
2.7.6 部署到 Vercel
3 Gatsby
Gatsby 是一个静态站点生成器官网