前言
- 距离上一次更新已经不知道过去了多久,5月份本站的云服务器正式到期,看了一下腾讯云服务器续费金额,打消了继续续费的念头——因为太太太贵了。- 于是第一步,我开始寻找有没有更加廉价的云服务器,寻找了一圈,发现基本上都要数百一年,不太适合我这小破站运营。
- 后面开始学习next.js这一框架,打算体验一下服务器组件的开发体验时,我在next.js的官方文档中看到了vercel的"广告"。意外发现vercel非常适合像博客网站这类小网站的部署。
- 首先vercel支持与github仓库联动,vercel能根据github的代码来打包部署项目,十分方便。
- 不过最重要的一点还是可以白嫖vercel的服务器,vercel允许用户免费部署项目,以及试用一个免费的云数据库。有了这两个要素,想要部署一个动态的博客网站的前提就已经集齐了。
- 本文就是在新的博客网站写下的。
技术架构
- 应用层框架:
next.js
- 开发库:
react
- orm库:
drizzle-orm
- UI库:
tailwindcss
+ headlessui/react
+ heroicons/react
- 网络请求库:
axios
- md编辑器:
vditor
+ next-mdx-remote
- JWT权限认证:
jsonwebtoken
服务器准备
- 服务器这方面上文就说了,使用的vercel的免费服务器。
- 在vercel上注册一个账号并不难,支持github账号的第三方注册登录,基本上毫无门槛- 创建一个项目,并将其与github上的仓库连接之后,便可以让vercel的部署与github的代码提交联动了,基本上vercel检测到github上的代码提交变动后,便会自动为你打包部署
- 具体数据库与项目相连接的办法我就不细说了,vercel那边的教程已经做的很好了
环境变量配置
- vercel也提供了环境变量的配置页,有了这个我们就可以不用往github上上传我们的环境变量配置文件,避免了密匙和配置项的泄漏
- 在这个页面配置了环境变量后,只需如下的代码来访问对应的变量即可、
process.env.对应的变量名
- 值得注意的是,由于nextjs的渲染是分服务端和客户端的,默认环境变量都是只能在服务端访问到,如果需要配置一个客户端环境变量,需要加上
NEXT_PUBLIC_
的前缀
数据库
- 由于vercel只提供了postgres这一免费的类sql数据库,所以也没得选择,只能选择postgres这一数据库来作为本站的数据库,vercel提供的数据库容量有256Mb,对于一个只存储文本的博客网站来讲绝对是绰绰有余的。
- postgres在语法和使用上基本与sql一致,对于我这种浅度使用的用户来讲,是可以无障碍上手的,就是数据格式有点不太一样,当初为了把我原来的网站数据迁移过来,还是花了不少劲的。
- postgres的管理客户端,我推荐使用pgAdmin这一软件,用来远程管理数据库十分方便。
Next.js
- Next.js是一个用于生产环境的React框架,本体就自带了ssr的支持,并且带有自己的一套路由,这意味着只要一个nextjs框架,就可以解决大部分使用场景,没必要像传统框架一样,引入一大帮全家桶库,增加项目的代码负担。
- 本项目使用的是nextjs的app路由模式,与历史路由模式不同,阅读的时候请注意(值得吐槽的是,nextjs的中文文档做的挺烂的,app路由的配置与实现我都是摸着英文文档看下来的)
SSR
- nextjs有一点牛逼的就是,它同时支持ssr,csr,ssg的渲染。通过nextjs特有的客户端与服务端组件,我们可以在项目中实现分块渲染,静态部分,我们可以通过它的服务器渲染组件以及客户端渲染组件,自由的决定页面哪些内容是客户端渲染,哪些内容是服务端渲染的。
- 这样一来,我们就可以将交互内容多的部分作为客户端组件来编写代码,如评论,分页,筛选等等
- 而纯展示的静态部分内容,如本篇博文,则可以作为服务端组件来编写代码,这样能加快页面的加载,以及更利于搜索引擎的SEO
app路由
- nextjs自带一套路由,路由路径是由项目路径决定的(这个得点赞,这样做的好处是减少了我们在编写项目时,寻找对应代码块的时间成本。比如我们看到页面上的url如/blog/21,我们很快就能找到对应的代码路径为src/app/blog/[id])
基本用法
- 在src/app目录下新建文件夹,项目路径即对应url的路径,如src/app/home对应的url路径为/home
- src/app/home路径下新建一个page.js文件,用于定义页面内容,模板如下,写法与react的function组件保持一致
export default function Home() {
return (
<MainLayout>
<main>
你好啊
</main>
</MainLayout>
)
}
带参数的路由
- 与基本的路由配置一样,不过文件夹名替换为以[]包裹,如[id]
export default function Page({ params }) {
const blogId = params['id']
return (
<MainLayout>
<main>
你好啊
</main>
</MainLayout>
)
}
- 如上,该function组件增加了params的传参,对应的就是url上的参数,如/blog/21,params['id']的值为21
服务端渲染数据
- nextJs的特色就是服务端渲染,所以我们肯定要体验体验这个特色
async function getData(params) {
const blogId = params['id']
try {
const data = await getBlogSql(blogId)
return data
} catch (error) {
console.error(error)
return undefined
}
}
export default async function Page({ params }) {
const blogId = params['id']
const blog = await getData(params) || {}
return (
<MainLayout>
<main>
你好啊
</main>
</MainLayout>
)
}
- 如上我们定义了一个服务端渲染组件,可以看到不同的地方在于,我们多定义了一个getData的异步方法,这个方法用于在服务端直接获取数据,比如各类sql方法,从数据库中查询数据,得到数据后交给组件处理,最后渲染为页面的静态内容,返回客户端
- 同样的,function组件也要被定义为异步函数
api的暴露
- nextjs除了允许你配置常规的页面路由外,它也可以让你暴露出接口路由,路径定义与上面的路由定义没有区别,同样由项目路径决定
get接口
import { NextResponse } from 'next/server';
import { eq } from "drizzle-orm";
export async function GET(request) {
const { searchParams } = new URL(request.url)
const blogId = searchParams.get('id')
try {
// 数据库操作
const data = await getBlogSql(blogId)
return NextResponse.json({ data })
} catch (error) {
console.error(error)
return NextResponse.json({ error: error?.message ?? error}, { status: 500 })
}
}
- 如上是一个get接口的定义模板
- 使用URL方法,可以获取到对应的查询参数,中间经过代码处理后,通过NextResponse这一方法,返回符合http协议的返回报文给接口调用方
post接口
import { NextResponse } from 'next/server';
import { headers } from 'next/headers'
export async function POST(request) {
const {
} = await request.json()
const headersList = headers()
const token = headersList.get('Authorization')
try {
// 权限校验相关
if (!checkTokenRole(token, 'admin')) {
return NextResponse.json({ error: '您无新增权限' })
}
// 数据库操作,留空
const data = null
return NextResponse.json({ data: data[0] })
} catch (error) {
console.error(error)
return NextResponse.json({ error: error?.message ?? error }, { status: 500 })
}
}
- 如上是定义post接口的一个模板
- 使用next的方法,headers来获取请求中的header信息,并通过request.json()这一方法来获取请求报文中的参数,在最后仍是以NextResponse这一方法,返回符合http协议的返回报文给接口调用方
权限校验
- 权限校验使用了JWT这一简单的技术,JWT是一种结构简单的权限认证技术
const token = jwt.sign(userInfo, tokenKey, { expiresIn: expiresTime })
const decoded = jwt.verify(token, tokenKey)
- 基于以上的sign和verify方法,便可以组成一个简单的jwt认证流程
- sign用于生成token,我们需要传入对应的密钥参数,加密内容,过期时间
- verify用于验证token,并返回对应的解码内容,以便我们后续处理
- 接下来给我们的项目写一个对应的utils文件,后面方便我们进行鉴权即可
- 至于jwt的秘钥,我们定义一个项目环境变量即可,如
process.env?.JWT_SECRET_KEY
,方便配置以及保障秘钥安全
ORM层
db层定义
- 首先我们在src目录下,建立一个db文件夹,代表着db层,用于定义数据库操作的接口,以及对应的实现
建立数据库连接
import { drizzle } from 'drizzle-orm/vercel-postgres';
import { sql } from '@vercel/postgres';
// Use this object to send drizzle queries to your DB
export const db = drizzle(sql);
- 如上,使用drizzle orm,与vercel的postgres数据库进行连接,并导出db对象,方便我们后续调用。基本上不用我们配置什么东西,十分方便。
- 建立连接后,自然也要拉取对应的环境变量到本地,vercel给我们提供了
vercel env pull .env.development.local
来拉取在vercel网站上配置的本地变量
定义数据库表对象
import { pgTable, text, varchar, integer, timestamp, bigint } from "drizzle-orm/pg-core";
export const m_blog = pgTable('m_blog', {
id: integer('blog_id').primaryKey(),
blogContent: text('blog_content'),
blogLike: integer('blog_like'),
blogRead: integer('blog_read'),
blogVisibility: integer('blog_visibility'),
blogType: integer('blog_type'),
blogTitle: varchar('blog_title'),
blogLabel: varchar('blog_label'),
blogWriter: varchar('blog_writer'),
blogTime: timestamp('blog_time').defaultNow(),
blogCreateTime: timestamp('blog_create_time').defaultNow(),
blogWriterId: integer('blog_writer_id')
});
- 如上图是我这个项目关于博文表的定义,定义一个pgTable对象后,导出,在数据库操作层方便我们使用
数据库操作
// 查找
export async function getBlogList(limit = 10, offset = 0) {
const {
blogContent,
...rest
} = m_blog
const data = await db.select({
...rest
}).from(m_blog)
.orderBy(desc(m_blog.blogTime))
.limit(limit)
.offset(offset)
return data
}
- 如上是我定义的一个查询博文列表的操作方法,通过
db.select
来查询数据库,from
来指定查询的表,orderBy
来指定排序规则,limit
来指定查询的条数,offset
来指定查询的偏移量,desc
来指定降序排列,asc
来指定升序排列,defaultNow
来指定默认当前时间
- 详细的增删查改我就不在这里赘述了,有兴趣的同学可以自己去drizzle-orm的官网去看
结尾
- 经过如上的一步步操作,就能得到一个自己的动态网站,可以说我还是花了不少功夫把这一整套流程给搞通了。搞定了这些之后,后续的拓展与添加便不是问题,可以根据自己的需求随心所欲的去更改了。