自建博客网站
访问地址:xiaxiang.tech
最开始使用 React 框架搭建了一套 SPA 网页,部署在 nginx 上,打包后的文件体积为 2M,放在腾讯云服务器上,下载完预计 12s 左右,体验感很差。
为了优化加载速度,优化点1: 将腾讯云的网络带宽从 1M 变为了 2M;优化点2: 使用 nextjs 框架优化加载速度,对网页资源进行分块。
使用 nextjs 中的动态路由后,无法部署静态网页,nginx 无法使用,所以打算在服务器上跑npm run start
命令启动项目。令人意外的是 1 核 1 GB 内存的服务器启动该命令后,直接产生 OOM 问题,内存溢出。云服务器内存溢出后将无法从常规方式进入控制台,远程连接会失效,需要重启服务器。
有一种解决办法是提高服务器配置,提高到 2 核 4 G,一年 1200。我已经动心了,只是突然看到角落里有一台自用神舟笔记本,平常玩游戏比较多。想想玩游戏肯定比不上学习,所以我动了用笔记本作为程序部署机器的想法。
这台神舟笔记本的配置还是不错的,16G 内存,i7 芯片,1060 的GPU,完胜云服务器。用笔记本做服务器的难点在如何固定外网 IP,现在固定 ip4 的地址基本不给发放。调研相关实现方式后,决定使用 frp 做内网穿刺,使用云服务器作为外网跳板。后续用户访问逻辑为:用户 -》 云服务器 -〉笔记本 -》云服务器 -〉用户
为了方便开发和部署,笔者使用 vs code 和 ssh 配置了云服务器和笔记本的远程访问,文件传输,代码编写,环境配置都方便了很多,后面用一台主开发机就能操控云服务器和神舟笔记本了。
为了进一步提高开发体验,笔者将代码更新和部署工作自动化了。将代码托管到码云上,每次 push 动作触发后,码云会向部署机上发送更新命令。部署机收到后会重新安装依赖,再启动项目。
前端技术选型
next + process + uiw/react-md-editor + react-markdown + mysql2 + sequelize
next
特殊的,在函数式组件中可以使用 awat 语句,在 return 方法前完成查询数据库等异步操作。
'use server' 代码在服务端执行,form 表单提交时可使用,方便处理数据,注意在 form 处理方法第一行加上。
const formAction = async (formData) => {
'use server'
// ...
redirect('/');
}
可以设置多个根目录表示不同页面,例如
app
(admin)
login
(display)
home
_header
()形式目录,不参与路径,但子目录可以继续参与。_name 自己和子目录均不参与路径
动态路由
以 [id] 形式作为文件名,从 page 页面中的 props 中可以取出 id 值。
中间件
可以在渲染组件前执行,做各类检查和补充信息的操作,例如查看登录重定向
export async function middleware(request) {
// Getting cookies from the request using the `RequestCookies` API
if(!cookie){
return NextResponse.rewrite(new URL('/login', request.url))
}
return NextResponse.next()
}
若用户没有某些 cookie 信息,则重定向到登录页面。
MdEditor
动态引入,不让这个包影响首屏加载速度
const MdEditor = dynamic(
() => import("@uiw/react-md-editor"),
{ssr: false});
使用起来十分方便,使用 useState,将 value 和 setValue 绑定在 MdEditor 组件上即可
<MdEditor value={value} onChange={setValue} height={500}/>
其样式如下
Markdown
使用 markdown 库可以直接展示 markdown 文件内容,但 react-markdown 并没有提供目录功能,为了优化体验,可以记录下 markdown 整篇文档中的标题数据,然后在侧边栏位置将其渲染出来。
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import remarkToc from "remark-toc";
import rehypeRaw from "rehype-raw";
import rehypeKatex from "rehype-katex";
import 'katex/dist/katex.min.css' // `rehype-katex` does not import the CSS for you
import Markdown from "react-markdown";
import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'
import {prism} from 'react-syntax-highlighter/dist/esm/styles/prism'
<Markdown
remarkPlugins={[remarkToc, remarkGfm, remarkMath]}
rehypePlugins={[rehypeRaw, rehypeKatex]}
children={md}
components={{
code(props) {
const {children, className, node, ...rest} = props
const match = /language-(\w+)/.exec(className || '');
return match ? (
<SyntaxHighlighter
{...rest}
PreTag="div"
children={String(children).replace(/\n$/, '')}
language={match[1]}
style={prism}
/>
) : (
<code {...rest} className={`${className} code-quote`}>
{children}
</code>
)
},
h1: ({children}) => {
console.error('xxx ', children)
return <h1 id={id}>{children}</h1>
},
h2: ({children}) => {
return <h2 className={'mark-down-page-h2'} id={id}>{children}</h2>
},
h3: ({children}) => {
return <h3 id={id} className={'mark-down-page-h3'}>{children}</h3>
},
p: ({children}) => children === '[TOC]' ? null : (
<p>{children}</p>
),
}}
/>
效果如下
数据库
第一步:连接数据库
数据使用的 mysql2
import {Sequelize} from "sequelize";
import mysqlDrive from 'mysql2';
require('dotenv').config();
export default new Sequelize('blog01', process.env.DB_USERNAME, process.env.DB_PASSWORD, {
host: 'localhost',
dialect: 'mysql',
dialectModule: mysqlDrive,
});
第二步:定义数据结构
import {DataTypes} from "sequelize";
import sequelize from '@/app/_db/index'
export default sequelize.define('item_card', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
// xxx
}, {
// 给表名默认加复数
freezeTableName: true,
// 列表名自动转驼峰
underscored: true,
// 禁止自动添加 created_at
timestamps: false,
});
注意:
- sequelize 是第一步定义的连接数据库对象
- freezeTableName 设置为 true 才不会自动给表名添加 s 后缀
- underscored 设置为 true 列表名才不会自动转换为驼峰
- timestamps 设置为 false 才不会自动添加 created_at 列信息
第三步:查询语句
这里列举 crud 语句,其他查询可以在官网查看
// 查找符合条件的所有元素
export const getSearchKeyBlog = async (key) => {
return moduleItemCard.findAll({
limit: 10,
order: [['create_time', 'DESC']],
where: {
class_type: 2,
title: {
[Op.like]: `%${key}%`
}
}
});
}
// 查询唯一 id
export const findItemById = async (id) => {
return moduleItemCard.findByPk(id);
}
// 更新内容
export const updateMd = async (id, data) => {
return moduleItemCard.update(data, {
where: {
id,
}
});
}
// 插入内容
export const insertMd = async (values) => {
return moduleItemCard.create(values);
}
frp 内网穿刺
第一步:安装
- 安装 systemd
# 使用 yum 安装 systemd(CentOS/RHEL)
yum install systemd
# 使用 apt 安装 systemd(Debian/Ubuntu)
apt install systemd
2. 安装 frps github.com/fatedier/fr…
- 创建 frps.service 文件,服务端配置
使用文本编辑器 (如 vim) 在 /etc/systemd/system
目录下创建一个 frps.service
文件,用于配置 frps 服务。
[Unit]
# 服务名称,可自定义
Description = frp server
After = network.target syslog.target
Wants = network.target
[Service]
Type = simple
# 启动frps的命令,需修改为您的frps的安装路径
ExecStart = /path/to/frps -c /path/to/frps.toml
[Install]
WantedBy = multi-user.target
4. 使用 systemd 命令管理 frps 服务
# 启动frp
sudo systemctl start frps
# 停止frp
sudo systemctl stop frps
# 重启frp
sudo systemctl restart frps
# 查看frp状态
sudo systemctl status frps
5. 设置 frps 开机自启动
sudo systemctl enable frps
第二步:配置服务端
在 fprs.toml 中更新配置
// 传输接口
bindPort = 7900
// http 端口
vhostHTTPPort = 80
// https 端口
vhostHTTPSPort = 443
# web 控制台端口
webServer.port = 7810
# dashboard 用户名密码,可选,默认为空
webServer.user = "xxx"
webServer.password = "xxx"
webServer.addr = "0.0.0.0"
后查看 web 页面可以看到总览,域名加 webServer.port 的域名
第三步:配置客户端
和服务器端安装步骤一致,但是使用的安装文件为 trpc 文件。使用文本编辑器 (如 vim) 在 /etc/systemd/system
目录下创建一个 frpc.service
文件,用于配置 frpc 服务。
vscode 配置免密远程访问
第一步:下载插件
vscode 下载远程访问插件:Remote - SSH
第二步:配置 rsa 授权
查看 /.ssh/id_rsa.pub ,新增一行,复制 id_rsa.pub 内容到服务器的/.ssh/authorized_keys 中
第三步:连接远程地址
输入 ip+端口 连接远程地址,选中本机上 id_rsa.pub 存储地址
码云自动化部署
我们可以把仓库建在码云上,通过 webhooks 自动更新源码,自动打包,自动发布。具体流程如下
第一步:需要先在码云上创建自己的仓库
第二步:打开仓库管理,打开 webHooks 管理
第三步:在服务器上建立服务,接受来自码云的提交动作
第四步:添加 webhooks,在 push 动作时给服务器上发生 post 命令
第五步:服务器服务收到码云命令后,开始重新安装依赖,打包和发布