V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
willin
V2EX  ›  分享创造

今年的情人节,给心爱的她一个不一样的礼物吧

  •  
  •   willin ·
    willin · 2022-02-12 12:21:29 +08:00 · 3560 次点击
    这是一个创建于 1049 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    参考示例网站:


    申请流程

    打开官网首页: 憨憨.我爱你

    (由于该网站数据库使用的是 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 (敬请期待)

    感觉慷慨

    跃跃欲试

    或许你也有很多想法,想要实现。您可以:

    • 使用 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 (静态站点生成)
    • 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,就会冲突,所以索性(偷懒)都存下吧。

    技术准备

    先把 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 | 憨憨.我爱你 | 项目源码

    24 条回复    2022-02-14 17:28:58 +08:00
    lawsiki
        1
    lawsiki  
       2022-02-12 12:41:23 +08:00   ❤️ 9
    恕我直言,典型的 IT 直男想法。。。
    Wenco
        2
    Wenco  
       2022-02-12 12:45:33 +08:00   ❤️ 1
    有点想笑🤣
    ampedee
        3
    ampedee  
       2022-02-12 12:51:01 +08:00 via iPhone   ❤️ 1
    名字不错,确实会被女生认为是憨憨
    doresu
        4
    doresu  
       2022-02-12 12:55:51 +08:00 via Android
    我还认真点进来看了
    x86
        5
    x86  
       2022-02-12 13:01:04 +08:00 via iPhone
    铁憨憨行为
    chunqiuyiyu
        6
    chunqiuyiyu  
       2022-02-12 13:01:39 +08:00
    虽然我没有对象,但是看到这个我真的笑了。
    su0818
        7
    su0818  
       2022-02-12 13:03:42 +08:00 via iPhone
    很直男
    iam174cm
        8
    iam174cm  
       2022-02-12 13:07:13 +08:00
    你这还不如直接买几根 YSL 好使。。。
    hnbcinfo
        9
    hnbcinfo  
       2022-02-12 13:09:43 +08:00
    哈哈,今年情人节流形玩尬的了吗?
    ypzhou
        10
    ypzhou  
       2022-02-12 13:41:48 +08:00
    直接打开京东下单一个不好...
    7vps
        11
    7vps  
       2022-02-12 13:58:12 +08:00
    嫌买礼物麻烦,你不如直接发个 520 红包
    xvnehc
        12
    xvnehc  
       2022-02-12 17:35:01 +08:00
    “给大家隆重介绍一个网站”,时间仿佛倒流了 15 年 😓
    Marmot
        13
    Marmot  
       2022-02-12 18:34:51 +08:00
    没看完都觉得还是发红包比较简单
    agdhole
        14
    agdhole  
       2022-02-12 18:45:57 +08:00
    看到标题就知道内容了
    guisheng
        15
    guisheng  
       2022-02-12 22:10:07 +08:00 via iPhone
    就我觉得不错吗?整体思路清晰,表述的很符合程序猿的风格。实际和过程,我不是老板我喜欢过程。
    sharunpeng
        16
    sharunpeng  
       2022-02-12 23:56:07 +08:00
    挺好,感谢分享
    fengyueheng
        17
    fengyueheng  
       2022-02-13 12:29:00 +08:00
    hhhh 老哥有心了
    seven123
        18
    seven123  
       2022-02-13 21:49:54 +08:00
    你这还不如直接买几根口红好使。。。或者陪逛街买买买好使。。。术有专攻,大部分妹子能对这些有多大兴趣?
    Swe3tDream
        19
    Swe3tDream  
       2022-02-13 22:32:33 +08:00
    男默女泪
    HiCoder
        20
    HiCoder  
       2022-02-14 09:58:54 +08:00
    你女朋友有没有打你?
    BlackCat02
        21
    BlackCat02  
       2022-02-14 11:18:20 +08:00
    有女朋友了么弟弟~
    sadfQED2
        22
    sadfQED2  
       2022-02-14 15:08:51 +08:00 via Android
    写的不错,下次别写了
    goodryb
        23
    goodryb  
       2022-02-14 15:44:17 +08:00
    什么年代了,还搞这种,学两句土味情话,送一束花不比这强 😳
    neilyoone
        24
    neilyoone  
       2022-02-14 17:28:58 +08:00
    恕我直言,典型的 IT 直男想法。。。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3987 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 05:14 · PVG 13:14 · LAX 21:14 · JFK 00:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.