Next.js 是一个用于构建 React 应用程序的 React 框架。它的目标是使 React 应用的开发变得更简单、更灵活。下面是一些 Next.js 的关键特性:
服务器渲染 (SSR): Next.js 支持服务器渲染,这意味着页面可以在服务器上生成,然后再发送到浏览器,有助于提高应用程序的性能和搜索引擎优化(SEO)。
静态生成 (Static Generation): 除了服务器渲染外,Next.js 还支持静态生成,可以在构建时预先生成页面,然后将它们作为静态文件提供,这对于构建性能高效的静态网站非常有用。
自动代码拆分 (Automatic Code Splitting): Next.js 会自动将应用程序的代码拆分成小块,只加载当前页面所需的代码,提高加载速度。
热模块替换 (Hot Module Replacement): 在开发模式下,Next.js 支持热模块替换,允许在运行时更新代码,无需重新加载整个页面。
使用Next.js+React,实现一个SSR服务器渲染的博客项目
1.安装lint
pnpm i eslint -D -w
2.初始化
npx eslint --init
3.手动安装其他包
pnpm i -D -w typesript @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
4.修改eslint 配置
{
"env": {
"browser": true,
"es2021": true,
"node": true,
"jest": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"prettier/prettier": "error",
"no-case-declarations": "off",
"no-constant-condition": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-var-requires": "off",
"no-unused-vars": "off"
}
}
5.安装ts lint
pnpm i -D -w @typescript-eslint/eslint-plugin
1.安装prettier
pnpm i -D -w prettier
2.新建.pretterrc.json
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": true,
"singleQuote": true,
"semi": true,
"trailingComma": "none",
"bracketSpacing": true
}
3.将pretter集成到eslint中
pnpm i -D -w eslint-config-prettier eslint-plugin-prettier
4.在scripts中增加lint命令
"lint": "eslint --ext .ts,.jsx,.tsx --fix --quiet ./packages"
5.安装eslint pretter两个vscode插件
6.在vscode settings中设置format:pretter和 on save
1.安装husky
pnpm i -D -w husky
2.初始化husky
npx husky install
3.将lint增加到husky中
npx husky add .husky/pre-commit "pnpm lint "
在commit的时候会执行pnpm lint
1.安装包
pnpm i -D -w commitlint @commitlint/cli @commitlint/config-conventional
2.新建.commitlintrc.js
module.exports = {
extends: ['@commitlint/config-conventional']
};
3.集成到husky中
在终端执行下面命令
npx husky add .husky/commit-msg "npx --no-install commitlint -e $HUSKY_GIT_PARAMS"
在根目录新建tsconfig.json
{
"compileOnSave": true,
"include": ["./packages/**/*"],
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": false,
"skipLibCheck": true,
"baseUrl": "./packages",
"paths": {
"hostConfig": ["./react-dom/src/hostConfig.ts"]
}
}
}
这样,我们的项目开发环境就配置好了。
看下面这张图:
从上图可以看到
在pages目录下来创建文件夹,文件夹的名称就代表路由。俗称约定式路由。现在很多框架都支持约定式路由,比如Umi框架。
1.比如pages/index.js,那么这个的路由就是 根路由
2.比如在pages下面新建 blog文件夹,在blog文件夹下面新建index.js,那此时这个文件对应的页面利用就是/blog
1.在pages目录下新建blog目录,在blog目录下新建first-post.js,注意此时不是index.js,那此时的文件夹是嵌套的,那么对应的路由也是嵌套的,路由也是根据嵌套的文件夹的名称而来,所以这个first-post.js文件页面对应的路由就是/blog/first-post
动态路由在实际业务中非常常见,接下来看下next.js中提供的动态路由。
1.在pages目录下新建blog文件夹,在文件夹下 新建 id.js,这个 id 就表示是动态路由,那展现的路由就是这个样子 /blog/:id ,这个里面的 :id 可以换成任意的路由,例如 /blog/1 , /blog/2
2.第二种是动态路由在中间,在pages目录下新建 id 文件夹,在id文件夹下面 创建setting.js, 那此时的动态路由就是 /:id/setting, :id 就是动态,例如 /1/setting, /2/setting
3.第三种动态路由是 任意匹配的路由,在pages目录下新建post文件夹,在post文件夹下面新建...all.js,此时这个 ...all表现的动态路由就是 /post/ ,这个 就代表任意路由,丽日: /post/2020/id/title
我们开始实现整体页面的布局。这里来讲解如何实现Layout布局,采用上中下的布局。
上中下的布局就是:上方 就是 导航区域,中间是内容区域,下方是 底部区域。
整个系统使用 Antd Design UI组件库。
我们先安装下 antd design
pnpm install antd
mkdir components
cd components
mkdir layout
touch index.tsx
2.在compoents 文件夹 新建Navbar文件夹,在Navbar文件夹新建index.tsx,同时创建index.module.scss
cd components
mkdir Navbar
cd Navbar
touch index.tsx
touch index.module.scss
3.在compoents 文件夹 新建Footer文件夹,在Footer文件夹新建index.tsx,同时创建index.module.scss
cd components
mkdir Footer
cd Footer
touch index.tsx
touch index.module.scss
这样先把Layout,Navbar, Footer的架子 搭建起来。
然后开始写 Layout的布局
在 layout/index.tsx中写入, 中间的内容区域,由 props的children来填充,这样的话 ,就实现了 上中下的布局
import type { NextPage } from 'next';
import Navbar from 'components/Navbar';
import Footer from 'components/Footer';
const Layout: NextPage = ({ children }) => {
return (
<div>
<Navbar />
<main>{children}</main>
<Footer />
</div>
)
}
export default Layout;
写好上面代码以后,需要再入口文件引入 layout
import Layout from 'components/layout'
import { NextPage } from 'next';
return (
<Layout>
<Component />
</Layout>
);
接下来 来开发 上部导航区域
先看下要实现的效果图,如下:这里采用 flex 布局
<div className={styles.navbar}>
<section className={styles.logoArea}>BLOG</section>
</div>
2.然后开始写标签,这几个标签,采用配置的方式,这里我们再 Navbar文件夹下新建 config.ts 来 存放 这几个导航数据
interfacee NavProps {
label: string;
value: string;
}
export const navs: NavProps[] = [
{
label: '首页',
value: '/',
},
{
label: '咨询',
value: '/info',
},
{
label: '标签',
value: '/tag',
},
];
3.在Navbar/index.tsx拿到config中的导航数据,然后遍历渲染出来。
同时引入 next提供的link,来进行路由跳转
import Link from 'next/link';
import { navs } from './config';
<section className={styles.linkArea}>
{navs?.map((nav) => (
<Link key={nav?.label} href={nav?.value}>
<a className={pathname === nav?.value ? styles.active : ''}>
{nav?.label}
</a>
</Link>
))}
</section>
4.最后再添加两个 写文章 和登录的按钮
<section className={styles.operationArea}>
<Button onClick={handleGotoEditorPage}>写文章</Button>
<Button type="primary" onClick={handleLogin}>
登录
</Button>
</section>
5.最后整体的样式文件如下:
.navbar {
height: 60px;
background-color: #fff;
border-bottom: 1px solid #f1f1f1;
display: flex;
align-items: center;
justify-content: center;
.logoArea {
font-size: 30px;
font-weight: bolder;
margin-right: 60px;
}
.linkArea {
a {
font-size: 18px;
padding: 0 20px;
color: #515767;
}
.active {
color: #1e80ff;
}
}
.operationArea {
margin-left: 150px;
button {
margin-right: 20px;
}
}
}
这样 导航部分的 初始页面就完成了。
接下来简单写下Footer部分
在 components/Footer/index.tsx中写入如下代码:
import type { NextPage } from 'next';
import styles from './index.module.scss';
const Footer: NextPage = () => {
return (
<div className={styles.footer}>
<p>博客系统</p>
</div>
);
};
export default Footer;
样式文件代码:
.footer {
text-align: center;
color: #72777b;
padding: 20px;
}
这样简单的footer部分就完成了
最后看下 这样写下来的效果
接下来我们要开发登录模块的开发,首先看下效果图:
1.首先在components创建Login文件夹,在Login文件夹创建index.tsx文件和index.modules.scss
cd components
mkdir Login
cd Login
touch index.tsx
touch index.module.scss
2.在Navbar组件中的 登录按钮 添加点击事件
<Button type="primary" onClick={handleLogin}>
登录
</Button>
3.定义一个state来控制 登录弹窗 是否显示。
const [isShowLogin, setIsShowLogin] = useState(false);
4.将isShowLogin 当做 props 传入 登录组件
<Login isShowLogin={isShowLogin} />
5.接下来开发登录弹窗的布局代码
return isShow ? (
<div className={styles.loginArea}>
<div className={styles.loginBox}>
<div className={styles.loginTitle}>
<div>手机号登录</div>
<div className={styles.close} onClick={handleClose}>
x
</div>
</div>
<input
name="phone"
type="text"
placeholder="请输入手机号"
value={form.phone}
onChange={handleFormChange}
/>
<div className={styles.verifyCodeArea}>
<input
name="verify"
type="text"
placeholder="请输入验证码"
value={form.verify}
onChange={handleFormChange}
/>
<span className={styles.verifyCode} onClick={handleGetVerifyCode}>
{isShowVerifyCode ? (
<CountDown time={10} onEnd={handleCountDownEnd} />
) : (
'获取验证码'
)}
</span>
</div>
<div className={styles.loginBtn} onClick={handleLogin}>
登录
</div>
<div className={styles.otherLogin} onClick={handleOAuthGithub}>
使用 Github 登录
</div>
<div className={styles.loginPrivacy}>
注册登录即表示同意
<a
href="https://moco.imooc.com/privacy.html"
target="_blank"
rel="noreferrer"
>
隐私政策
</a>
</div>
</div>
</div>
) : null;
6.对应的样式代码如下:
.loginArea {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
width: 100vw;
height: 100vh;
background-color: rgb(0 0 0 / 30%);
.loginBox {
width: 320px;
height: 320px;
background-color: #fff;
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 20px;
input {
width: 100%;
height: 37px;
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
border: 1px solid #888;
outline: none;
}
input:focus {
border: 1px solid #1e80ff;
}
.verifyCodeArea {
position: relative;
cursor: pointer;
.verifyCode {
color: #1e80ff;
position: absolute;
right: 20px;
top: 8px;
font-size: 14px;
}
}
}
.loginTitle {
font-size: 20px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.close {
color: #888;
cursor: pointer;
}
}
.loginBtn {
height: 40px;
line-height: 40px;
border-radius: 5px;
margin-top: 15px;
background-color: #007fff;
color: #fff;
text-align: center;
cursor: pointer;
}
.otherLogin {
margin-top: 15px;
font-size: 14px;
color: #1e80ff;
cursor: pointer;
}
.loginPrivacy {
margin-top: 10px;
color: #333;
font-size: 14px;
a {
color: #1e80ff;
}
}
}
接下来 编写 点击逻辑
1.首先 当点击关闭的时候,把弹窗关闭
使用 props 中的 onClose 方法,onClose方法在父组件 Navbar 通过isShowLogin控制隐藏
// Login/index.tsx
const { onClose } = props;
const handleClose = () => {
onClose && onClose();
};
在入口引入
<Login isShow={isShowLogin} onClose={handleClose} />
const handleClose = () => {
setIsShowLogin(false);
};
接下来开始编写 获取验证码的 逻辑
获取验证码 需要提前编写一个倒计时的组件
接下来开始编写 倒计时组件
cd components
mkdir CountDown
cd CountDown
touch index.tsx
touch index.module.scss
在index.tsx中编写如下代码:
思路是: 提供一个 time,表示倒计时的时间。提供一个onEnd回调函数,表示当倒计时结束的时候,进行一些回调处理。
这里需要注意下, 当 time时间为0的时候,需要主动 调 一些 onEnd,表示结束。
import { useState, useEffect } from 'react';
import styles from './index.module.scss';
interface IProps {
time: number;
onEnd: Function;
}
const CountDown = (props: IProps) => {
const { time, onEnd } = props;
const [count, setCount] = useState(time || 60);
useEffect(() => {
const id = setInterval(() => {
setCount((count) => {
if (count === 0) {
clearInterval(id);
onEnd && onEnd();
return count;
}
return count - 1;
});
}, 1000);
return () => {
clearInterval(id);
};
}, [time, onEnd]);
return <div className={styles.countDown}>{count}</div>;
};
export default CountDown;
这样完成了倒计时组件的开发。接着编写获取验证码的逻辑。
1.首先 通过 isShowVerifyCode 控制 显示 验证码文字 还是倒计时
<span className={styles.verifyCode} onClick={handleGetVerifyCode}>
{isShowVerifyCode ? (
<CountDown time={10} onEnd={handleCountDownEnd} />
) : (
'获取验证码'
)}
</span>
2.接着当点击 获取验证码的时候,校验一下 手机号是否输入, 如果手机号没有输入,提示用户输入手机号
<span className={styles.verifyCode} onClick={handleGetVerifyCode}>获取验证码</span>
const handleGetVerifyCode = () => {
if (!form?.phone) {
message.warning('请输入手机号');
return;
}
}
3.如果 手机号输入,则开始 调 获取验证码的接口
const handleGetVerifyCode = () => {
if (!form?.phone) {
message.warning('请输入手机号');
return;
}
request
.post('/api/user/sendVerifyCode', {
to: form?.phone,
templateId: 1,
})
.then((res: any) => {
if (res?.code === 0) {
setIsShowVerifyCode(true);
} else {
message.error(res?.msg || '未知错误');
}
});
};
接下来开始编辑 获取 验证码 接口的逻辑
这里采用 云 的 验证码接口
1.根据 云的 接入文档,拼成url
const session: ISession = req.session;
const { to = '', templateId = '1' } = req.body;
const AppId = 'xxx'; // 接入自己的AppId
const AccountId = 'xxx'; // 接入自己的AccountId
const AuthToken = 'xxx'; // 接入自己的AuthToken
const NowDate = format(new Date(), 'yyyyMMddHHmmss');
const SigParameter = md5(`${AccountId}${AuthToken}${NowDate}`);
const Authorization = encode(`${AccountId}:${NowDate}`);
const verifyCode = Math.floor(Math.random() * (9999 - 1000)) + 1000;
const expireMinute = '5';
const url = `https://xxx.com:8883/2013-12-26/Accounts/${AccountId}/SMS/TemplateSMS?sig=${SigParameter}`;
2.使用request调用接口,参数 to 代表手机号,templateId 代表是 通过手机号进行登录,appId和datas按文档传入
const response = await request.post(
url,
{
to,
templateId,
appId: AppId,
datas: [verifyCode, expireMinute],
},
{
headers: {
Authorization,
},
}
);
3.获取 response,根据response 进行处理。当接口调用成功的时候,将验证码保存到session中,同时返回200状态码和成功的数据,当失败的时候,返回失败的原因
const { statusCode, templateSMS, statusMsg } = response as any;
if (statusCode === '000000') {
session.verifyCode = verifyCode;
await session.save();
res.status(200).json({
code: 0,
msg: statusMsg,
data: {
templateSMS
}
});
} else {
res.status(200).json({
code: statusCode,
msg: statusMsg
});
}
4.当验证码调成功的时候,显示 倒计时
request
.post('/api/user/sendVerifyCode', {
to: form?.phone,
templateId: 1,
})
.then((res: any) => {
if (res?.code === 0) {
setIsShowVerifyCode(true);
} else {
message.error(res?.msg || '未知错误');
}
});
效果如下:
开始倒计时,并成功收到验证码
当成功获取验证码,然后开始进行登录
在用户输入手机号和验证码,点击登录按钮的时候,去调用登录的接口
接口为:/api/user/login
传入表单数据,当成功的时候 将 用户的信息 存入到 store中,并且调用 onClose 将弹窗关闭
const handleLogin = () => {
request
.post('/api/user/login', {
...form,
identity_type: 'phone',
})
.then((res: any) => {
if (res?.code === 0) {
// 登录成功
store.user.setUserInfo(res?.data);
onClose && onClose();
} else {
message.error(res?.msg || '未知错误');
}
});
};
接下来开始编写 登录接口的逻辑
1.首先从session中获取验证码
const session: ISession = req.session;
2.从body中获取传入的验证码
const { phone = '', verify = '', identity_type = 'phone' } = req.body;
3.比较两个验证码是否相等,如果不相等,则返回 验证码错误
4.如果两个验证码相等,则去用户表中查找,判断用户是否存在,如果用户不存在,则表示注册,如果存在,则表示登录。
// 验证码正确,在 user_auths 表中查找 identity_type 是否有记录
const userAuth = await userAuthRepo.findOne(
{
identity_type,
identifier: phone,
},
{
relations: ['user'],
}
);
5.当用户存在的时候,从数据库中读取除用户信息,存入session和cookie中,并将用户信息返回
// 已存在的用户
const user = userAuth.user;
const { id, nickname, avatar } = user;
session.userId = id;
session.nickname = nickname;
session.avatar = avatar;
await session.save();
setCookie(cookies, { id, nickname, avatar });
res?.status(200).json({
code: 0,
msg: '登录成功',
data: {
userId: id,
nickname,
avatar,
},
});
6.当用户不存在的时候,将输入的信息 存入到数据库,session和 cookie中,表示用户注册,返回用户信息
// 新用户,自动注册
const user = new User();
user.nickname = `用户_${Math.floor(Math.random() * 10000)}`;
user.avatar = '/images/avatar.jpg';
user.job = '暂无';
user.introduce = '暂无';
const userAuth = new UserAuth();
userAuth.identifier = phone;
userAuth.identity_type = identity_type;
userAuth.credential = session.verifyCode;
userAuth.user = user;
const resUserAuth = await userAuthRepo.save(userAuth);
const {
user: { id, nickname, avatar },
} = resUserAuth;
session.userId = id;
session.nickname = nickname;
session.avatar = avatar;
await session.save();
setCookie(cookies, { id, nickname, avatar });
res?.status(200).json({
code: 0,
msg: '登录成功',
data: {
userId: id,
nickname,
avatar,
},
});
点击登录,即可登录成功。
我们这里使用typeorm数据库
首先在根目录创建db文件夹,在db文件建创建entity文件夹,entity存放各个模块的表模型
在db文件夹创建index.ts,用来导出各个模块的表模型
新建db/entity/user.ts
1.Entity指定数据库中的哪个数据表,这里指定 users 数据表
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity({name: 'users'})
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
readonly id!: number;
@Column()
nickname!: string;
@Column()
avatar!: string;
@Column()
job!: string;
@Column()
introduce!: string;
}
2.使用typeorm链接mysql
3.从typeorm引入
import { Connection, getConnection, createConnection } from 'typeorm';
4.引入数据表
import { User, UserAuth, Article, Comment, Tag } from './entity/index';
5.链接mysql数据库
import 'reflect-metadata';
import { Connection, getConnection, createConnection } from 'typeorm';
import { User, UserAuth, Article, Comment, Tag } from './entity/index';
const host = process.env.DATABASE_HOST;
const port = Number(process.env.DATABASE_PORT);
const username = process.env.DATABASE_USERNAME;
const password = process.env.DATABASE_PASSWORD;
const database = process.env.DATABASE_NAME;
let connectionReadyPromise: Promise<Connection> | null = null;
console.log('username', username)
export const prepareConnection = () => {
if (!connectionReadyPromise) {
connectionReadyPromise = (async () => {
try {
const staleConnection = getConnection();
await staleConnection.close();
} catch (error) {
console.log(error);
}
const connection = await createConnection({
type: 'mysql',
host,
port,
username,
password,
database,
entities: [User, UserAuth, Article, Comment, Tag],
synchronize: false,
logging: true,
},6.
return connection;
})();
}
return connectionReadyPromise;
};
6.在接口侧 引入数据库
import { prepareConnection } from 'db/index';
const db = await prepareConnection();
7.引入数据表,使用db获取 指定的数据表,userAuthRepo来操作mysql
import { User, UserAuth } from 'db/entity/index';
const db = await prepareConnection();
const userAuthRepo = db.getRepository(UserAuth);
8.从users表查询数据
const userAuth = await userAuthRepo.findOne(
{
identity_type,
identifier: phone,
},
{
relations: ['user'],
}
);
9.如果userAuth 有数据,则表示登录,没有数据则表示注册
10.如果是登录,从user中获取当前用户的信息,将这些信息一方面存入session,一方面存入cookie,最后返回200状态码,同时将用户信息返回
11.如果是注册,将这些输入的用户信息,存入users表中,同时将这些信息存入到session和cookie中,同时返回200状态码和这些用户信息
if (userAuth) {
// 已存在的用户
const user = userAuth.user;
const { id, nickname, avatar } = user;
session.userId = id;
session.nickname = nickname;
session.avatar = avatar;
await session.save();
setCookie(cookies, { id, nickname, avatar });
res?.status(200).json({
code: 0,
msg: '登录成功',
data: {
userId: id,
nickname,
avatar,
},
});
} else {
// 新用户,自动注册
const user = new User();
user.nickname = `用户_${Math.floor(Math.random() * 10000)}`;
user.avatar = '/images/avatar.jpg';
user.job = '暂无';
user.introduce = '暂无';
const userAuth = new UserAuth();
userAuth.identifier = phone;
userAuth.identity_type = identity_type;
userAuth.credential = session.verifyCode;
userAuth.user = user;
const resUserAuth = await userAuthRepo.save(userAuth);
const {
user: { id, nickname, avatar },
} = resUserAuth;
session.userId = id;
session.nickname = nickname;
session.avatar = avatar;
await session.save();
setCookie(cookies, { id, nickname, avatar });
res?.status(200).json({
code: 0,
msg: '登录成功',
data: {
userId: id,
nickname,
avatar,
},
});
}
1.当点击 写文章的时候,先判断用户是否登录,如果没有登录,则提示用户先登录,如果已经登录,则跳到新建文章页面
<Button onClick={handleGotoEditorPage}>写文章</Button>
const handleGotoEditorPage = () => {
if (userId) {
push('/editor/new');
} else {
message.warning('请先登录');
}
};
2.在pages目录下创建 editor/new.tsx,表示 新建文章的页面
3.首先编写 markdown编辑器,这里使用 开源的一款markdown编辑器,@uiw/react-md-editor
安装
yarn add @uiw/react-md-editor
4.引入编辑器
const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });
import '@uiw/react-md-editor/markdown-editor.css';
import '@uiw/react-markdown-preview/markdown.css';
<MDEditor />
5.定义state表示编辑器的内容
const [content, setContent] = useState('');
<MDEditor value={content} height={1080} />
6.添加change事件
<MDEditor value={content} height={1080} onChange={handleContentChange} />
const handleContentChange = (content: any) => {
setContent(content);
};
7.添加 输入标题 组件
const [title, setTitle] = useState('');
const handleTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
setTitle(event?.target?.value);
};
<Input
className={styles.title}
placeholder="请输入文章标题"
value={title}
onChange={handleTitleChange}
/>
8.添加 标签选择 组件
<Select
className={styles.tag}
mode="multiple"
allowClear
placeholder="请选择标签"
onChange={handleSelectTag}
>{allTags?.map((tag: any) => (
<Select.Option key={tag?.id} value={tag?.id}>{tag?.title}</Select.Option>
))}</Select>
9.新增 state 控制 标签
const [allTags, setAllTags] = useState([]);
10.添加 选择 标签的 事件
const handleSelectTag = (value: []) => {
setTagIds(value);
}
11.新建 标签的 数据表
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { User } from './user'
import { Article } from './article'
@Entity({name: 'tags'})
export class Tag extends BaseEntity {
@PrimaryGeneratedColumn()
readonly id!: number;
@Column()
title!: string;
@Column()
icon!: string;
@Column()
follow_count!: number;
@Column()
article_count!: number;
@ManyToMany(() => User, {
cascade: true
})
@JoinTable({
name: 'tags_users_rel',
joinColumn: {
name: 'tag_id'
},
inverseJoinColumn: {
name: 'user_id'
}
})
users!: User[]
@ManyToMany(() => Article, (article) => article.tags)
@JoinTable({
name: 'articles_tags_rel',
joinColumn: {
name: 'tag_id'
},
inverseJoinColumn: {
name: 'article_id'
}
})
articles!: Article[]
}
新增 获取所有标签的接口,新建 api/tag/get.ts
1.从session中获取用户信息
2.从tag表 查询 所有 标签数据
3.关联users表,根据users表,查询所有标签,返回allTags
4.关联User表,根据当前登录用户的信息,查询该用户 关注的标签,返回followTags
import { NextApiRequest, NextApiResponse } from 'next';
import { withIronSessionApiRoute } from 'iron-session/next';
import { ironOptions } from 'config/index';
import { ISession } from 'pages/api/index';
import { prepareConnection } from 'db/index';
import { Tag } from 'db/entity/index';
export default withIronSessionApiRoute(get, ironOptions);
async function get(req: NextApiRequest, res: NextApiResponse) {
const session: ISession = req.session;
const { userId = 0 } = session;
const db = await prepareConnection();
const tagRepo = db.getRepository(Tag);
const followTags = await tagRepo.find({
relations: ['users'],
where: (qb: any) => {
qb.where('user_id = :id', {
id: Number(userId),
});
},
});
const allTags = await tagRepo.find({
relations: ['users'],
});
res?.status(200)?.json({
code: 0,
msg: '',
data: {
followTags,
allTags,
},
});
}
5.在editor/new.tsx中 调 获取 标签的接口拿到标签数据
useEffect(() => {
request.get('/api/tag/get').then((res: any) => {
if (res?.code === 0) {
setAllTags(res?.data?.allTags || [])
}
})
}, []);
最后渲染 所有标签
<Select
className={styles.tag}
mode="multiple"
allowClear
placeholder="请选择标签"
onChange={handleSelectTag}
>{allTags?.map((tag: any) => (
<Select.Option key={tag?.id} value={tag?.id}>{tag?.title}</Select.Option>
))}</Select>
这样页面就出来了,也获取到了markdown,标签,标题的数据
然后开始写发布文章:
1.先判断是否输入标题,如果没有输入标题,就提示用户输入标题
2.然后调 发布文章的接口,参数就是 标题,markdown数据,标签
3.当接口调取成功的时候,提示发布成功,并跳到用户中心 的页面
4.当接口调取失败的时候,提示发布失败
const handlePublish = () => {
if (!title) {
message.warning('请输入文章标题');
return ;
}
request.post('/api/article/publish', {
title,
content,
tagIds
}).then((res: any) => {
if (res?.code === 0) {
userId ? push(`/user/${userId}`) : push('/');
message.success('发布成功');
} else {
message.error(res?.msg || '发布失败');
}
})
};
现在写下 发布文章的接口
新建 api/artice/publish.ts
1.引入数据库和user, tag, article三张数据表
import { prepareConnection } from 'db/index';
import { User, Article, Tag } from 'db/entity/index';
2.链接三个数据表
const db = await prepareConnection();
const userRepo = db.getRepository(User);
const articleRepo = db.getRepository(Article);
const tagRepo = db.getRepository(Tag);
3.从req.body中获取传入的参数
const { title = '', content = '', tagIds = [] } = req.body;
4.从session中获取用户信息
const session: ISession = req.session;
5.根据session从user表中查询当前用户信息
const user = await userRepo.findOne({
id: session.userId,
});
6.根据传入的标签,获取所有的标签
const tags = await tagRepo.find({
where: tagIds?.map((tagId: number) => ({ id: tagId })),
});
7.将传入的数据 存入到 article表中, 如果有用户信息,将用户信息也存入表,并且标签数量增加
const article = new Article();
article.title = title;
article.content = content;
article.create_time = new Date();
article.update_time = new Date();
article.is_delete = 0;
article.views = 0;
if (user) {
article.user = user;
}
if (tags) {
const newTags = tags?.map((tag) => {
tag.article_count = tag?.article_count + 1;
return tag;
});
article.tags = newTags;
}
const resArticle = await articleRepo.save(article);
if (resArticle) {
res.status(200).json({ data: resArticle, code: 0, msg: '发布成功' });
} else {
res.status(200).json({ ...EXCEPTION_ARTICLE.PUBLISH_FAILED });
}
这样就完成了文章发布
nextjs 提供 getServerSideProps 来获取数据,返回到props中,然后在react组件中通过props获取数据进行渲染,达到ssr效果。
1.引入数据库和tag,article两张表
import { prepareConnection } from 'db/index';
import { Article, Tag } from 'db/entity';
2.链接数据库
const db = await prepareConnection();
3.根据 关联的 user和tag查询出 所有 文章
const articles = await db.getRepository(Article).find({
relations: ['user', 'tags'],
});
4.根据 关联的 user 查询出 标签
const tags = await db.getRepository(Tag).find({
relations: ['users'],
});
5.最后将 文章和标签通过props返回
return {
props: {
articles: JSON.parse(JSON.stringify(articles)) || [],
tags: JSON.parse(JSON.stringify(tags)) || [],
},
};
6.在react组件中 通过props获取 文章和标签
const { articles = [], tags = [] } = props;
7.默认将 获取的 文章,存放到所有文章的state中
const [showAricles, setShowAricles] = useState([...articles]);
8.然后渲染当前所有的文章
<div className="content-layout">
{showAricles?.map((article) => (
<>
<DynamicComponent article={article} />
<Divider />
</>
))}
</div>
9.上面的文章列表通过 异步加载的方式加载
const DynamicComponent = dynamic(() => import('components/ListItem'));
10.新建 components/ListItem/index.tsx components/ListItem/index.module.scss
通过 props 可以获取到 从 父组件传过来的 article和 user信息
拿到这两个信息后,将这两个字段里面的内容 渲染处理即可
需要注意的是,需要点击谋篇文章的时候,跳转到该文章的详情页面,所以需要使用 Link
另外一个需要注意的地方是,渲染文章的时候,文章是markdown格式
所以使用 markdown-to-txt 第三方包 来加载 markdown格式的数据
所以代码是这样的
import Link from 'next/link';
import { formatDistanceToNow } from 'date-fns';
import { IArticle } from 'pages/api/index';
import { Avatar } from 'antd';
import { EyeOutlined } from '@ant-design/icons';
import { markdownToTxt } from 'markdown-to-txt';
import styles from './index.module.scss';
interface IProps {
article: IArticle;
}
const ListItem = (props: IProps) => {
const { article } = props;
const { user } = article;
return (
// eslint-disable-next-line @next/next/link-passhref
<Link href={`/article/${article.id}`}>
<div className={styles.container}>
<div className={styles.article}>
<div className={styles.userInfo}>
<span className={styles.name}>{user?.nickname}</span>
<span className={styles.date}>
{formatDistanceToNow(new Date(article?.update_time))}
</span>
</div>
<h4 className={styles.title}>{article?.title}</h4>
<p className={styles.content}>{markdownToTxt(article?.content)}</p>
<div className={styles.statistics}>
<EyeOutlined />
<span className={styles.item}>{article?.views}</span>
</div>
</div>
<Avatar src={user?.avatar} size={48} />
</div>
</Link>
);
};
export default ListItem;
11.css代码
.container {
margin: 0 atuo;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
cursor: pointer;
.article {
width: 90%;
.userInfo {
margin-bottom: 10px;
display: flex;
align-items: center;
span {
padding: 0 10px;
border-right: 1px solid #e5e6eb;
}
span:first-of-type {
padding-left: 0;
}
span:last-of-type {
border-right: 0;
}
.name {
color: #4e5969;
}
.name:hover {
text-decoration: underline;
color: #1e80ff;
}
.date {
color: #86909c;
}
}
.title {
font-size: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content {
font-size: 16px;
color: #86909c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.statistics {
display: flex;
align-items: center;
.item {
margin-left: 5px;
}
}
}
}
看下效果:
这里需要使用 nextjs中的动态路由
1.在pages/article 新建 id.tsx,表示 文章详情页的入口文件
同时新建 pages/article/index.module.scss
2.通过 url 获取 文章的 id字段
3.然后根据通过ssr获取文章详情数据
4.根据id 去数据表中查询当前文章的详情
5.这里增加一个功能,就是浏览次数,当前查询的时候,浏览次数增加 1 次
整体代码如下:
export async function getServerSideProps({ params }: any) {
const articleId = params?.id;
const db = await prepareConnection();
const articleRepo = db.getRepository(Article);
const article = await articleRepo.findOne({
where: {
id: articleId,
},
relations: ['user', 'comments', 'comments.user'],
});
if (article) {
// 阅读次数 +1
article.views = article?.views + 1;
await articleRepo.save(article);
}
return {
props: {
article: JSON.parse(JSON.stringify(article)),
},
};
}
通过以上 ssr 代码就拿到了 当前文章的数据
然后渲染这些基本信息
这里 markdown的内容 使用 markdown-to-jsx 第三方库 来加载
<div className="content-layout">
<h2 className={styles.title}>{article?.title}</h2>
<div className={styles.user}>
<Avatar src={avatar} size={50} />
<div className={styles.info}>
<div className={styles.name}>{nickname}</div>
<div className={styles.date}>
<div>
{format(new Date(article?.update_time), 'yyyy-MM-dd hh:mm:ss')}
</div>
<div>阅读 {article?.views}</div>
</div>
</div>
</div>
<MarkDown className={styles.markdown}>{article?.content}</MarkDown>
</div>
接着增加 是否显示编辑的逻辑
通过store拿到 当前登录的用户信息
const store = useStore();
const loginUserInfo = store?.user?.userInfo;
当 用户登录的时候,显示编辑按钮
并且 点击 编辑 按钮 跳转到 文章 编辑页面
{Number(loginUserInfo?.userId) === Number(id) && (
<Link href={`/editor/${article?.id}`}>编辑</Link>
)}
因为 编辑文章是编辑不同的文章,所以这里需要 使用动态 路由
1.首先新建 pages/editor/id.tsx 和 index.module.scss
2.编辑文章 首先 需要 把 当前的文章详情 回显到页面上
这里通过 url 获取 当前 文章的 id,然后通过ssr渲染的方式进行渲染
3.根据 文章 id 和 关联的 用户表,链接 文章的 数据表,查询出来 属于 当前用户发布的这篇文章
最后将 查询出来的 文章详情返回
export async function getServerSideProps({ params }: any) {
const articleId = params?.id;
const db = await prepareConnection();
const articleRepo = db.getRepository(Article);
const article = await articleRepo.findOne({
where: {
id: articleId,
},
relations: ['user'],
});
return {
props: {
article: JSON.parse(JSON.stringify(article)),
},
};
}
在react客户端组件中,通过props获取article数据
1.将 文章标题,文章内容通过state来控制,初始值是props获取的数据
const [title, setTitle] = useState(article?.title || '');
const [content, setContent] = useState(article?.content || '');
2.通过 useRouter hooks 获取 文章Id
const { push, query } = useRouter();
const articleId = Number(query?.id)
3.将获取的文章数据渲染出来
return (
<div className={styles.container}>
<div className={styles.operation}>
<Input
className={styles.title}
placeholder="请输入文章标题"
value={title}
onChange={handleTitleChange}
/>
<Select
className={styles.tag}
mode="multiple"
allowClear
placeholder="请选择标签"
onChange={handleSelectTag}
>{allTags?.map((tag: any) => (
<Select.Option key={tag?.id} value={tag?.id}>{tag?.title}</Select.Option>
))}</Select>
<Button
className={styles.button}
type="primary"
onClick={handlePublish}
>
发布
</Button>
</div>
<MDEditor value={content} height={1080} onChange={handleContentChange} />
</div>
);
4.修改标题,通过state控制
const handleTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
setTitle(event?.target?.value);
};
5.修改 文章内容的时候,也是通过state控制
const handleContentChange = (content: any) => {
setContent(content);
};
6.这里 新增一个 获取所有标签的接口
首先 调用 标签接口,将标签数据存到state中
useEffect(() => {
request.get('/api/tag/get').then((res: any) => {
if (res?.code === 0) {
setAllTags(res?.data?.allTags || [])
}
})
}, []);
接下来编写下 获取标签的接口
新建 pages/api/tag/get.ts
1.首先通过session获取当前用户信息
const session: ISession = req.session;
const { userId = 0 } = session;
2.链接 标签的数据表
const db = await prepareConnection();
const tagRepo = db.getRepository(Tag);
3.根据当前关联的用户表,查询出来所有标签
const allTags = await tagRepo.find({
relations: ['users'],
});
4.根据用户id查询出来 当前用户关注的标签
const followTags = await tagRepo.find({
relations: ['users'],
where: (qb: any) => {
qb.where('user_id = :id', {
id: Number(userId),
});
},
});
5.最后将所有的标签 和 当前用户 关注的 标签 返回
res?.status(200)?.json({
code: 0,
msg: '',
data: {
followTags,
allTags,
},
});
6.在客户端 拿到 所有标签数据后渲染出来
<Select
className={styles.tag}
mode="multiple"
allowClear
placeholder="请选择标签"
onChange={handleSelectTag}
>{allTags?.map((tag: any) => (
<Select.Option key={tag?.id} value={tag?.id}>{tag?.title}</Select.Option>
))}</Select>
1、当点击更新的时候,首先判断一下 是否 输入了标题,如果没有输入标题,则提示用户输入标题
if (!title) {
message.warning('请输入文章标题');
return ;
}
2、然后传参数调用更新文章的接口
3、传的参数包括 文章id、标题、内容、标签
4、当调用更新文章接口成功的时候提示更新文章成功并跳到当前文章
5、如果失败,则提示发布失败
request.post('/api/article/update', {
id: articleId,
title,
content,
tagIds
}).then((res: any) => {
if (res?.code === 0) {
articleId ? push(`/article/${articleId}`) : push('/');
message.success('更新成功');
} else {
message.error(res?.msg || '发布失败');
}
})
6、接着编写 更新文章的接口,新建 pages/api/article/update.ts
7、通过body获取 前端传过来的数据
const { title = '', content = '', id = 0, tagIds = [] } = req.body;
8、链接文章和标签的数据库
const articleRepo = db.getRepository(Article);
const tagRepo = db.getRepository(Tag);
9、根据文章的id,关联用户表和标签表,查询出来当前文章
const article = await articleRepo.findOne({
where: {
id,
},
relations: ['user', 'tags'],
});
10、判断查询出来的article是否存在,如果不存在,则提示文章不存在
res.status(200).json({ ...EXCEPTION_ARTICLE.NOT_FOUND });
11、如果存在,则将传过来的文章数据 覆盖之前的数据,如果保存成功,则提示成功,否则提示失败
if (article) {
article.title = title;
article.content = content;
article.update_time = new Date();
article.tags = newTags;
const resArticle = await articleRepo.save(article);
if (resArticle) {
res.status(200).json({ data: resArticle, code: 0, msg: '更新成功' });
} else {
res.status(200).json({ ...EXCEPTION_ARTICLE.UPDATE_FAILED });
}
}
12、这里需要根据传过来的标签id,查询出来所有标签,然后将标签数量加1
const tags = await tagRepo.find({
where: tagIds?.map((tagId: number) => ({ id: tagId })),
});
const newTags = tags?.map((tag) => {
tag.article_count = tag.article_count + 1;
return tag;
});
13、最后记得将 需要的 第三方库引入进来
import { NextApiRequest, NextApiResponse } from 'next';
import { withIronSessionApiRoute } from 'iron-session/next';
import { ironOptions } from 'config/index';
import { prepareConnection } from 'db/index';
import { Article, Tag } from 'db/entity/index';
import { EXCEPTION_ARTICLE } from 'pages/api/config/codes';
这样就完成了编辑文章的前后端开发。
1.首先 先编写 发布评论 和评论列表的页面,只有登录的用户才能发布评论,所以这里有个判断,判断只有获取到用户的信息,才显示 发布评论的 按钮
const store = useStore();
const loginUserInfo = store?.user?.userInfo;
{loginUserInfo?.userId && (
<div className={styles.enter}>
<Avatar src={avatar} size={40} />
<div className={styles.content}>
<Input.TextArea
placeholder="请输入评论"
rows={4}
value={inputVal}
onChange={(event) => setInputVal(event?.target?.value)}
/>
<Button type="primary" onClick={handleComment}>
发表评论
</Button>
</div>
</div>
)}
2.然后 获取 所有的 评论 列表,渲染到页面上
<div className={styles.display}>
{comments?.map((comment: any) => (
<div className={styles.wrapper} key={comment?.id}>
<Avatar src={comment?.user?.avatar} size={40} />
<div className={styles.info}>
<div className={styles.name}>
<div>{comment?.user?.nickname}</div>
<div className={styles.date}>
{format(
new Date(comment?.update_time),
'yyyy-MM-dd hh:mm:ss'
)}
</div>
</div>
<div className={styles.content}>{comment?.content}</div>
</div>
</div>
))}
</div>
这里 有 两个逻辑接口,一个是 发布评论的接口,一个是 获取所有评论数据的接口
首先 编写 发布评论的接口
1.首先获取 参数,一个参数是文章的id,一个是评论的内容
2.将这两个参数 传给 发布评论的接口
post('/api/comment/publish', {
articleId: article?.id,
content: inputVal,
})
3.接下来 看下 发布评论的接口
4.新建 pages/api/comment/publish.ts
5.引入 数据库 和 session的配置
import { NextApiRequest, NextApiResponse } from 'next';
import { withIronSessionApiRoute } from 'iron-session/next';
import { ironOptions } from 'config/index';
import { ISession } from 'pages/api/index';
import { prepareConnection } from 'db/index';
import { User, Article, Comment } from 'db/entity/index';
import { EXCEPTION_COMMENT } from 'pages/api/config/codes';
6.通过 传过来的参数 获取 文章id 和 评论的内容
const { articleId = 0, content = '' } = req.body;
7.链接 评论接口的 数据库
const db = await prepareConnection();
const commentRepo = db.getRepository(Comment);
const comment = new Comment();
8.实例化 Comment类,根据 session信息,从users表中查询 当前用户,根据文章id,查询文章信息,将这些信息全部添加到 comment实例中,保存到 comment表中
const comment = new Comment();
comment.content = content;
comment.create_time = new Date();
comment.update_time = new Date();
const user = await db.getRepository(User).findOne({
id: session?.userId,
});
const article = await db.getRepository(Article).findOne({
id: articleId,
});
if (user) {
comment.user = user;
}
if (article) {
comment.article = article;
}
const resComment = await commentRepo.save(comment);
9.如果保存成功,则提示发布成功,否则提示发布失败
if (resComment) {
res.status(200).json({
code: 0,
msg: '发表成功',
data: resComment,
});
} else {
res.status(200).json({
...EXCEPTION_COMMENT.PUBLISH_FAILED,
});
}
10.当调用发布接口成功的时候,提示发布成功,并且将新发布的评论 添加到 评论列表中,显示在评论中。同时把评论框的内容清空。注意这个将 新发布的评论 添加到 评论列表的时候,使用react的不可变原则,使用concat方法。
request
.post('/api/comment/publish', {
articleId: article?.id,
content: inputVal,
})
.then((res: any) => {
if (res?.code === 0) {
message.success('发表成功');
const newComments = [
{
id: Math.random(),
create_time: new Date(),
update_time: new Date(),
content: inputVal,
user: {
avatar: loginUserInfo?.avatar,
nickname: loginUserInfo?.nickname,
},
},
].concat([...(comments as any)]);
setComments(newComments);
setInputVal('');
} else {
message.error('发表失败');
}
});
11.最后拿到最新的 评论列表,将评论列表 遍历 渲染到页面上
<div className={styles.display}>
{comments?.map((comment: any) => (
<div className={styles.wrapper} key={comment?.id}>
<Avatar src={comment?.user?.avatar} size={40} />
<div className={styles.info}>
<div className={styles.name}>
<div>{comment?.user?.nickname}</div>
<div className={styles.date}>
{format(
new Date(comment?.update_time),
'yyyy-MM-dd hh:mm:ss'
)}
</div>
</div>
<div className={styles.content}>{comment?.content}</div>
</div>
</div>
))}
</div>
首先 新建 pages/tag/index.tsx和 pages/tag/index.module.scss分别 存放 标签的 页面和样式
这个页面 我们采用 csr的方式来渲染页面,看看和ssr渲染页面的方式有何不同
在这个页面 我们设计成 全部标签 和关注的标签,页面效果如下:
首先 我们 先 编写接口, 来获取 全部标签和已关注的标签
新建 pages/api/tag/get.ts
1.首先 引入 数据库等的配置
import { NextApiRequest, NextApiResponse } from 'next';
import { withIronSessionApiRoute } from 'iron-session/next';
import { ironOptions } from 'config/index';
import { ISession } from 'pages/api/index';
import { prepareConnection } from 'db/index';
import { Tag } from 'db/entity/index';
2.通过 session 获取 当前用户的id,因为我们需要根据用户id获取该用户的标签数据
const { userId = 0 } = session;
3.链接 标签 数据库的 配置
const db = await prepareConnection();
const tagRepo = db.getRepository(Tag);
4.首先 获取 全部标签的数据,这个我们只需要 根据 关联 的用户表去 标签的 数据表 查询即可
const allTags = await tagRepo.find({
relations: ['users'],
});
5.接下来 获取 关注的标签,关注的标签逻辑是,根据当前用户的id去查询标签数据,这样获取的数据就是该用户关注的标签数据
const followTags = await tagRepo.find({
relations: ['users'],
where: (qb: any) => {
qb.where('user_id = :id', {
id: Number(userId),
});
},
});
6.最后将 获取的 所有标签数据 和 关注的标签数据 返回
res?.status(200)?.json({
code: 0,
msg: '',
data: {
followTags,
allTags,
},
});
7.接下来 我们在客户端 使用 csr的方式 来获取 全部标签和已关注的标签数据。同followTags和allTags来分别存储全部标签数据和已关注的标签数据
const [followTags, setFollowTags] = useState<ITag[]>();
const [allTags, setAllTags] = useState<ITag[]>();
useEffect(() => {
request('/api/tag/get').then((res: any) => {
if (res?.code === 0) {
const { followTags = [], allTags = [] } = res?.data || {};
setFollowTags(followTags);
setAllTags(allTags);
}
})
}, [needRefresh]);
8.接下来 来渲染 全部标签的数据,这里有个逻辑,就是 显示 关注 还是已关注。当 当前用户id 能够在 接口返回的users中返回的id中能够找打,则表明 当前用户 已关注了 这个标签,则页面上显示 已关注,否则显示关注。当显示已关注的时候,按钮事件则是 取消关注的逻辑,否则则是 关注的逻辑。
<TabPane tab="全部标签" key="all" className={styles.tags}>
{
allTags?.map(tag => (
<div key={tag?.title} className={styles.tagWrapper}>
<div>{(ANTD_ICONS as any)[tag?.icon]?.render()}</div>
<div className={styles.title}>{tag?.title}</div>
<div>{tag?.follow_count} 关注 {tag?.article_count} 文章</div>
{
tag?.users?.find((user) => Number(user?.id) === Number(userId)) ? (
<Button type='primary' onClick={() => handleUnFollow(tag?.id)}>已关注</Button>
) : (
<Button onClick={() => handleFollow(tag?.id)}>关注</Button>
)
}
</div>
))
}
</TabPane>
9.首先 编写 关注 标签的逻辑,新建 pages/api/tag/follow.ts
10.首先引入数据库配置
import { NextApiRequest, NextApiResponse } from 'next';
import { withIronSessionApiRoute } from 'iron-session/next';
import { ironOptions } from 'config/index';
import { ISession } from 'pages/api/index';
import { prepareConnection } from 'db/index';
import { Tag, User } from 'db/entity/index';
import { EXCEPTION_USER, EXCEPTION_TAG } from 'pages/api/config/codes';
export default withIronSessionApiRoute(follow, ironOptions);
11.从session获取用户的id
const session: ISession = req.session;
const { userId = 0 } = session;
12.从 body中获取 前端传过来的参数,一共两个参数,一个type,值分别是follow和unfollow,表示是取消关注还是关注,另外一个参数数标签的id
const { tagId, type } = req?.body || {};
13.链接 标签和用户的数据库
const db = await prepareConnection();
const tagRepo = db.getRepository(Tag);
const userRepo = db.getRepository(User);
14.根据用户id去用户表中查询该用户信息,如果没找到,则提示当前用户不存在
const user = await userRepo.findOne({
where: {
id: userId,
},
});
if (!user) {
res?.status(200).json({
...EXCEPTION_USER?.NOT_LOGIN,
});
return;
}
15.根据标签id从标签的数据表中查询所有标签
const tag = await tagRepo.findOne({
relations: ['users'],
where: {
id: tagId,
},
});
16.如果从标签表中查询出有用户,如果类型是follow,则表示是关注操作,则将当前用户添加到 关注该标签的用户数据中,并且将关注该标签的数据增加1,如果类型是unfollow,则表示取消关注操作,则将当前用户从 关注该标签的用户数据中剔除,并且将关注该标签的数据减1.
if (tag?.users) {
if (type === 'follow') {
tag.users = tag?.users?.concat([user]);
tag.follow_count = tag?.follow_count + 1;
} else if (type === 'unfollow') {
tag.users = tag?.users?.filter((user) => user.id !== userId);
tag.follow_count = tag?.follow_count - 1;
}
}
17.最后将 标签的数据存入 标签的数据表中,如果成功,则返回200,否则提示失败
if (tag) {
const resTag = await tagRepo?.save(tag);
res?.status(200)?.json({
code: 0,
msg: '',
data: resTag,
});
} else {
res?.status(200)?.json({
...EXCEPTION_TAG?.FOLLOW_FAILED,
});
}
18.在前端点击关注的时候,传入两个参数,一个参数是type,值为follw,另外一个参数是标签id,如果接口成功,在前端提示关注成功,并且重新调标签的数据,刷新页面
request.post('/api/tag/follow', {
type: 'follow',
tagId
}).then((res: any) => {
if (res?.code === 0) {
message.success('关注成功');
setNeedRefresh(!needRefresh);
} else {
message.error(res?.msg || '关注失败');
}
})
19.取消关注,则是将type参数的值改成 unfollow。
这样完成了标签管理功能。
首先看下页面的效果
接下来 就按照 这个设计 来编写代码
个人中心页面,我们使用ssr的方式来渲染
1.首先引入数据库等的配置
/* eslint-disable @next/next/link-passhref */
import React from 'react';
import Link from 'next/link';
import { observer } from 'mobx-react-lite';
import { Button, Avatar, Divider } from 'antd';
import {
CodeOutlined,
FireOutlined,
FundViewOutlined,
} from '@ant-design/icons';
import ListItem from 'components/ListItem';
import { prepareConnection } from 'db/index';
import { User, Article } from 'db/entity';
2.通过ssr的方式获取用户信息和文章相关的数据
3.根据url获取当前用户的id
const userId = params?.id;
4.根据当前用户的id查询 从用户表中查询当前用户的信息
const user = await db.getRepository(User).findOne({
where: {
id: Number(userId),
},
});
5.根据用户id以及关联的用户表和标签表查询相关联的文章
const articles = await db.getRepository(Article).find({
where: {
user: {
id: Number(userId),
},
},
relations: ['user', 'tags'],
});
6.最后将上面两个数据返回
return {
props: {
userInfo: JSON.parse(JSON.stringify(user)),
articles: JSON.parse(JSON.stringify(articles)),
},
};
7.在前端 通过 props 拿到 数据
const { userInfo = {}, articles = [] } = props;
8.获取 全部文章的 总浏览数
const viewsCount = articles?.reduce(
(prev: any, next: any) => prev + next?.views,
0
);
9.最后将 所有的数据渲染出来
<div className={styles.userDetail}>
<div className={styles.left}>
<div className={styles.userInfo}>
<Avatar className={styles.avatar} src={userInfo?.avatar} size={90} />
<div>
<div className={styles.nickname}>{userInfo?.nickname}</div>
<div className={styles.desc}>
<CodeOutlined /> {userInfo?.job}
</div>
<div className={styles.desc}>
<FireOutlined /> {userInfo?.introduce}
</div>
</div>
<Link href="/user/profile">
<Button>编辑个人资料</Button>
</Link>
</div>
<Divider />
<div className={styles.article}>
{articles?.map((article: any) => (
<div key={article?.id}>
<ListItem article={article} />
<Divider />
</div>
))}
</div>
</div>
<div className={styles.right}>
<div className={styles.achievement}>
<div className={styles.header}>个人成就</div>
<div className={styles.number}>
<div className={styles.wrapper}>
<FundViewOutlined />
<span>共创作 {articles?.length} 篇文章</span>
</div>
<div className={styles.wrapper}>
<FundViewOutlined />
<span>文章被阅读 {viewsCount} 次</span>
</div>
</div>
</div>
</div>
</div>
10.这里有个地方是 编辑 个人资料的 入口,点击 跳转到 编辑个人资料的页面
<Link href="/user/profile">
<Button>编辑个人资料</Button>
</Link>
首先看下 编辑个人资料的页面
这里的逻辑就是 首先 从接口 获取当前用户的信息,然后修改个人信息,最后 保存修改。
1.首先通过接口获取用户信息
useEffect(() => {
request.get('/api/user/detail').then((res: any) => {
if (res?.code === 0) {
console.log(333333);
console.log(res?.data?.userInfo);
form.setFieldsValue(res?.data?.userInfo);
}
});
}, [form]);
2.接着将用户信息渲染到表单中
return (
<div className="content-layout">
<div className={styles.userProfile}>
<h2>个人资料</h2>
<div>
<Form
{...layout}
form={form}
className={styles.form}
onFinish={handleSubmit}
>
<Form.Item label="用户名" name="nickname">
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item label="职位" name="job">
<Input placeholder="请输入职位" />
</Form.Item>
<Form.Item label="个人介绍" name="introduce">
<Input placeholder="请输入个人介绍" />
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
保存修改
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
);
3.最后调用保存修改的接口 将 修改后的数据 更新到 数据表中
const handleSubmit = (values: any) => {
console.log(99999);
console.log(values);
request.post('/api/user/update', { ...values }).then((res: any) => {
if (res?.code === 0) {
message.success('修改成功');
} else {
message.error(res?.msg || '修改失败');
}
});
};
最后我们使用vercel进行部署,体验地址:博客系统
通过这篇文章,我们实操了全栈博客系统开发。
我们应用了前后端技术栈:
· Next.js+React
· Typescript
· Antd
· Node
· MySQL
提高了全栈开发能力:
· 掌握数据表设计基本思想
· 掌握Next.js框架的使用
理解并应用SSR同构原理:
· 前端注水及页面接管
· 服务端渲染及数据预取
希望这篇文章能够带你进入全栈开发。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。