第三期 - 前端模版开发
第三期 - 前端模版开发
面试吧笔记第三期 - 前端模板开发
开发前端服务端渲染网站模板,并完成面试吧 Web 前端的部分基础页面。
1. 服务端渲染
1.1 什么是客户端和服务端渲染

1. 客户端渲染(Client-Side Rendering,CSR):
在客户端渲染模式下,服务器返回一个基本的 HTML 页面结构和一些必要的 JavaScript 和 CSS 文件。然后,客户端的浏览器通过执行 JavaScript 代码来请求数据,并根据数据动态生成页面内容。
这种模式下,页面的渲染过程主要由客户端的浏览器完成,可以实现更丰富的交互和动态内容。
但是,初始加载时需要下载和执行大量的 JavaScript 代码,页面加载速度相对较慢,对搜索引擎的可访问性较差。
2. 服务端渲染(Server-Side Rendering,SSR):
在服务端渲染模式下,服务器在接收到客户端请求后,会将页面的初始 HTML 内容生成并发送给客户端。客户端接收到 HTML 后,进行解析和渲染,最终呈现给用户。
在这种模式下,页面的大部分内容在服务器端已经渲染完成,因此页面加载速度相对较快,并且对搜索引擎友好。
但是,交互式的内容和功能需要等待客户端的 JavaScript 代码执行完成后才能实现。
相关信息
客户端渲染和服务端渲染的主要区别在于渲染过程发生的地点。
1.2 客户端渲染
CSR 的过程如下:
- 客户端发送请求:客户端浏览器发送页面请求给服务器。
- 服务器处理请求:服务器接收到请求后,返回一个包含基本 HTML 结构和 JavaScript 的响应。
- 客户端下载资源:客户端浏览器接收到响应后,开始下载所需的 JavaScript 文件和其他静态资源。
- 客户端执行 JavaScript:客户端浏览器解析并执行下载的 JavaScript 文件,生成页面的 DOM 结构。
- 数据获取和处理:客户端通过 JavaScript 发起异步请求,从服务器获取所需的数据。
- 模板渲染和内容填充:客户端使用获取到的数据,通过 JavaScript 操作 DOM,将数据填充到页面中的相应位置。
- 页面展示:客户端浏览器根据最终的 DOM 结构和样式,渲染并展示页面给用户。
CSR 的优点包括:
- 更快的页面切换速度:由于只需要获取基本的 HTML 结构和 JavaScript 文件,页面切换时可以更快地加载和渲染。
- 减轻服务器压力:服务器只需提供基本的 HTML 结构和 JavaScript 文件,不需要进行页面渲染和数据处理,减轻了服务器的压力。
- 适用于复杂的交互和动态内容:通过异步请求和前端 JavaScript 处理,可以实现复杂的交互和动态内容,提供更好的用户体验。
CSR 的缺点包括:
- 首屏加载速度较慢:由于需要下载和执行 JavaScript 文件,页面的首次加载速度相对较慢,用户可能在加载过程中看到空白页面。
- 不利于搜索引擎优化:由于初次加载的页面内容较少,搜索引擎难以理解和索引页面的全部内容,对搜索引擎优化不友好。
- 对浏览器兼容性要求较高:CSR 需要客户端浏览器支持 JavaScript,并且不同浏览器对 JavaScript 的解析和执行可能存在差异,对兼容性要求较高。
CSR 通过在客户端浏览器执行 JavaScript,实现了快速的页面切换和复杂交互,但也面临首屏加载速度较慢和搜索引擎优化不友好的问题。在具体项目中,需要综合考虑需求和限制,选择合适的渲染模式。
1.3 服务端渲染
服务端渲染的过程如下:
- 客户端发送请求:客户端浏览器发送页面请求给服务器。
- 服务器处理请求:服务器接收到请求后,根据请求的路由和数据,执行相应的处理逻辑。
- 数据获取和处理:服务器从数据库或其他数据源中获取所需的数据,并进行必要的处理和转换。
- 模板渲染:服务器使用获取到的数据和事先定义好的模板引擎,将数据填充到模板中,生成完整的 HTML 页面。
- 返回渲染结果:服务器将渲染好的 HTML 页面作为响应返回给客户端浏览器。
- 客户端解析和渲染:客户端浏览器接收到 HTML 响应后,进行解析和渲染,最终呈现给用户。
SSR 的优点包括:
- 更快的首屏加载速度:由于服务器在渲染过程中已经将页面的大部分内容生成,用户在访问网站时可以快速看到页面的内容,提升了用户体验。
- 更好的搜索引擎优化:搜索引擎可以直接获取到完整的 HTML 页面,可以更好地理解和索引网页内容,提高网站在搜索结果中的排名。
- 更好的可访问性:由于服务器端已经生成了完整的 HTML 页面,即使客户端浏览器不支持 JavaScript 或 JavaScript 出错,用户仍然可以正常访问和浏览网页内容。
SSR 的缺点包括:
- 服务器压力较大:由于页面的渲染逻辑在服务器端执行,需要服务器进行更多的计算和处理,对服务器的性能要求较高。
- 复杂的交互和动态内容有延迟:复杂的交互和动态内容需要等待客户端的 JavaScript 代码执行完成后才能实现,可能会有一定的延迟。
- 开发复杂度较高:SSR 需要在服务器端和客户端进行不同的处理和逻辑,开发和维护的复杂度相对较高。
SSR 通过在服务器端生成页面的初始 HTML 内容,提供了更快的加载速度和更好的搜索引擎优化,但也面临服务器压力大和开发复杂度高的挑战。在具体项目中,需要综合考虑需求和限制,选择合适的渲染模式。
在 SSR 模式下,服务器会执行页面的渲染逻辑,包括数据获取、模板渲染和路由处理等。服务器会将渲染好的 HTML 页面发送给客户端,客户端浏览器接收到 HTML 后,进行解析和渲染,最终呈现给用户。
1.4 其他渲染方式 - 静态网站生成
静态网站生成(Static Site Generation,SSG)是一种在构建阶段生成静态 HTML 文件的技术。与服务端渲染不同,静态网站生成是在构建时(而不是用户请求时)生成页面,所有页面都以静态文件的形式存在。
这种方式本质上也是客户端渲染,但是不需要由客户端再动态地向后端发送请求来获取数据,这些静态文件可以直接由内容分发网络(CDN)或静态服务器提供。
优点:
- 高性能:由于服务器仅需提供静态文件,性能极高;而且由于数据不变化,特别适合通过 CDN 缓存加速。
- SEO 友好:搜索引擎最喜欢的就是静态 HTML 文件,可以轻松索引并提升 SEO 效果。
- 简化基础设施:无需复杂的前后端交互逻辑,静态文件的部署和维护成本较低。
缺点:
- 动态内容有限:SSG 适合内容变化不频繁的场景,对于需要实时更新内容的网站,生成静态页面可能不够灵活
- 构建时间:生成大量静态页面时,构建时间可能较长,特别是数据量大的时候。
基于这些优缺点,静态网站生成适合内容数量有限的、内容基本不变的网站,比如个人博客。像 VuePress、Hugo、Hexo、Astro 都是主流的静态网站生成器。
随着静态网站内容越来越多,每次构建会越来越慢。在这种情况下,可以采用增量静态生成技术。
增量静态生成(Incremental Static Regeneration,ISR)允许部分页面在构建之后进行更新,而无需重新构建整个站点。这种技术适用于那些大多数内容不变、但某些部分需要动态更新的网站。
工作流程:
- 在构建阶段,生成初始的静态页面。
- 当页面内容更新时,通过配置的再生成间隔,静态页面可以增量更新,而不是重新生成整个站点,大幅减少构建时间。
- 用户请求时,如果页面内容过期或更新,则后台自动生成新的静态页面并缓存。
这样一来,可以在既能享受静态网站高性能、SEO 友好特性的同时,又能及时更新网站的内容,并减少构建时间。
不过缺点就是架构更复杂、维护成本更高。但值得一提的是,很多大型网站为了做 SEO 优化,专门把动态网站转为静态 HTML(静态化)。
1.5 结合使用(推荐)
实际情况下,前面讲到的几种方式可以结合使用。
比如 部分预渲染(Partial Prerendering,PPR)是一种将服务端渲染(或静态生成)与客户端渲染结合的技术。
工作流程:
- 在构建阶段或请求阶段,页面的静态部分预先渲染(如导航栏、页脚等)。
- 页面加载时,静态部分直接显示,动态部分由 JavaScript 在客户端加载并渲染。
- 通过水合(Hydration)过程,客户端的 JavaScript 接管已经渲染的静态内容,并继续处理动态交互。
这样一来,就可以兼具 SSR 的 SEO 友好和快速初始加载、以及 CSR 灵活动态交互的优点。

同构渲染
还有一个跟部分预渲染相似的概念叫 同构渲染,是指同一套代码可以在服务端和客户端运行,并在服务端渲染页面的初始内容,然后在客户端接管渲染和交互。
2. 开发 Next.js 前端万用模板
自主打造一套前端万用模版。
2.1 确认环境
Next.js 官方文档:https://nextjs.org/docs/app/getting-started/installation
注意:官方文档才是最新版,国内的中文文档不够新!
本项目我们使用的是 14 版本的 Next.js,可以看到 Node.js 的版本要求必须 18.18,一定要注意!
注意:Next.js 有 种开发模式。本项目用的是新的开发模式 App Router。

2.2 创建项目
直接按照官方文档的指引,使用 Npm 自带的 Npx 脚手架工具 create-next-app
来自动安装 Next.js 初始化项目:https://nextjs.org/docs/getting-started/installation#automatic-installation
执行安装命令:
npx create-next-app@latest
# 或者:npx create-next-app@14.2.6
其中,latest 表示当前脚手架的最新版本。本项目中使用的 create-next-app
脚手架版本是 14.2.6
,可以在 npm 包管理器网站 查看版本情况,可以直接把命令中的 latest 替换为 14.2.6
。
脚手架可以帮我们自动整合 React、Next.js、TypeScript 语法、ESLint 校验等库,如下所示创建前端项目:

脚手架会自动生成代码并安装依赖,如果安装依赖卡住,可能需要更换 Npm 镜像为国内源:
npm config set registry https://registry.npmmirror.com/
接着使用 WebStorm 打开项目,在终端执行 npm run dev
命令,能访问到网页就成功了。
2.3 前端工程化配置
使用脚手架创建项目后,脚手架已经帮我们配置了 ESLint 自动校验、TypeScript 类型校验,但是我们还需要手动整合代码自动格式化插件 Pretier。
注意
前端整合多个工具时,很容易出现版本冲突的问题,尤其是 ESlint 和 Prettier 整合时,校验规则可能也会存在冲突。
所以,最好按照官方文档的指引来操作。
- 先去 prettier 官网 安装 prettier,执行命令:
npm install --save-dev --save-exact prettier
- 接着按照 Next.js 整合 prettier 的官方文档:Next.js With Prettier,通过命令安装整合包 eslint-config-prettier:
npm install --save-dev eslint-config-prettier
- 再修改项目文件
.eslintrc.json
的配置:
{
"extends": ["next/core-web-vitals", "prettier"]
}
- 最后在 WebStorm 设置中开启代码美化插件:

在任意一个 tsx 文件中执行格式化快捷键(Ctrl + Alt + L),不报错则代表配置工程化成功。
修改 .eslintrc.json
文件可以改变校验规则,一般项目不需要修改,具体可以到 ESLint 和 Prettier 的官方文档中查看。
相关信息
如果不使用脚手架,就需要自己按照下面这些文档整合这些工具:
- 代码规范:https://eslint.org/docs/latest/use/getting-started
- 代码美化:https://prettier.io/docs/en/install.html
- 直接整合:https://github.com/prettier/eslint-plugin-prettier#recommended-configuration(包括了 https://github.com/prettier/eslint-config-prettier#installation)
但不推荐,自己整合很容易出错,使用脚手架可以帮助我们快速构建。
2.4 引入组件库
- Ant Design 是 React 项目主流的组件库,Ant Design Procomponents 是在此基础上进一步封装的高级业务组件库。
参考官方文档在 Next.js 项目中引入 Ant Design 5.x 版本的组件库:https://ant-design.antgroup.com/docs/react/use-with-next-cn
执行安装命令:
npm install antd --save
针对 App Router 模式的 Next.js,需要处理页面闪动的情况:
npm install @ant-design/nextjs-registry --save
修改页面全局布局文件 app/layout.tsx
:
import React from "react";
import { AntdRegistry } from "@ant-design/nextjs-registry";
const RootLayout = ({ children }: React.PropsWithChildren) => (
<html lang="en">
<body>
<AntdRegistry>{children}</AntdRegistry>
</body>
</html>
);
export default RootLayout;
此时,在任意 .tsx
文件中引入 Ant Design 即可正常显示。
- 引入 Ant Design 后,我们还可以引入 Ant Design Procomponents,参考 官方文档 使用下面命令安装:
npm i @ant-design/pro-components --save
由于 ProComponents 基于原始的 Ant Design 组件进行了更高级别的封装,所以每个组件都比较大,因此每一个组件都是一个独立的包,需要在项目中安装对应的 npm 包才能使用。
比如使用 ProTable 表格组件,还需要安装 @ant-design/pro-table
。
- 引入组件库后,可以清理掉
app/globals.css
中的样式,减少样式冲突。
修改为如下样式,减少浏览器默认样式的影响:
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
max-height: 100vh;;
}
2.5 Next.js 开发规范
对于一个新项目,统一的开发规范是至关重要的。
一般可以在官方文档中搜索 “最佳实践” 找到。比如,在 Next.js 官方文档中搜索 “best practices”,如下所示:

2.5.1 约定式路由
Next.js 使用 约定式路由,根据文件夹的结构和名称,自动将对应的 URL 地址映射到页面文件。
常见的几种路由规则如下:
- 基础规则:以
app
目录作为根路径,根据文件夹的名称和嵌套层级,自动映射为 URL 地址。

注意
只有目录下直接包含 page
文件(js、jsx、ts、tsx 都支持)才会被识别为路由。
- 路由组:可以通过
(xxx)
语法,创建一个路由组,不会被转化为路径,可用于对路由进行分组管理。比如同组路由使用同一套布局。

- 动态路由:可以通过
[xxx]
语法,让多个不同参数的 URL 复用同一个页面。
比如 app/question/[questionId]/page.tsx
中 questionId 就是动态路由参数,可以匹配 /question/1
、/question/2
等 URL 地址,在页面中可以获取到 questionId 并加载不同的题目。
export default function Page({ params }: { params: { questionId: string } }) {
return <div>我的题目: {params.questionId}</div>
}
以上只是 Next.js 的几种常用路由规则,其他规则详见 Next.js 的官方文档:https://nextjs.org/docs/app/building-your-application/routing
2.5.2 静态资源
Next.js 约定在 /public
目录下存放静态资源。在其下新建 assets
目录,可以在里面存放图片等静态资源文件。
对应官方文档:https://nextjs.org/docs/app/building-your-application/optimizing/static-assets
比如网站的 Logo,可以用 Next.js 的 Image
组件加载静态资源:
<Image src={`/assets/logo.png`} alt={alt} width="64" height="64" />
Next.js 会针对该组件进行特定的图像优化,提升性能。
注意
某些特殊的、常用的元信息文件不是放在 public
目录下,而是应该根据特定规则放在 app
目录下!
对应官方文档:https://nextjs.org/docs/app/api-reference/file-conventions/metadata
1)将 favicon.ico 放到 app
的根目录下,可展示站点小图标:

2)将 robots.txt
放到 app
的根目录下,可用于告诉搜索引擎爬虫能否访问特定的页面、以及站点地图的地址,比如:
User-Agent: *
Allow: /
Disallow: /private/
Sitemap: https://mianshiba.com/sitemap.xml
2.5.3 文件组织形式
首先,项目中的每个页面和组件都是单独的文件夹。
基于 Next.js 的约定式路由,我们每个页面目录内需要添加 page.tsx
页面文件和 index.css
样式文件;每个组件目录内添加 index.tsx
页面文件和 index.css
样式文件。
- 对于项目中多页面公用的组件,放在
src/components
目录下; - 对于某个页面私有的组件,放在该页面的
components
目录下。
2.5.4 页面开发规范
Next.js 文持 React 的语法,可以用函数的方式声明页面和组件。每个页面的根元素必须有 id、每个组件根元素必须有 className,用于控制样式和快速定位。
为了区分服务端和客户端渲染,每个页面(或组件)都必须在开头显示编写 "use client"
或 "use server"
。
2.5.5 其他注意事项
开发时要严格注意 TypeScript 的类型和编辑器的错误提示,并且定期打包构建。因为 Next.js 的构建要求非常严格,稍有不慎就会报错。构建报错的话,注意查看和处理构建中的报错信息。
在项目中慎用 window 等浏览器环境才支持的对象,服务端无法使用。注意保证客户端渲染页面和服务端渲染页面的一致性,否则会出现水合错误。
2.6 全局通用布局
所谓的全局通用布局,是指在多个页面间复用的 UI 元素,比如导航栏。
Next.js 支持全局根布局(每个页面都会生效)以及嵌套布局(可以只对部分页面生效),详情可 参考文档。

2.6.1 基础布局结构
新建 src/layouts
目录,用于存放项目中的各种布局。在该目录下新建一个布局文件夹 BasicLayout
,包括 index.tsx
页面和 index.css
样式文件。
直接使用 Ant Design Procomponents 的布局组件 快速实现包含导航栏、内容、底部栏的响应式布局。
以 基础布局示例 为基础模版:

在 app/layout.tsx
全局布局文件中引入 BasicLayout:
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh">
<body>
<AntdRegistry>
<BasicLayout>{children}</BasicLayout>
</AntdRegistry>
</body>
</html>
);
}
然后按需精简和修改 BasicLayout 中复制来的布局代码,直到项目可以正常运行并符合预期。
2.6.2 全局底部栏
新建 src/components
目录,表示全局公用组件。
在该目录下创建 GlobalFooter
文件夹,表示全局底部栏,通常用于展示版权信息。
index.tsx
如下:
"use client";
import React from "react";
import "./index.css";
/**
* 全局底部栏
* @constructor
*/
export default function GlobalFooter() {
const currentYear: number = new Date().getFullYear();
return (
<div className={"global-footer"}>
<div>© {currentYear} 面试吧</div>
<div>
<a href={"https://github.com/BraumAce/mianshiba"} target={"_blank"}>
作者:BraumAce
</a>
</div>
</div>
);
}
index.css
如下:
.global-footer {
position: absolute;
bottom: 0;
width: 100%;
background: #efefef;
text-align: center;
padding: 16px;
}
在 src/layouts/BasicLayout/index.tsx
目录下引用:
// 底部栏
footerRender={() => {
return <GlobalFooter />;
}}
2.6.3 全局顶部导航栏
可以直接利用 Ant Design Procomponents 的 ProLayout 组件实现。将其 layout 属性设置为 top
,开启顶部导航栏:
<ProLayout>
layout={"top"}
</ProLayout>
ProLayout 将顶部导航栏从左到右分为几个区:标题区、菜单区、操作区、头像区。
(1)标题区:用于展示网站图标和标题。
// 标题渲染
headerTitleRender={(logo, title, _) => {
const defaultDom = (
<a>
{logo}
{title}
</a>
);
}}
该渲染函数中有 logo
和 title
参数,可以在 Prolayout 中添加对应的属性,以展示网站的图标和标题。同理,也可以在 ProLayout 中添加其他属性,在渲染函数中进行调用。
(2)菜单区:用于展示导航栏的菜单,供用户切换页面。
// 菜单项数据
menuDataRender={() => {
return [
{
path: "/",
name: "主页",
},
{
path: "/banks",
name: "题库",
},
];
}}
// 菜单渲染
menuItemRender={(item, dom) => (
<Link href={item.path || "/"}>
{dom}
</Link>
)}
(3)操作区:用于配置右侧的操作栏,如搜索条、小按钮等。
actionsRender={(props) => {
if (props.isMobile) return [];
return [
<SearchInput key={"search"} />,
<a
key={"github"}
href={"https://github.com/BraumAce/mianshiba"}
target={"_blank"}
>
<GithubFilled key="GithubFilled" />
</a>,
];
}}
(4)头像区:用于展示登录用户头像、用户昵称,鼠标悬浮可展示更多用户操作。
2.6.4 导航菜单配置
通过添加独立的配置文件更方便地修改导航菜单项,不需要每次都修改布局代码。
- 在
config
目录下编写通用菜单配置文件menus.tsx
。
import { MenuDataItem } from "@ant-design/pro-layout";
import { CrownOutlined } from "@ant-design/icons";
// 菜单列表
export const menus: MenuDataItem[] = [
{
path: "/",
name: "主页",
},
{
path: "/banks",
name: "题库",
},
{
path: "/questions",
name: "题目",
},
{
name: "面试吧",
path: "https://mianshiba.com",
target: "_blank",
},
{
path: "/admin",
name: "管理",
icon: <CrownOutlined />,
children: [
{
path: "/admin/user",
name: "用户管理",
}
],
},
] as MenuDataItem[];
- 在全局布局的 ProLayout 中引入菜单数据:
// 菜单项数据
menuDataRender={() => {
return menus;
}}