头图

Today is February 2, 2022, and this year's Valentine's Day is less than two weeks away.

I would like to introduce a website to you, Hanhan. I love you

Reference sample website:


application process

Open the home page of the official website: Hanhan. I love you

(As the website database uses PlanetScale free service and is located in the US East, access may be slightly slow. Please be patient.)

请添加图片描述

Use the Authing account to log in (you can register with your mobile phone or email address, log in with your Github account, or scan the code in the WeChat applet, and more login methods will be added in the future).

请添加图片描述

You can click to enter domain name application and email application respectively.

Domain Name Application

The domain name application interface is as follows:

请添加图片描述

There are three supported binding methods:

  • CNAME: Platforms that provide hosting services can be used with Github Pages, Netlify, Gitee, Coding.net, Cloudflare, etc.

    • Value reference: willin.github.io
    • Note: Vercel is not supported, because Vercel does not support binding second-level domain names by default (unless the ownership is under your personal name)
  • A: IPv4 needs to build its own server and bind it

    • Value reference: 1.2.3.4 (your server's ip address)
  • AAAA: IPv6 needs to build its own server and bind it

    • I don't make a statement, and it is not recommended for non-professionals to choose
    • If you need to bind IPv4 and IPv6 at the same time, it is recommended to register type A, and then ISSUE or email to contact me for cooperation

There is also a Proxied (CDN), if you don't know the function, you can try to turn it on or off to test.

Email application

The domain name application interface is as follows:

请添加图片描述

At present, Cloudflare's mail forwarding service is used, but because IDN domain names are not supported for the time being, it is possible to preemptively register and have it as soon as possible.

other instructions

if you need help

Welcome to follow me on Github: willin , if you have any problems preparing gifts for your beloved, we can provide you with free technical advice.

want another domain name

  • js.cool (After many negotiations, Vercel binding is now supported)
  • log.lu (stay tuned)

feel generous

eager to try

Maybe you also have a lot of ideas that you want to realize. you can:


open source

Next, an important link begins. As the saying goes, it is better to teach a man to fish than to give him a fish. I will Hanhan. I love your source code , and explain in detail the whole process of design and implementation.

design

This project took me about 3 hours to complete. In order to avoid weaknesses, I used a UI framework, so there is no additional UI design, and a few basic components are used to quickly start the project.

Technical selection

First, the first step is technology selection. Because what I want to provide is a free service, I try to choose some free service providers and some related technology stacks.

Choice of service provider:

  • Cloudflare : Provides free domain name resolution, CDN acceleration and open interfaces
  • Vercel : Free application hosting for individuals, support Node.js environment, use Next.js framework
  • PlanetScale : Cloud MySQL service with a certain free quota
  • Prisma : Cloud Studio management database

In fact, I originally wanted to use the Cloudflare family bucket, which is to use Cloudflare Pages (static website) + Cloudflare Workers (Serverless method execution) and KV (keyless method execution) Therefore, a simpler and faster implementation method is adopted.

Technology stack:

  • Typescript : While I like to do more with less code, TS brings me a more efficient stage for teamwork
  • Next.js : A full stack framework (using React on the front end, and a http module and Express on the back end) that supports SSR (Server Side Rendering) and SSG (Static Site Generation)

  • Prisma : The next-generation ORM framework that supports multiple databases (MySQL is used in this project) and database migration (Migration)
  • Tailwind CSS : The next generation CSS framework, practical first

Database Design

Since I am using the Authing user integration, the design of the user table and the design of the user-related interface are omitted.

// 域名类型
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])
}

Very simple, refer to the notes. In addition, I originally planned to save only one name, but due to repeated registration, for example, I registered a Chinese name Lao Wang, and you registered a corresponding punycode code name xn--qbyt9x , it will conflict, so simply (lazy ) save it.

Technical preparations

First build the Next.js website framework and deploy it to Vercel for testing. This can be coupled with Tailwind CSS and Authing SSO integration. The first preparatory work is complete.

interface design

For fast (lazy) implementation, I created four interfaces for adding, deleting, modifying, and checking.

Query interface:

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

Create an interface:

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

The database query whether the user has registered a domain name and whether there is a same name can be completed with one query. Here, in order to improve the query performance, it is split.

Modify the interface:

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

Deleting an interface is the same as modifying an interface. The mailbox interface is similar to the domain name and will not be repeated here.

Code

Packaging the Cloudflare SDK

Of course, there are also ready-made libraries that can be used directly, but because there are only a few lines of code, I made them by hand.

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;
};

Package verification tool class

A certain regular basis is required. If you need online debugging tools, you can visit: regexper.js.cool

Domain name (CNAME) verification regular:

/^((?!-))(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,})$/;

Email verification regular:

/^[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 checksum:

/^((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 checksum:

/^((([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}))|:)))(%.+)?$/;

page request wrapper

Take domain name registration submission as an example:

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('出错啦!请稍后重试');
  }
}

Reusable code can be encapsulated. Refer to the idea of software engineering: high cohesion, low coupling. What I am citing here is a relatively negative teaching material, with bloated code and low readability.

be careful

  • Thanks to the new JIT mechanism in Tailwind CSS 3, purgecss is no longer required
  • useState attention to React performance. Hooks such as 061f9db4d131e2 should be placed at the page level as much as possible, not at the component level (especially the components that will be generated cyclically)
  • Use useMemo such as 061f9db4d13202 and debounce for caching, anti-shake, and current limiting to improve application performance
  • When using the Next.js framework (or a normal React application), in most cases, swr and some of the core ideas inside it

The rest of the code is boring and simple.


That's about it. Remember to share!

Willin | . Han Han I love you | project source


willin
213 声望12 粉丝

欢迎在各平台 Follow 我。