最开始使用 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
特殊的,在函数式组件中可以使用 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 信息,则重定向到登录页面。
动态引入,不让这个包影响首屏加载速度
const MdEditor = dynamic( () => import("@uiw/react-md-editor"), {ssr: false});
使用起来十分方便,使用 useState,将 value 和 setValue 绑定在 MdEditor 组件上即可
<MdEditor value={value} onChange={setValue} height={500}/>
其样式如下
使用 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, });
注意:
这里列举 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); }
# 使用 yum 安装 systemd(CentOS/RHEL)
yum install systemd
# 使用 apt 安装 systemd(Debian/Ubuntu)
apt install systemd
创建 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
# 启动frp
sudo systemctl start frps
# 停止frp
sudo systemctl stop frps
# 重启frp
sudo systemctl restart frps
# 查看frp状态
sudo systemctl status 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 下载远程访问插件:Remote - SSH
查看 /.ssh/id_rsa.pub ,新增一行,复制 id_rsa.pub 内容到服务器的/.ssh/authorized_keys 中
输入 ip+端口 连接远程地址,选中本机上 id_rsa.pub 存储地址
我们可以把仓库建在码云上,通过 webhooks 自动更新源码,自动打包,自动发布。具体流程如下
第一步:需要先在码云上创建自己的仓库
第二步:打开仓库管理,打开 webHooks 管理
第三步:在服务器上建立服务,接受来自码云的提交动作
第四步:添加 webhooks,在 push 动作时给服务器上发生 post 命令
第五步:服务器服务收到码云命令后,开始重新安装依赖,打包和发布