nextjs实战-服务端渲染项目从0开发到发布上线那些事
一、项目初始化
1、什么事服务端渲染?
服务端渲染(Server-Side Rendering,SSR)是一种将页面的渲染过程从客户端移动到服务器端的技术。传统的客户端渲染(Client-Side Rendering,CSR)在用户访问页面时,会先下载 HTML、CSS 和 JavaScript 文件,然后通过 JavaScript 在客户端完成页面的渲染。
而服务端渲染则是在服务器端生成完整的 HTML 页面,然后再将其发送给客户端。服务器端执行一部分或全部的页面渲染工作,包括数据获取、模板渲染等,最终生成带有动态内容的完整 HTML 页面返回给客户端。客户端接收到的页面已经包含了初始化的内容,用户可以更快地看到页面的完整内容和交互功能。
相对于客户端渲染,服务端渲染有以下几个主要优势:
- 首屏加载速度更快:由于服务器端已经在渲染过程中生成了完整的 HTML 页面,可以直接发送给客户端,用户无需等待 JavaScript 文件下载和执行,可以更快地看到页面内容。
- 更好的 SEO:搜索引擎爬虫可以直接抓取到完整的 HTML 页面内容,能够更好地索引和理解页面的信息,对搜索引擎优化(SEO)更友好。
- 更好的用户体验:用户在等待页面加载完成时不会看到空白页面或加载中的状态,可以更快地与页面进行交互,提升用户体验。
- 更好的可访问性:对于一些无法执行 JavaScript 的环境,如搜索引擎爬虫、屏幕阅读器等,服务端渲染可以提供可用的内容。
需要注意的是,服务端渲染也有一些局限性。相比客户端渲染,它对服务器的压力更大,因为服务器需要完成渲染和数据获取等工作。同时,由于服务端渲染会在每次请求时都重新生成完整的 HTML 页面,页面的状态不会像客户端渲染那样被保留,可能需要额外的开发工作来处理页面状态的恢复和持久化。
2、nextjs是怎么处理ssr的?
Next.js 是一个流行的 React 框架,它内置了对服务端渲染(SSR)的支持。下面是 Next.js 处理 SSR 的基本工作流程:
- 页面请求:当用户在浏览器中请求一个页面时,服务器接收到请求,并交给 Next.js 运行时处理。
- 数据获取:Next.js 中的页面可以通过在
getServerSideProps
或getStaticProps
方法中获取数据。这些方法会在服务器端执行,可以用于从数据库、API 或其他数据源获取数据。 - 页面渲染:在数据获取完成后,服务器使用获取到的数据和页面组件生成一个完整的 HTML 页面。
- 客户端交互:服务器将生成的 HTML 页面发送给浏览器,用户可以看到具有初始化内容的页面。同时,Next.js 会将页面的 JavaScript 代码发送给浏览器,以便后续的客户端交互。
- 客户端渲染:一旦页面的 JavaScript 代码在浏览器中加载和执行,页面的进一步渲染和交互将由客户端处理。这意味着页面可以在客户端通过 React 组件进行动态更新和交互。
Next.js 使用基于 React 的组件模型来构建页面和布局,并提供了一些生命周期函数和特殊方法来处理服务端渲染。getServerSideProps
方法在每次页面请求时都会执行,可用于从服务器获取数据。与之对应的是 getStaticProps
方法,它在构建时静态生成页面,并将静态数据预先注入到页面中。选择使用哪种方法取决于你的应用需求和数据更新频率。
3、如何开始一个nextjs工程?
首先要保证您的电脑安装了nodejs,并且版本大于16.14
// 执行初始化命令
npx create-next-app@latest
// 选择推荐
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias? No / Yes
What import alias would you like configured? @/*
// 等待安装完毕既可以获得一个服务端渲染的雏形
命令可参考下面我的生成放生,因为我这里要做一个基于rrweb监控用户记录,上报项目错误的一个实验性项目,所以我直接将文件夹命名为了rrweb
我们等待完成即可
二、项目配置完善,让他更适合我们的编程习惯
当我们的项目下载完成后,我们进入项目文件夹,此时运行npm run dev,如果顺利的话你会打开一个本地为3000端口的服务,我们访问localhsot:3000,就可以看到我们的项目了,如下图所示:
到这里我们已经初始化完毕了我们的项目,接下来我们需要为开发增加一些必要配置
1、增加接口代理
我们都知道本地开发调试有跨域的问题,为了解决这个问题,我们需要在本地初始化一个serve服务,做一层代理转发 nextjs给le我们一种配置方案,可参考nextjs.org/docs/app/ap… 在配置中做代理,我这里使用另外一种方式,因为nextjs内置了nodejs服务,所以我们借助node模块去做这层代理
2、增加代理配置,修改命令启动方式
首先让我们在根目录下创建serve.js,这是我们的服务启动时的代理文件
安装express,http-proxy-middleware cross-env执行下面命令
npm install --save-dev express http-proxy-middleware cross-env
安装完毕将package.json命令做如下修改
"scripts": { "dev": "cross-env APP_ENV=development node serve.js", "build": "cross-env APP_ENV=production next build", "start": "next start", "lint": "next lint" }
创建环境变量,这里主要是我们后边上线区分开发环境和线上环境使用
我们直接在根目录下创建两个.env文件,分别为.env.development和.env.production
然后我们定义两个变量
APP_ENV="development" NEXT_PUBLIC_API='http://175.178.xxx.169:9300/'
完善serve.js的代码
const express = require("express");
const next = require("next");
const { createProxyMiddleware } = require("http-proxy-middleware");
const devProxy = {
"/api": {
target: "http://175.178.xxx.169:9300/", // 替换为自己项目接口
pathRewrite: {
"^/api": "/",
},
changeOrigin: true,
},
};
const port = parseInt(process.env.PORT, 10) || 8001;
const dev = process.env.NODE_ENV !== "production";
const app = next({
dev,
});
const handle = app.getRequestHandler();
app
.prepare()
.then(() => {
const server = express();
if (dev && devProxy) {
Object.keys(devProxy).forEach(function (context) {
server.use(createProxyMiddleware(context, devProxy[context]));
});
}
server.all("*", (req, res) => {
handle(req, res);
});
server.listen(port, (err) => {
if (err) {
throw err;
}
console.log(`> Ready on http://localhost:${port}`);
});
})
.catch((err) => {
console.log(err);
});
添加完毕后让我们重新运行命令npm run dev,此时如果顺利我们会打开一个8001的服务,访问页面,跟我们初始化时的页面一致,这样我们就完成了本地开发时接口代理的配置
3、增加axios,react-ToolKit, antd,nookies
下面让我们增加接口请求工具包axios,react新的状态管理工具包react-toolkit,以及ui组件库antd,和nookies
先执行下面命令安装上面所有包
npm install --save axios @reduxjs/toolkit react-redux antd nookies
nookies是一个运行在服务端的包,主要是用来存储cookie,我们可以借助他实现登陆鉴权的一些逻辑
ps:本来我们的项目是使用next最新的src文件夹的结构创建,但是我在实际的操作过程中,发现配置最新的toolkit一直报错,找不到_react,无法实现我们的项目运行,修改了很多版本还是不行,于是我将项目改回了原来的pages结构
3.1、在根目录下创建pages文件夹
在该文件夹下面创建_app.tsx和_document.tsx和index.tsx _app.tsx是我们的根文件,相当于src下的layout,_document.js是我们的跟文件,起代码分别如下
import dynamic from "next/dynamic";
import React, {
useEffect,
useCallback,
Suspense,
useRef,
useState,
} from "react";
import type { AppProps } from "next/app";
import { Provider } from "react-redux";
import { store } from "../store/store";
import "../styles/globals.css";
import zhCN from "antd/locale/zh_CN";
import { ConfigProvider } from "antd";
import theme from "../styles/config";
const MyApp = ({ Component, pageProps }: AppProps) => {
return (
<Provider store={store}>
<ConfigProvider theme={theme} locale={zhCN}>
<Component {...pageProps} />
</ConfigProvider>
</Provider>
);
};
export default MyApp;
import Document, { Html, Head, NextScript, Main } from "next/document";
import { StyleProvider, createCache, extractStyle } from "@ant-design/cssinjs";
import type { DocumentContext } from "next/document";
class MyDocument extends Document {
render() {
return (
<Html>
<Head>
<meta name="description" content="全局" />
<meta name="keywords" content="全局" />
</Head>
<body>
<Main></Main>
<NextScript />
</body>
</Html>
);
}
}
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
const cache = createCache();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
(
<StyleProvider cache={cache}>
<App {...props} />
</StyleProvider>
),
});
const initialProps = await Document.getInitialProps(ctx);
const style = extractStyle(cache, true);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
<style dangerouslySetInnerHTML={{ __html: style }} />
</>
),
};
};
export default MyDocument;
3.2、在根目录下创建store文件夹
首先创建store.ts文件其代码如下
import { configureStore } from "@reduxjs/toolkit";
// ...
import userReducer from "./reducers/user";
export const store = configureStore({
reducer: {
user: userReducer,
},
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
然后创建hooks.ts文件其代码如下:
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
最后创建,reducers文件夹,在该文件下创建每个不同的reducer,我这里给出一个示例
user-reducer
import { createSlice } from "@reduxjs/toolkit";
import { setCookie } from "nookies";
export interface UserState {
token: string | null;
user: any;
}
const initialState: UserState = {
token: null, // 选中 nav 数据
user: null,
};
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {
setToken: (state, action) => {
setCookie(null, "token", action.payload, {
maxAge: 30 * 24 * 60 * 60,
path: "/",
});
state.token = action.payload;
},
setUser: (state, action) => {
state.user = action.payload;
},
},
});
export const { setToken, setUser } = userSlice.actions;
export default userSlice.reducer;
3.3、在根目录下创建components,request,styles文件夹
components文件夹主要用来放置我们的功能组件,request用来放置我们的http请求,styles用来放弃我们的一些全局样式,这里我再_app.js里面有引入
3.4、配置antd
这个很简单,具体可以完全参考antd给出的方案,我这里就不贴代码了,可以直接访问下面的链接
3.5、封装axios
在src文件夹下面创建requerst文件夹
在文件夹下面创建axios.ts文件
其代码如下:
import axios, { AxiosResponse, InternalAxiosRequestConfig } from "axios";
import { parseCookies, setCookie, destroyCookie } from "nookies";
import { alert } from "../components/alert"; // 是我封装的alert组件
// 需要走服务端的接口
const serverUrl: string[] = []; // 定义需要走服务端的接口
const service = axios.create({
baseURL: "", // 获取环境变量配置
timeout: 10000, // 设置超时时间
}); // Request interceptor
// 自定义传入的参数
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const cookies = parseCookies(); // token判断
if (cookies?.token) {
config.headers["Authorization"] = "Bearer " + cookies?.token;
}
// 是否需要走服务端
if (serverUrl.includes(config.url || "")) {
config.baseURL = process.env.NEXT_PUBLIC_API;
}
config.headers["Content-Type"] = "application/json";
return config;
},
(error: any) => {
Promise.reject(error);
}
); // Response interceptors
service.interceptors.response.use(
async (response: AxiosResponse) => {
const code = response.data.code;
if (code === 401) {
destroyCookie(null, "token");
if (typeof window !== "undefined") {
alert("error", "登录状态已过期,请重新登录");
if (typeof window !== "undefined") {
window.location.href = "/login";
}
}
} else if (code === 6401) {
destroyCookie(null, "token");
return false;
} else if (code === 400 || code === 403) {
if (typeof window !== "undefined") {
alert("error", response.data.msg);
}
} else {
return response.data;
}
},
(error: any) => {
if (error?.response?.status === 401 || error?.response?.status === 403) {
destroyCookie(null, "token");
if (typeof window !== "undefined") {
window.location.href = "/login";
}
return;
} else if (error?.code === "ERR_CANCELED") {
return;
} else {
return Promise.reject(error);
}
}
);
export default service;
三、开发页面
1、创建第一个除默认首页外的页面
首先在pages文件夹下面创建about文件夹,about文件夹下面创建index.tsx,因为nextjs采用约定式的路由 此时,我们一但按照nextjs的约定创建了文件,即表示创建了路由,好了,我们在url页面更改路由为localhost/about
此时我们就进入了about页面
2、如何创建一个有二级页面的页面
加入我们的页面需要,在一个页面下又有三个子页面,那么此时怎么办呢?
按照nextjs的约定,让我们试一下该如何创建,我们创建一个info的页面,这个页面有三个子页面
ok,首先还是一样的套路先在pages下创建info文件夹,此时注意,我们需要创建index.tsx文件,这会是我们子路由的上层文件,代码如下:
import Nav from "./nav";
function InfoLayout({
children, // will be a page or nested layout
}: {
children: React.ReactNode;
}) {
return (
<section>
<Nav></Nav>
{children}
</section>
);
}
export default InfoLayout;
在创建一个nav.tsx文件
import React, { useState, useEffect, useRef } from "react";
import Link from "next/link";
function Info() {
return (
<div>
<Link href="/info/info1">info1</Link>
<Link href="/info/info2">info2</Link>
<Link href="/info/info3">info3</Link>
</div>
);
}
export default Info;
下面创建info1文件夹,在该文件夹下创建index.tsx文件,主要需要引入跟文件下的index.tsx作为跟文件
import Layout from "../index";
function Info1() {
return <Layout>info1</Layout>;
}
export default Info1;
同样的方式创建info2,info3, 此时我们在路由中输入locahost:8001/info 就会进入info下的layout页面,即index.tsx 文件我们点击按钮info1,路由就会跳转到对应的页面
3、下面介绍一个nextjs重要的组件 Image组件
Image组件nextjs帮我们进行了优化,他具有图片懒加载,以及可以约束是否需要nextjs帮我们优化图片,我这里直接给我个例子,这个例子报错,图片不需要nextjs优化,以及给image组件增加一个loading图
function myImageLoader({ src }) {
return src;
}
const [loading, setLoading] = useState(true);
const handleImageLoaded = () => {
setLoading(false);
};
{loading && (
<img
src="../../images/failtoload.png" // 修改为诶自己的loading图片
alt="Placeholder"
style={{
position: 'absolute',
left: '4px',
top: '4px',
width: '74px',
zIndex: 10,
}}
/>
)}
<Image
loader={(src) => myImageLoader(src)}
src={src}
width={imgWidth}
height={imgHeight}
layout="responsive"
onLoad={handleImageLoaded}
alt={alt}
/>
如果有需要图片优化可以使用上面的方式
四、部署
1、我们使用docker的方式进行部署
我这里使用docker的方式对程序进行部署,这个需要有一定的docker基础,不懂的可以先简单学习下docker,很简单,我直接给我docker的代码了
FROM node:16.17.0
RUN mkdir -p www/app
WORKDIR /www/app
COPY ./ ./
ENV APP_ENV test
ENV NEXT_PUBLIC_API xxx
ENV NEXT_PUBLIC_URL xxx
RUN npm config set registry https://registry.npm.taobao.org
RUN npm install
RUN npm run build:test
EXPOSE 3000
CMD [ "npm","start" ]
2、使用nginx做接口代理
同样需要一定的nginx基础,我的工程配置如下,我这里配置了ssl证书,给出来做个参考吧
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
gzip on;
#低于1kb的资源不压缩
gzip_min_length 1k;
#压缩级别1-9,越大压缩率越高,同时消耗cpu资源也越多,建议设置在5左右。
gzip_comp_level 5;
#需要压缩哪些响应类型的资源,多个空格隔开。不建议压缩图片.
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
#配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)
gzip_disable "MSIE [1-6].";
#是否添加“Vary: Accept-Encoding”响应头
gzip_vary on;
server {
listen 8888;
server_name adminweb;
location / {
root /www/adminweb;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
server {
listen 7005;
server_name document;
location / {
root /www/document;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
server {
listen 80;
server_name messln.cn;
rewrite ^(.*)$ https://${server_name}$1 permanent;
}
server {
#SSL 默认访问端口号为 443
listen 443 ssl;
#请填写绑定证书的域名
server_name messln.cn;
#请填写证书文件的相对路径或绝对路径
ssl_certificate messln.cn_bundle.crt;
#请填写私钥文件的相对路径或绝对路径
ssl_certificate_key messln.cn.key;
# ssl_session_timeout 5m;
# #请按照以下协议配置
# ssl_protocols TLSv1.2 TLSv1.3;
# #请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。
# ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
# ssl_prefer_server_ciphers on;
location / {
proxy_pass 项目启动后地址;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}
location /api {
rewrite /api/(.*) /$1 break;
proxy_pass 代理的接口;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
}
ps:项目的css方案可选择scss.module或者less.module方案,直接安装对应的sass-loader或者less-loader即可
ok,以上就是我们基本的nextjs完整的项目配置了,如果您觉的有用,不妨留个赞再走,小小的赞,是作者持续创作的动力
git仓库地址:gitee.com/SongTaoo/ne…
转载自:https://juejin.cn/post/7278685769375285300