nextjs实战-服务端渲染项目从0开发到发布上线那些事

一、项目初始化

1、什么事服务端渲染?

服务端渲染(Server-Side Rendering,SSR)是一种将页面的渲染过程从客户端移动到服务器端的技术。传统的客户端渲染(Client-Side Rendering,CSR)在用户访问页面时,会先下载 HTML、CSS 和 JavaScript 文件,然后通过 JavaScript 在客户端完成页面的渲染。

而服务端渲染则是在服务器端生成完整的 HTML 页面,然后再将其发送给客户端。服务器端执行一部分或全部的页面渲染工作,包括数据获取、模板渲染等,最终生成带有动态内容的完整 HTML 页面返回给客户端。客户端接收到的页面已经包含了初始化的内容,用户可以更快地看到页面的完整内容和交互功能。

相对于客户端渲染,服务端渲染有以下几个主要优势:

  1. 首屏加载速度更快:由于服务器端已经在渲染过程中生成了完整的 HTML 页面,可以直接发送给客户端,用户无需等待 JavaScript 文件下载和执行,可以更快地看到页面内容。
  2. 更好的 SEO:搜索引擎爬虫可以直接抓取到完整的 HTML 页面内容,能够更好地索引和理解页面的信息,对搜索引擎优化(SEO)更友好。
  3. 更好的用户体验:用户在等待页面加载完成时不会看到空白页面或加载中的状态,可以更快地与页面进行交互,提升用户体验。
  4. 更好的可访问性:对于一些无法执行 JavaScript 的环境,如搜索引擎爬虫、屏幕阅读器等,服务端渲染可以提供可用的内容。

需要注意的是,服务端渲染也有一些局限性。相比客户端渲染,它对服务器的压力更大,因为服务器需要完成渲染和数据获取等工作。同时,由于服务端渲染会在每次请求时都重新生成完整的 HTML 页面,页面的状态不会像客户端渲染那样被保留,可能需要额外的开发工作来处理页面状态的恢复和持久化。

2、nextjs是怎么处理ssr的?

Next.js 是一个流行的 React 框架,它内置了对服务端渲染(SSR)的支持。下面是 Next.js 处理 SSR 的基本工作流程:

  1. 页面请求:当用户在浏览器中请求一个页面时,服务器接收到请求,并交给 Next.js 运行时处理。
  2. 数据获取:Next.js 中的页面可以通过在 getServerSidePropsgetStaticProps 方法中获取数据。这些方法会在服务器端执行,可以用于从数据库、API 或其他数据源获取数据。
  3. 页面渲染:在数据获取完成后,服务器使用获取到的数据和页面组件生成一个完整的 HTML 页面。
  4. 客户端交互:服务器将生成的 HTML 页面发送给浏览器,用户可以看到具有初始化内容的页面。同时,Next.js 会将页面的 JavaScript 代码发送给浏览器,以便后续的客户端交互。
  5. 客户端渲染:一旦页面的 JavaScript 代码在浏览器中加载和执行,页面的进一步渲染和交互将由客户端处理。这意味着页面可以在客户端通过 React 组件进行动态更新和交互。

Next.js 使用基于 React 的组件模型来构建页面和布局,并提供了一些生命周期函数和特殊方法来处理服务端渲染。getServerSideProps 方法在每次页面请求时都会执行,可用于从服务器获取数据。与之对应的是 getStaticProps 方法,它在构建时静态生成页面,并将静态数据预先注入到页面中。选择使用哪种方法取决于你的应用需求和数据更新频率。

3、如何开始一个nextjs工程?

首先要保证您的电脑安装了nodejs,并且版本大于16.14

js 复制代码
// 执行初始化命令
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的代码

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

先执行下面命令安装上面所有包

js 复制代码
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是我们的跟文件,起代码分别如下

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;
js 复制代码
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文件其代码如下

js 复制代码
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文件其代码如下:

js 复制代码
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,我这里给出一个示例

js 复制代码
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给出的方案,我这里就不贴代码了,可以直接访问下面的链接

antd官方链接

github链接

3.5、封装axios

在src文件夹下面创建requerst文件夹

在文件夹下面创建axios.ts文件

其代码如下:

js 复制代码
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文件,这会是我们子路由的上层文件,代码如下:

js 复制代码
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文件

js 复制代码
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作为跟文件

js 复制代码
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图

js 复制代码
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的代码了

js 复制代码
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证书,给出来做个参考吧

js 复制代码
#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...

相关推荐
持久的棒棒君14 分钟前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_8572979125 分钟前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
undefined&&懒洋洋1 小时前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者3 小时前
React 19 新特性详解
前端
小程xy3 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
随云6323 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6323 小时前
WebGL编程指南之进入三维世界
前端·webgl
无知的小菜鸡3 小时前
路由:ReactRouter
react.js
寻找09之夏4 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
多多米10055 小时前
初学Vue(2)
前端·javascript·vue.js