V2.0更新日志

2024-1-04 12:24:24

#更新日志

321

前言

  • 距离上一次更新已经不知道过去了多久,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层

  • 所谓ORM,即Object Relational Mapping Database Tools的缩写,用于充当我们编写代码与数据库操作之间的中间层,方便代码编写,提高效率与对应的数据库操作安全性
  • 翻找资料的时候找到了一篇不错的文章,在这里分享一下:What is an ORM – The Meaning of Object Relational Mapping Database Tools
  • 我这个项目选择的是Drizzle ORM,其实当时也没多想,单纯是看到vercel全家桶里有Drizzle ORM,想着两者之间的兼容性会比较好,所以才使用,后面也确实证明了这一点,在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的官网去看

结尾

  • 经过如上的一步步操作,就能得到一个自己的动态网站,可以说我还是花了不少功夫把这一整套流程给搞通了。搞定了这些之后,后续的拓展与添加便不是问题,可以根据自己的需求随心所欲的去更改了。