乐趣区

关于程序员:今年的情人节给心爱的她一个不一样的礼物吧

明天是 2022 年 2 月 2 日,间隔往年的情人节只有不到两周的工夫了。

给大家隆重介绍一个网站,憨憨. 我爱你

参考示例网站:

  • 表白幻灯片. 憨憨. 我爱你

    • 源码
  • 恋爱计时器. 憨憨. 我爱你

    • 源码

申请流程

关上官网首页:憨憨. 我爱你

(因为该网站数据库应用的是 PlanetScale 收费服务,位于美东,所以拜访可能会略微有点慢。请急躁期待。)

应用 Authing 账号登录(能够手机、邮箱注册,Github 账号登录,或者微信小程序扫码,后续还会增加更多登录形式)。

能够别离点击进入域名申请和邮箱申请。

域名申请

域名申请界面如下:

反对的绑定形式有三种:

  • CNAME:能够应用 Github Pages、Netlify、Gitee、Coding.net、Cloudflare 等提供托管服务的平台

    • 值参考:willin.github.io
    • 留神:不反对 Vercel,因为 Vercel 默认状况下并不反对绑定二级域名(除非所有权在你集体名下)
  • A:IPv4 须要本人搭建服务器,并进行绑定

    • 值参考:1.2.3.4(你服务器的 ip 地址)
  • AAAA: IPv6 须要本人搭建服务器,并进行绑定

    • 不做表述,不太举荐非专业人士抉择
    • 如果你须要同时绑定 IPv4 和 IPv6 的话,倡议倡议注册 A 类型,而后 ISSUE 或邮件分割我配合解决

其中还有一项 Proxied(CDN),如果不晓得作用,能够尝试开启或敞开来测试。

邮箱申请

域名申请界面如下:

目前应用了 Cloudflare 的邮件转发服务,但因为暂不反对 IDN 域名,所以能够提前抢注,第一工夫领有。

其余阐明

如需帮忙

欢送在 Github 上关注我:willin,如果在为可爱的她筹备礼物时遇到问题,能够为你收费提供技术咨询。

还想要其余的域名

  • js.cool(在屡次协商后,目前曾经反对 Vercel 绑定)
  • log.lu(敬请期待)

感觉慷慨

  • 您能够将该网站分享给更多的人
  • 您也能够通过以下渠道进行打赏:

    • 微信
    • 支付宝
    • 爱发电
    • Github Sponsors
    • Paypal

蠢蠢欲动

或者你也有很多想法,想要实现。您能够:

  • 应用 Authing 疾速集成开发你本人的利用
  • Fork 本我的项目源码(齐全开源),并提供你本人的域名服务

    • 我的项目源码
  • 在 Github 上对本我的项目进行欠缺和优化

    • 我的项目后续布局

开源

接下来,开始一个重要的环节。俗话说,授人以鱼不如授人以渔。我将 憨憨. 我爱你 的源码 进行开源,并具体解说一下设计与实现的全副过程。

设计

这个我的项目大略花了我 3 个小时左右实现。为了取长补短,我应用了 UI 框架,所以就没有额定的 UI 设计了,间接用几个根底组件疾速上手该我的项目。

技术选型

首先,第一步是技术选型。因为我要提供的是一项收费的服务,所以尽量也抉择一些收费的服务商,及一些相干的技术栈。

服务商的抉择:

  • Cloudflare:提供收费的域名解析、CDN 减速以及凋谢的接口
  • Vercel:面向集体的收费利用托管,反对 Node.js 环境,应用 Next.js 框架
  • PlanetScale:具备肯定收费额度的云端 MySQL 服务
  • Prisma:Cloud Studio 治理数据库

其实,原本是想应用 Cloudflare 全家桶的,就是用 Cloudflare Pages(动态网站)+ Cloudflare Workers(Serverless 办法执行)及 KV(键值对存储),然而因为工夫和精力的限度,所以就采纳了更简略快捷的实现形式。

技术栈:

  • Typescript:尽管我喜爱用更少的代码做更多的事件,但 TS 带给我更高效的团队合作舞台
  • Next.js:一个全栈框架(前端应用 React,后端相似于 http 模块和 Express),反对 SSR(服务器端渲染)和 SSG(动态站点生成)

    • @authing/nextjs:Authing SSO 集成 SDK
  • Prisma:下一代的 ORM 框架,反对多种数据库(本我的项目应用为 MySQL)和数据库迁徙(Migration)
  • Tailwind CSS:下一代的 CSS 框架,实用第一

    • Daisy UI:封装了一些 UI 款式组件

数据库设计

因为我用的是 Authing 用户集成,所以省去了用户表的设计和用户相干接口的设计。

// 域名类型
enum DomainType {
  A
  AAAA
  CNAME
}

// 审核状态
enum Status {
  // 待审核
  PENDING
  // 激活
  ACTIVE
  // 已删除
  DELETED
  // 被管理员禁用
  BANNED
}

// 域名记录表
model Domains {
  // Cloudflare 域名记录的 ID,同时作为表主键 id
  id        String      @id @default(cuid()) @db.VarChar(32)
  // 自增 id,没有什么实际意义,只是为了缩小查问(毕竟有调用配额限度),理论我的项目中不举荐自增主键及自增 id 应用
  no        Int         @default(autoincrement()) @db.UnsignedInt
  name      String      @db.VarChar(255)
  punycode  String      @db.VarChar(255)
  type      DomainType  @default(CNAME)
  content   String      @default("") @db.VarChar(255)
  proxied   Boolean     @default(true)
  // Authing 的用户 id
  user      String      @default("") @db.VarChar(32)
  status    Status      @default(ACTIVE)
  createdAt DateTime    @default(now())
  updatedAt DateTime    @updatedAt

  @@index([no])
  @@index([name, punycode])
  @@index([user, status, createdAt])
}

// 邮箱表
model Emails {
  // 因为 Cloudflare 邮箱还没有提供凋谢接口,所以须要人工审核和操作,这里会填入默认的 cuid 作为主键 id
  id        String      @id @default(cuid()) @db.VarChar(32)
  // 自增 id,没有什么实际意义,只是为了缩小查问(毕竟有调用配额限度),理论我的项目中不举荐自增主键及自增 id 应用
  no        Int         @default(autoincrement()) @db.UnsignedInt
  name      String      @db.VarChar(255)
  punycode  String      @db.VarChar(255)
  content   String      @default("") @db.VarChar(255)
  user      String      @default("") @db.VarChar(32)
  status    Status      @default(PENDING)
  createdAt DateTime    @default(now())
  updatedAt DateTime    @updatedAt

  @@index([no])
  @@index([name, punycode])
  @@index([user, status, createdAt])
}

非常简单,参考正文阐明。另外,我原本是打算只存一个名称的,但因为会反复注册,比如说我注册了一个中文名 老王,你又注册了一个对应的 punycode 代码名 xn--qbyt9x,就会抵触,所以索性(偷懒)都存下吧。

技术筹备

  • 中文域名须要把握的 Punycode 常识:RFC 3492 标准
  • Cloudflare API 接口

    • 创立一条解析:Create DNS Record
    • 批改一条解析:Patch DNS Record
    • 删除一条解析:Delete DNS Record
  • Authing SSO 集成,能够参考我之前的文章:《全栈框架利用疾速集成 Authing SSO》

先把 Next.js 网站框架搭建起来,部署到 Vercel 上进行测试。能够再加上 Tailwind CSS 和 Authing SSO 集成。第一步筹备工作就算实现了。

接口设计

为了疾速(偷懒)实现,我别离创立了增删改查四个接口。

查问接口:

graph TD
    Start1(Start)
    --> | 查看域名是否存在 | check1{是否登录}
    --> |F| fail1[失败]
    --> End1(End)
    check1 --> |T| check12{查看是否为保留域名}
    --> |T| fail1
    check12 --> |F| check13{查看数据库反复}
    --> |T| fail1
    check13 --> |F| success1[容许注册]
    --> End1

创立接口:

graph TD
    Start2(Start)
    --> | 创立域名 | check2{是否登录}
    --> |F| fail2[失败]
    --> End2(End)
    check2 --> |T| check22{查看是否为保留域名}
    --> |T| fail2
    check22 --> |F| check23{用户是否曾经注册域名}
    --> |T| fail2
    check23 --> |F| check24{查看数据库反复}
    --> |T| fail2
    check24 --> |F| success2[注册]
    --> End2

数据库查问用户是否曾经注册域名和是否存在同名能够用一次查问实现,这里为了进步查问性能进行了拆分。

批改接口:

graph TD
    Start3(Start)
    --> | 查看域名是否存在 | check3{是否登录}
    --> |F| fail3[失败]
    --> End3(End)
    check3 --> |T| check32{批改 id 和 用户匹配的记录}
    --> |F 批改记录数 0| fail3
    check32 --> |T| success3[批改胜利]
    --> End3

删除接口与批改接口同。邮箱接口与域名相似,不再赘述。

代码实现

封装 Cloudflare SDK

当然也有现成的库能够间接用,然而因为没几行代码,我就本人手撸了。

import {Domains} from '@prisma/client';
import {CfAPIToken, CfZoneId} from '../config';

const BASE_URL = 'https://api.cloudflare.com/client/v4';

export type CFResult = {
  success: boolean;
  result: {id: string;};
};

const headers = {Authorization: `Bearer ${CfAPIToken}`,
  'Content-Type': 'application/json'
};

export const createDomain = async (form: Pick<Domains, 'name' | 'content' | 'type' | 'proxied'>): Promise<string> => {const res = await fetch(`${BASE_URL}/zones/${CfZoneId}/dns_records`, {
    method: 'POST',
    headers,
    body: JSON.stringify({...form, ttl: 1})
  });
  const data = (await res.json()) as CFResult;
  if (data.success) {return data.result.id;}
  return '';
};

export const updateDomain = async (
  id: string,
  form: Pick<Domains, 'name' | 'content' | 'type' | 'proxied'>
): Promise<boolean> => {const res = await fetch(`${BASE_URL}/zones/${CfZoneId}/dns_records/${id}`, {
    method: 'PATCH',
    headers,
    body: JSON.stringify({...form, ttl: 1})
  });
  const data = (await res.json()) as CFResult;
  console.error(data);
  return data.success;
};

export const deleteDomain = async (id: string): Promise<boolean> => {const res = await fetch(`${BASE_URL}/zones/${CfZoneId}/dns_records/${id}`, {
    method: 'DELETE',
    headers
  });
  const data = (await res.json()) as CFResult;
  return !!data.result.id;
};

封装校验工具类

须要有肯定的正则根底,如果你须要在线调试工具,能够拜访:regexper.js.cool

域名(CNAME)校验正则:

/^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$/;

邮箱校验正则:

/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;

IPv4 校验正则:

/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;

IPv6 校验正则:

/^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:)))(%.+)?$/;

页面申请封装

以域名注册提交为例:

async function submit(e: SyntheticEvent) {e.preventDefault();
  // 因为我 Vue、React 都会用,且用的都比拟少
  // 所以获取表单数据,我用的是 Vanilla JS 形式,通用性更高
  // 如果你不相熟,能够用 React 的形式
  const target = e.currentTarget as typeof e.currentTarget & {type: { value: DomainType};
    content: {value: string};
    proxied: {checked: boolean};
  };
  const type = target.type.value;
  const content = target.content.value;
  if (!validateContent(type, content)) {return;}
  const form = {
    type,
    content,
    proxied: target.proxied.checked,
    name,
    punycode: toASCII(name)
  };
  // 我倡议对 Fetch 进行封装,为了谋求效率(偷懒),我就没有做
  const res = await fetch(`/api/domain/create`, {
    method: 'POST',
    body: JSON.stringify(form),
    headers: {'content-type': 'application/json'}
  });
  // 所以像这样的解决,就十分不优雅,而且还能够对立封装,将谬误提醒应用告诉条组件之类的
  const result = (await res.json()) as {success: boolean; id: string};
  if (result.success) {router.reload();
  } else {alert('出错啦!请稍后重试');
  }
}

可复用的代码能够进行封装。参考软件工程的思维:高内聚、低耦合。我这里举的是一个较为背面的教材,代码臃肿、可读性低。

留神点

  • 因为 Tailwind CSS 3 采纳了全新的 JIT 机制,purgecss 不再须要
  • 关注 React 性能,如 useState 之类的 Hooks,尽量放在页面级别,不要放在组件级别(尤其是会循环生成的组件)
  • 应用 useMemodebounce 之类的形式进行缓存、防抖、限流,以晋升利用性能
  • 应用 Next.js 框架(或一般 React 利用)时,大部分状况下,多理解 swr 及其外部的一些核心思想会很有裨益

剩下的代码局部就干燥且简略了。


差不多就讲这么多吧。记得分享哦!

willin | 憨憨. 我爱你 | 我的项目源码

退出移动版