跳到主要内容

博客

代码人生:编织技术与生活的博客之旅

前言

本篇我们讲解如何用 Vercel 部署我们的 Next.js 项目。

Vercel 公司

Vercel 既是一个产品也是一家公司。我们就先说说 Vercel 这家公司的故事。

Vercel 是由 Guillermo Rauch 创立的云服务公司,它的前身是 2015 年 Guillermo 创立的 Zeit,2020 年才更名为 Vercel。

Zeit 于 2016 年推出了核心产品 now,用于帮助开发者快速将应用部署到云端。因为在那个时候,云部署还并未像现在这样便利,now 将域名解析、证书、缓存等服务都做到产品内部,让用户能够一键部署,节省时间精力。

2016 年也正是前后端分离架构开始流行的时候,虽然前后端分离带来了前端的“繁荣”,但也带来了诸如 SEO 等问题。于是很多开发者开始设计 SSR 框架,Guillermo 也看到了这一问题(以及问题背后的机遇),创建了 Next.js,一个基于 React (当时正火)的 SSR 框架。在之后的这些年里,Next.js 持续深耕运营,如大家所见,Next.js 目前已成为 React 领域里的明星项目。

Next.js 的成功也带动了 Vercel 的发展,Vercel 也深度集成了 Next.js,这使得 Next.js 项目的开发者会更倾向于使用 Vercel 进行部署。2021 年,Vercel 完成了 1.5 亿美元的 D 轮融资。目前估值已经达到了 25 亿美金。

除此之外,Vercel 被人津津乐道的是它挖了不少业界大佬,如:

  • Sebastian Markbage:原 React 团队 Tech Lead
  • Rich Harries:sveltejs 作者 & rollup 核心贡献者
  • Donny:swc 作者
  • Tobias Koppers:webpack 作者
  • Alexander Akait:webpack 核心贡献者 & prettier 贡献者
  • Jared Palme:Turborepo 创始人
  • ……

目前 Next.js 依然由 Vercel 来维护,再加上全明星的开发团队,未来可期。

Vercel 产品

接下来我们说说 Vercel 这个产品,根据官网的介绍:

Vercel is the Frontend Cloud. Build, scale, and secure a faster, personalized web.

简单来说,Vercel 是一个网站托管平台,部署体验好。

具体来说,有以下这些功能特点:

  1. 部署方便:一键部署,可以快速将前端应用程序、静态网站、API 等部署到 CDN 上,支持自定义域名、HTTPS、数据监控等,与 Git 集成,支持自动化部署,当提交了新的代码时会自动构建并部署
  2. 性能与拓展性:支持多个框架的部署,并利用缓存、路由、边缘网络提供最佳性能和高流量处理能力
  3. Serverless 函数:可以轻松构建和部署无服务器函数和 API,无需关心服务器的维护和扩展
  4. 团队协作:支持团队协作功能,并与 GitHub、Gitlab 等平台无缝集成

不过国内因为一些原因,Vercel 部署的网站无法直接访问,使用 Vercel 部署的项目更适合用于出海。

使用 Vercel

使用 Vercel 部署自然需要一个账号,注册地址:vercel.com/signup,建议使用 GitHub 账号登陆,这样可以方便部署自己在 GitHub 上的项目。

部署纯前端项目

现在让我们部署一个纯前端项目来熟悉下基本流程吧!

导入项目

Vercel 支持导入 Git 项目或者使用 Vercel 提供的现成模板

点击上图左侧的 Install 按钮,会授权 Vercel 读取和写入 GitHub 的仓库和 Git 信息,可用于自动化部署。

这里我选择了自己账号下的 next-app-demo 项目,这是我们小册基础篇的 Demo:

如果你也想试着部署这个项目,可以选择 Import Third-Party Git Repository,然后输入地址:

https://github.com/mqyqingfeng/next-app-demo/tree/Intercepting-Routes

Vercel 会让你在 Git 平台创建一个对应仓库方便后续修改和部署,按照 Vercel 指示操作即可。

部署项目

进入部署页面后,因为我们的代码比较简单,不需要额外的填写,直接点击 Deploy即可:

当部署完成后会进入以下页面:

Dashboard 页面点击 Visit 按钮即可访问部署后的效果:

像我这次的部署地址为:next-app-demo-ebon.vercel.app/,效果如下:

此外,当你推送代码到这个分支的时候,Vercel 会自动进行部署。如果出现错误,你也可以选择右上角的 Instant Rollback 进行回滚。

自定义域名

如果我们想要使用自定义的域名呢?首先你要有一个自己的域名,域名购买和备案可以参考《一篇域名从购买到备案到解析的详细教程》

假设你已经有了域名,比如我有了 yayujs.com 这个域名,我希望将 nextdemo.yayujs.com这个域名解析到 Vercel 部署的地址。打开域名控制台(我是在万网买的域名,所以这里链接的地址是阿里云域名控制台),选择对应的域名,点击“解析”。

根据 Vercel 官方文档域名添加的介绍:

If the domain is in use by another Vercel account, you will need to verify access to the domain, with a TXT record

If you're using an Apex domain (e.g. example.com), you will need to configure it with an A record

If you're using a Subdomain (e.g. docs.example.com), you will need to configure it with a CNAME record

也就是说,如果是根域,就配置 A 记录,如果是子域,就配置 CNAME 记录。这里因为配置的是子域,所以选择的 CNAME,配置如下:

其中记录值填写我们项目部署的域名。

最后在 Vercel 的设置中添加设置的域名:

效果如下:

国内访问

Vercel 直接部署的域名是无法访问的,我们可以通过 tool.chinaz.com/dnsce 检测我们的域名:

如何让国内用户也可以访问呢?可以修改我们的域名解析:

如果用的 A 记录,记录值为 76.223.126.88

如果用的 CNAME,记录值为 cname-china.vercel-dns.com

过一小段时间后,再次检测:

注:不过有的时候还是会无法访问,国内还是建议用自己的服务器

部署 Next.js + Redis 项目

我们以 React Notes 的 day1 项目为例,此时我们代码中使用了 Next.js 和 Redis,我们看下如何部署。

下载项目

下载我们的 day1 分支代码:

git clone -b day1 git@github.com:mqyqingfeng/next-react-notes-demo.git

大家还记得 day1 实现的效果吗?我们本地运行以下代码:

cd next-react-notes-demo && npm i && npm run dev

因为 day1 代码需要开启 redis 服务,所以另起一个命令行运行:

redis-server

等 Redis 服务成功开启,此时打开 http://localhost:3000/,页面正常访问:

左侧笔记列表的标题和时间取自于 Redis 数据库,说明代码运行正常。

Vercel Cli

Vercel 提供了 Vercel Cli 用于命令行部署 Next.js 项目,全局安装 vercel 命令:

npm i -g vercel

安装完成后,运行以下命令,检查是否成功安装:

vercel --version

效果如下:

进入我们的项目根目录,目前在 day1 分支,因为 day1 分支的代码不准备再改动,所以我们切换出一个新的分支用于部署:

git checkout -b vercel-redis

项目根目录运行:

vercel

首次在项目中运行 vercel 时,Vercel CLI 需要知道要将项目部署到哪里。所以会有一系列的操作提示,这些操作会让你验证身份、在 Vercel 上创建项目,进行构建部署等等。交互效果如下:

此步会在项目构建部署的时候出错,因为我们的项目中用了 Redis,但此时并没有开启 Redis,所以运行 npm run build 会失败。

既然会失败,其实也没有必要部署这一次。如果你只是希望在 Vercel 上创建一个项目并进行关联,那就运行 vercel git connect,等需要部署的时候再运行 vercel deploy。交互效果如下:

创建数据库

通过刚才的步骤建立一个项目后,在 Vercel 平台进入创建的项目,选择 Storage 选项,这里展示了 Vercel 目前支持的四种数据库:

分别是:

  • Vercel KV:基于 Redis 的解决方案,适用于 Key/Value 和 JSON 数据,由 Upstash 提供支持
  • Vercel Postgres:基于 PostgreSQL 的解决方案,轻量关系型数据库,由 Neon 提供支持
  • Vercel Blob:提供文件存储解决方案,由 Cloudflare R2 提供支持
  • Edge Config:全局数据存储,能在 Edge Server 读取,适用于频繁读取但少有改动的配置

这里我们选择 KV,点击 Create,地区选择默认的即可(选择其他的还会提示你跟项目部署的地区不一致):

继续点击 Connect:

现在可以看到我们创建的 Redis 数据库的地址,Vercel 也贴心的提供了接下来要做的事情:

我们解释下这些要做的事情:

  1. Connect to a project:将已有的 Vercel 项目与该数据库进行关联,刚才已经点击了 Connect ,所以不需要再点了。Connect 后,Vercel 会为项目自动添加数据库相关的环境变量:

  1. Pull your latest environment variables:Vercel 提示让你在本地运行 vercel env pull .env.development.local,它的作用是在本地创建一个名为 .env.development.local的文件,自动写入上图中的这些环境变量的值,方便你在本地直接使用
  2. Install our package:为方便用户操作数据库,Vercel 提供了自己的库 @vercel/kv,具体 API 参考 vercel.com/docs/storag…

如果一开始就确定用 Vercel 部署,那最好使用 @vercel-kv,不过我们用到的 redis API 也比较简单,使用 ioredis 也是可以的。现在已经有了 redis 数据库的地址,我们修改下 /lib/redis.js

const redis = new Redis(process.env.REDIS_URL)

修改 .env.development.local,添加如下代码:

REDIS_URL="rediss://default:xxxxxxxxxxxxxx@xxxxxxxxxxx:33605"

我们做的修改就是复制原本的 KV_URLredis://xxxx改为 rediss://xxxx,加个 s 表示建立 SSL 连接。

此时本地已经可以成功运行:

现在我们将代码进行提交(注意 .env.development.local 顾名思义,不用提交):

git status
git add .
git commit -m "update redis.js"
git push origin vercel-redis

提交到 GitHub 后,Vercel 会自动进行部署。但此时部署会失败:

因为服务端的环境变量还没有建立,我们在该 Vercel 项目上添加一个新的环境变量 REDIS_URL

然后重新部署,点击 Redeploy:

此次应该会成功部署:

Preview 与 Production

此时我们已经成功的部署了一个 Next.js + Redis 的项目。在后续的发布中,有一点要注意,那就是 Preview 与 Production 环境的区别。

现在我们用的是 vercel-redis 分支,当第一次推送的时候,Vercel 会将其内容部署到生产环境,但是比如你修改了一些内容,然后再次推送到 vercel-redis 分支,Vercel 会进行自动化部署,但会放在 Preview 环境中:

这是因为 Vercel 默认将 main / master 分支用于生产环境,只有当你推送代码到 main/master 分支的时候,才会进行生产部署,推送到其他分支就是 Preview 环境。这很好,可以进行多个版本的开发预览。但如果你就是想要指定如 vercel-redis 分支作为生产环境部署,可以在 Settings 中修改:

此时再推送到 vercel-redis 分支就会进行生产部署。

部署 Next.js + 关系型数据库 + Prisma 项目

通过前面的示例,想必你已经对 Vercel 平台的使用有所了解,那就来正式部署我们的项目吧。

下载项目

依然选用我们的 day11 分支代码。此时我们的技术选型是 Next.js + MySQL + Prisma。

下载我们的 day11 分支代码:

git clone -b day11 git@github.com:mqyqingfeng/next-react-notes-demo.git

老规矩,先本地运行一下,验证代码无问题:

# 注意要在本地开启 MySQL 后运行:
npm i && npm run dev

正如大家在上节看到,Vercel 是没有提供 MySQL 数据库的,关系型数据库只有 Vercel Postgres。如果确定用 MySQL 数据库,那可以使用搭配使用 PlanetScale,它是一个 MySQL 云数据库。如果用 Mongodb,则通常会搭配 MongoDB Atlas 云数据库。所以对于我们的项目,部署有两种选择:

  1. 改用 Vercel 提供的 Vercel Postgres,也就是改用 PostgreSQL 数据库
  2. 使用 PlanetScale 云 MySQL 数据库

从技术选型的角度来讲,如果我知道最终用 Vercel 进行部署,我可能一开始就会选择用 PostgreSQL 数据库。

从钱的角度来讲,Vercel 的免费版只支持一个 PostgreSQL 数据库,且有 256MB 和每月 60h 计算时间的限制。PlanetScale 的免费版则是 5 GB 存储空间,10 亿行读取次数每月,1000 万行写入每月。更推荐用 PlanetScale。

从学习的角度来讲,就让我们顺便学习一下 Prisma 和 PostgreSQL 如何搭配使用吧!

所以我们改用 PostgreSQL 数据库(其实两种方式操作差不多)。所幸我们的项目用了 Prisma,切换的成本并不高。让我们看看如何实现吧!

切换数据库

进入项目目录,运行 vercel git connect在 Vercel 平台上建立关联项目:

建立项目后,选择 Storage选项,建立一个 Vercel Postgres 数据库,最终获得该数据库地址为:

拷贝上图 .env.local 选项中的环境变量,将其写入 .env文件:

注:为什么不运行 vercel env pull .env呢?因为这会强行覆盖 .env 文件。为什么不写入其他文件如 .env.local呢?因为 prisma 默认读取的是 .env 文件中的环境变量。为了简单起见选择手动拷贝的方式。

修改 prisma/schema.prisma文件:

datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL")
}

运行 npx prisma db push,将数据模型同步数据库。

现在,让我们在本地再次运行 npm run dev校验数据库切换是否有问题:

部署线上

修改 package.json,代码如下:

{
"scripts": {
"dev": "npx prisma generate && next dev",
"build": "npx prisma generate && npx prisma db push && next build"
}
}

修改 .env.production

# 注释掉 AUTH_URL,v5 之后默认不需要了,但比如用了代理的时候依然需要
# AUTH_URL=https://notes.yayujs.com

因为使用了 next-auth,在 Vercel 项目的环境变量中需要添加 AUTH_SECRET

目前我们还在 day11 分支,切换为新的 vercel-postgres分支,然后将代码提交到远程的 vercel-postgres分支,因为是首次部署,所以会部署到生产版本,交互效果如下:

Vercel 自动部署后,效果如下:

部署后的项目源码:github.com/mqyqingfeng…

参考链接

  1. juejin.cn/post/705733…
  2. www.zhihu.com/question/50…
  3. vercel.com/
  4. lastrev.com/blog/introd…
  5. github.com/vercel/exam…
defisolidityuniswapdexsmart-contracts阅读需 13 分钟

前言

本篇我们讲解如何用 Vercel 部署我们的 Next.js 项目。

Vercel 公司

Vercel 既是一个产品也是一家公司。我们就先说说 Vercel 这家公司的故事。

Vercel 是由 Guillermo Rauch 创立的云服务公司,它的前身是 2015 年 Guillermo 创立的 Zeit,2020 年才更名为 Vercel。

Zeit 于 2016 年推出了核心产品 now,用于帮助开发者快速将应用部署到云端。因为在那个时候,云部署还并未像现在这样便利,now 将域名解析、证书、缓存等服务都做到产品内部,让用户能够一键部署,节省时间精力。

2016 年也正是前后端分离架构开始流行的时候,虽然前后端分离带来了前端的“繁荣”,但也带来了诸如 SEO 等问题。于是很多开发者开始设计 SSR 框架,Guillermo 也看到了这一问题(以及问题背后的机遇),创建了 Next.js,一个基于 React (当时正火)的 SSR 框架。在之后的这些年里,Next.js 持续深耕运营,如大家所见,Next.js 目前已成为 React 领域里的明星项目。

Next.js 的成功也带动了 Vercel 的发展,Vercel 也深度集成了 Next.js,这使得 Next.js 项目的开发者会更倾向于使用 Vercel 进行部署。2021 年,Vercel 完成了 1.5 亿美元的 D 轮融资。目前估值已经达到了 25 亿美金。

除此之外,Vercel 被人津津乐道的是它挖了不少业界大佬,如:

  • Sebastian Markbage:原 React 团队 Tech Lead
  • Rich Harries:sveltejs 作者 & rollup 核心贡献者
  • Donny:swc 作者
  • Tobias Koppers:webpack 作者
  • Alexander Akait:webpack 核心贡献者 & prettier 贡献者
  • Jared Palme:Turborepo 创始人
  • ……

目前 Next.js 依然由 Vercel 来维护,再加上全明星的开发团队,未来可期。

Vercel 产品

接下来我们说说 Vercel 这个产品,根据官网的介绍:

Vercel is the Frontend Cloud. Build, scale, and secure a faster, personalized web.

简单来说,Vercel 是一个网站托管平台,部署体验好。

具体来说,有以下这些功能特点:

  1. 部署方便:一键部署,可以快速将前端应用程序、静态网站、API 等部署到 CDN 上,支持自定义域名、HTTPS、数据监控等,与 Git 集成,支持自动化部署,当提交了新的代码时会自动构建并部署
  2. 性能与拓展性:支持多个框架的部署,并利用缓存、路由、边缘网络提供最佳性能和高流量处理能力
  3. Serverless 函数:可以轻松构建和部署无服务器函数和 API,无需关心服务器的维护和扩展
  4. 团队协作:支持团队协作功能,并与 GitHub、Gitlab 等平台无缝集成

不过国内因为一些原因,Vercel 部署的网站无法直接访问,使用 Vercel 部署的项目更适合用于出海。

使用 Vercel

使用 Vercel 部署自然需要一个账号,注册地址:vercel.com/signup,建议使用 GitHub 账号登陆,这样可以方便部署自己在 GitHub 上的项目。

部署纯前端项目

现在让我们部署一个纯前端项目来熟悉下基本流程吧!

导入项目

Vercel 支持导入 Git 项目或者使用 Vercel 提供的现成模板

点击上图左侧的 Install 按钮,会授权 Vercel 读取和写入 GitHub 的仓库和 Git 信息,可用于自动化部署。

这里我选择了自己账号下的 next-app-demo 项目,这是我们小册基础篇的 Demo:

如果你也想试着部署这个项目,可以选择 Import Third-Party Git Repository,然后输入地址:

https://github.com/mqyqingfeng/next-app-demo/tree/Intercepting-Routes

Vercel 会让你在 Git 平台创建一个对应仓库方便后续修改和部署,按照 Vercel 指示操作即可。

部署项目

进入部署页面后,因为我们的代码比较简单,不需要额外的填写,直接点击 Deploy即可:

当部署完成后会进入以下页面:

Dashboard 页面点击 Visit 按钮即可访问部署后的效果:

像我这次的部署地址为:next-app-demo-ebon.vercel.app/,效果如下:

此外,当你推送代码到这个分支的时候,Vercel 会自动进行部署。如果出现错误,你也可以选择右上角的 Instant Rollback 进行回滚。

自定义域名

如果我们想要使用自定义的域名呢?首先你要有一个自己的域名,域名购买和备案可以参考《一篇域名从购买到备案到解析的详细教程》

假设你已经有了域名,比如我有了 yayujs.com 这个域名,我希望将 nextdemo.yayujs.com这个域名解析到 Vercel 部署的地址。打开域名控制台(我是在万网买的域名,所以这里链接的地址是阿里云域名控制台),选择对应的域名,点击“解析”。

根据 Vercel 官方文档域名添加的介绍:

If the domain is in use by another Vercel account, you will need to verify access to the domain, with a TXT record

If you're using an Apex domain (e.g. example.com), you will need to configure it with an A record

If you're using a Subdomain (e.g. docs.example.com), you will need to configure it with a CNAME record

也就是说,如果是根域,就配置 A 记录,如果是子域,就配置 CNAME 记录。这里因为配置的是子域,所以选择的 CNAME,配置如下:

其中记录值填写我们项目部署的域名。

最后在 Vercel 的设置中添加设置的域名:

效果如下:

国内访问

Vercel 直接部署的域名是无法访问的,我们可以通过 tool.chinaz.com/dnsce 检测我们的域名:

如何让国内用户也可以访问呢?可以修改我们的域名解析:

如果用的 A 记录,记录值为 76.223.126.88

如果用的 CNAME,记录值为 cname-china.vercel-dns.com

过一小段时间后,再次检测:

注:不过有的时候还是会无法访问,国内还是建议用自己的服务器

部署 Next.js + Redis 项目

我们以 React Notes 的 day1 项目为例,此时我们代码中使用了 Next.js 和 Redis,我们看下如何部署。

下载项目

下载我们的 day1 分支代码:

git clone -b day1 git@github.com:mqyqingfeng/next-react-notes-demo.git

大家还记得 day1 实现的效果吗?我们本地运行以下代码:

cd next-react-notes-demo && npm i && npm run dev

因为 day1 代码需要开启 redis 服务,所以另起一个命令行运行:

redis-server

等 Redis 服务成功开启,此时打开 http://localhost:3000/,页面正常访问:

左侧笔记列表的标题和时间取自于 Redis 数据库,说明代码运行正常。

Vercel Cli

Vercel 提供了 Vercel Cli 用于命令行部署 Next.js 项目,全局安装 vercel 命令:

npm i -g vercel

安装完成后,运行以下命令,检查是否成功安装:

vercel --version

效果如下:

进入我们的项目根目录,目前在 day1 分支,因为 day1 分支的代码不准备再改动,所以我们切换出一个新的分支用于部署:

git checkout -b vercel-redis

项目根目录运行:

vercel

首次在项目中运行 vercel 时,Vercel CLI 需要知道要将项目部署到哪里。所以会有一系列的操作提示,这些操作会让你验证身份、在 Vercel 上创建项目,进行构建部署等等。交互效果如下:

此步会在项目构建部署的时候出错,因为我们的项目中用了 Redis,但此时并没有开启 Redis,所以运行 npm run build 会失败。

既然会失败,其实也没有必要部署这一次。如果你只是希望在 Vercel 上创建一个项目并进行关联,那就运行 vercel git connect,等需要部署的时候再运行 vercel deploy。交互效果如下:

创建数据库

通过刚才的步骤建立一个项目后,在 Vercel 平台进入创建的项目,选择 Storage 选项,这里展示了 Vercel 目前支持的四种数据库:

分别是:

  • Vercel KV:基于 Redis 的解决方案,适用于 Key/Value 和 JSON 数据,由 Upstash 提供支持
  • Vercel Postgres:基于 PostgreSQL 的解决方案,轻量关系型数据库,由 Neon 提供支持
  • Vercel Blob:提供文件存储解决方案,由 Cloudflare R2 提供支持
  • Edge Config:全局数据存储,能在 Edge Server 读取,适用于频繁读取但少有改动的配置

这里我们选择 KV,点击 Create,地区选择默认的即可(选择其他的还会提示你跟项目部署的地区不一致):

继续点击 Connect:

现在可以看到我们创建的 Redis 数据库的地址,Vercel 也贴心的提供了接下来要做的事情:

我们解释下这些要做的事情:

  1. Connect to a project:将已有的 Vercel 项目与该数据库进行关联,刚才已经点击了 Connect ,所以不需要再点了。Connect 后,Vercel 会为项目自动添加数据库相关的环境变量:

  1. Pull your latest environment variables:Vercel 提示让你在本地运行 vercel env pull .env.development.local,它的作用是在本地创建一个名为 .env.development.local的文件,自动写入上图中的这些环境变量的值,方便你在本地直接使用
  2. Install our package:为方便用户操作数据库,Vercel 提供了自己的库 @vercel/kv,具体 API 参考 vercel.com/docs/storag…

如果一开始就确定用 Vercel 部署,那最好使用 @vercel-kv,不过我们用到的 redis API 也比较简单,使用 ioredis 也是可以的。现在已经有了 redis 数据库的地址,我们修改下 /lib/redis.js

const redis = new Redis(process.env.REDIS_URL)

修改 .env.development.local,添加如下代码:

REDIS_URL="rediss://default:xxxxxxxxxxxxxx@xxxxxxxxxxx:33605"

我们做的修改就是复制原本的 KV_URLredis://xxxx改为 rediss://xxxx,加个 s 表示建立 SSL 连接。

此时本地已经可以成功运行:

现在我们将代码进行提交(注意 .env.development.local 顾名思义,不用提交):

git status
git add .
git commit -m "update redis.js"
git push origin vercel-redis

提交到 GitHub 后,Vercel 会自动进行部署。但此时部署会失败:

因为服务端的环境变量还没有建立,我们在该 Vercel 项目上添加一个新的环境变量 REDIS_URL

然后重新部署,点击 Redeploy:

此次应该会成功部署:

Preview 与 Production

此时我们已经成功的部署了一个 Next.js + Redis 的项目。在后续的发布中,有一点要注意,那就是 Preview 与 Production 环境的区别。

现在我们用的是 vercel-redis 分支,当第一次推送的时候,Vercel 会将其内容部署到生产环境,但是比如你修改了一些内容,然后再次推送到 vercel-redis 分支,Vercel 会进行自动化部署,但会放在 Preview 环境中:

这是因为 Vercel 默认将 main / master 分支用于生产环境,只有当你推送代码到 main/master 分支的时候,才会进行生产部署,推送到其他分支就是 Preview 环境。这很好,可以进行多个版本的开发预览。但如果你就是想要指定如 vercel-redis 分支作为生产环境部署,可以在 Settings 中修改:

此时再推送到 vercel-redis 分支就会进行生产部署。

部署 Next.js + 关系型数据库 + Prisma 项目

通过前面的示例,想必你已经对 Vercel 平台的使用有所了解,那就来正式部署我们的项目吧。

下载项目

依然选用我们的 day11 分支代码。此时我们的技术选型是 Next.js + MySQL + Prisma。

下载我们的 day11 分支代码:

git clone -b day11 git@github.com:mqyqingfeng/next-react-notes-demo.git

老规矩,先本地运行一下,验证代码无问题:

# 注意要在本地开启 MySQL 后运行:
npm i && npm run dev

正如大家在上节看到,Vercel 是没有提供 MySQL 数据库的,关系型数据库只有 Vercel Postgres。如果确定用 MySQL 数据库,那可以使用搭配使用 PlanetScale,它是一个 MySQL 云数据库。如果用 Mongodb,则通常会搭配 MongoDB Atlas 云数据库。所以对于我们的项目,部署有两种选择:

  1. 改用 Vercel 提供的 Vercel Postgres,也就是改用 PostgreSQL 数据库
  2. 使用 PlanetScale 云 MySQL 数据库

从技术选型的角度来讲,如果我知道最终用 Vercel 进行部署,我可能一开始就会选择用 PostgreSQL 数据库。

从钱的角度来讲,Vercel 的免费版只支持一个 PostgreSQL 数据库,且有 256MB 和每月 60h 计算时间的限制。PlanetScale 的免费版则是 5 GB 存储空间,10 亿行读取次数每月,1000 万行写入每月。更推荐用 PlanetScale。

从学习的角度来讲,就让我们顺便学习一下 Prisma 和 PostgreSQL 如何搭配使用吧!

所以我们改用 PostgreSQL 数据库(其实两种方式操作差不多)。所幸我们的项目用了 Prisma,切换的成本并不高。让我们看看如何实现吧!

切换数据库

进入项目目录,运行 vercel git connect在 Vercel 平台上建立关联项目:

建立项目后,选择 Storage选项,建立一个 Vercel Postgres 数据库,最终获得该数据库地址为:

拷贝上图 .env.local 选项中的环境变量,将其写入 .env文件:

注:为什么不运行 vercel env pull .env呢?因为这会强行覆盖 .env 文件。为什么不写入其他文件如 .env.local呢?因为 prisma 默认读取的是 .env 文件中的环境变量。为了简单起见选择手动拷贝的方式。

修改 prisma/schema.prisma文件:

datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL")
}

运行 npx prisma db push,将数据模型同步数据库。

现在,让我们在本地再次运行 npm run dev校验数据库切换是否有问题:

部署线上

修改 package.json,代码如下:

{
"scripts": {
"dev": "npx prisma generate && next dev",
"build": "npx prisma generate && npx prisma db push && next build"
}
}

修改 .env.production

# 注释掉 AUTH_URL,v5 之后默认不需要了,但比如用了代理的时候依然需要
# AUTH_URL=https://notes.yayujs.com

因为使用了 next-auth,在 Vercel 项目的环境变量中需要添加 AUTH_SECRET

目前我们还在 day11 分支,切换为新的 vercel-postgres分支,然后将代码提交到远程的 vercel-postgres分支,因为是首次部署,所以会部署到生产版本,交互效果如下:

Vercel 自动部署后,效果如下:

部署后的项目源码:github.com/mqyqingfeng…

参考链接

  1. juejin.cn/post/705733…
  2. www.zhihu.com/question/50…
  3. vercel.com/
  4. lastrev.com/blog/introd…
  5. github.com/vercel/exam…
defisolidityuniswapdexsmart-contracts阅读需 11 分钟

NFT(非同质化代币)作为区块链技术的重要应用,已经在艺术、游戏、收藏品等领域展现出巨大潜力。本文将带你从零开始构建一个功能完整的NFT市场。

NFT基础概念

什么是NFT?

NFT(Non-Fungible Token)是非同质化代币,每个代币都是独一无二的,不可互换。这与比特币等同质化代币形成对比。

ERC721标准

ERC721是以太坊上NFT的标准协议,定义了NFT的基本接口:

interface IERC721 {
function balanceOf(address owner) external view returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address operator);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);
}

构建NFT合约

1. 基础NFT合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract MyNFT is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;

uint256 public maxSupply = 10000;
uint256 public mintPrice = 0.01 ether;

event Minted(address indexed to, uint256 indexed tokenId, string tokenURI);

constructor(string memory name, string memory symbol)
ERC721(name, symbol)
Ownable(msg.sender)
{}

function mintNFT(string memory tokenURI) public payable returns (uint256) {
require(_tokenIds.current() < maxSupply, "Max supply reached");
require(msg.value >= mintPrice, "Insufficient payment");

_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();

_safeMint(msg.sender, newTokenId);
_setTokenURI(newTokenId, tokenURI);

emit Minted(msg.sender, newTokenId, tokenURI);

return newTokenId;
}

function totalMinted() public view returns (uint256) {
return _tokenIds.current();
}

function withdraw() public onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No funds to withdraw");
payable(owner()).transfer(balance);
}

// Override required functions
function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize)
internal
override(ERC721, ERC721Enumerable)
{
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}

function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}

function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}

function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}

2. 带有版税的NFT合约

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Royalty.sol";

contract RoyaltyNFT is MyNFT, ERC721Royalty {
uint96 public royaltyFee = 250; // 2.5% royalty (250 / 10000)

constructor(string memory name, string memory symbol)
MyNFT(name, symbol)
ERC721Royalty()
{
_setDefaultRoyalty(msg.sender, royaltyFee);
}

function mintNFT(string memory tokenURI) public payable returns (uint256) {
uint256 tokenId = super.mintNFT(tokenURI);
return tokenId;
}

function setRoyalty(address receiver, uint96 feeNumerator) external onlyOwner {
_setDefaultRoyalty(receiver, feeNumerator);
}

// Override required functions
function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize)
internal
override(MyNFT, ERC721Enumerable)
{
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}

function _burn(uint256 tokenId)
internal
override(MyNFT, ERC721URIStorage, ERC721Royalty)
{
super._burn(tokenId);
}

function supportsInterface(bytes4 interfaceId)
public
view
override(MyNFT, ERC721Royalty)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}

构建NFT市场合约

1. 基础市场合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract NFTMarketplace is ReentrancyGuard, Pausable, Ownable, IERC721Receiver {
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
bool isActive;
}

struct Auction {
address seller;
address nftContract;
uint256 tokenId;
uint256 startingPrice;
uint256 highestBid;
address highestBidder;
uint256 endTime;
bool isActive;
}

mapping(uint256 => Listing) public listings;
mapping(uint256 => Auction) public auctions;
mapping(address => uint256) public pendingWithdrawals;

uint256 public listingFee = 0.001 ether;
uint256 public auctionFee = 0.002 ether;
uint256 public marketplaceFee = 250; // 2.5% (250/10000)
uint256 public nextListingId;
uint256 public nextAuctionId;

event NFTListed(uint256 indexed listingId, address indexed seller, address indexed nftContract, uint256 tokenId, uint256 price);
event NFTPurchased(uint256 indexed listingId, address indexed buyer, uint256 price);
event NFTDelisted(uint256 indexed listingId);
event AuctionCreated(uint256 indexed auctionId, address indexed seller, address indexed nftContract, uint256 tokenId, uint256 startingPrice, uint256 endTime);
event BidPlaced(uint256 indexed auctionId, address indexed bidder, uint256 amount);
event AuctionEnded(uint256 indexed auctionId, address indexed winner, uint256 amount);

constructor() Ownable(msg.sender) {}

// List NFT for sale
function listNFT(address _nftContract, uint256 _tokenId, uint256 _price) external payable nonReentrant whenNotPaused {
require(msg.value >= listingFee, "Insufficient listing fee");
require(_price > 0, "Price must be greater than 0");

IERC721 nft = IERC721(_nftContract);
require(nft.ownerOf(_tokenId) == msg.sender, "Not the owner");
require(nft.getApproved(_tokenId) == address(this), "Marketplace not approved");

uint256 listingId = nextListingId++;
listings[listingId] = Listing({
seller: msg.sender,
nftContract: _nftContract,
tokenId: _tokenId,
price: _price,
isActive: true
});

nft.safeTransferFrom(msg.sender, address(this), _tokenId);

emit NFTListed(listingId, msg.sender, _nftContract, _tokenId, _price);
}

// Purchase listed NFT
function purchaseNFT(uint256 _listingId) external payable nonReentrant {
Listing storage listing = listings[_listingId];
require(listing.isActive, "Listing not active");
require(msg.value >= listing.price, "Insufficient payment");

listing.isActive = false;

uint256 fee = (listing.price * marketplaceFee) / 10000;
uint256 sellerProceeds = listing.price - fee;

pendingWithdrawals[listing.seller] += sellerProceeds;
pendingWithdrawals[owner()] += fee;

if (msg.value > listing.price) {
pendingWithdrawals[msg.sender] += msg.value - listing.price;
}

IERC721(listing.nftContract).safeTransferFrom(address(this), msg.sender, listing.tokenId);

emit NFTPurchased(_listingId, msg.sender, listing.price);
}

// Create auction
function createAuction(address _nftContract, uint256 _tokenId, uint256 _startingPrice, uint256 _duration) external payable nonReentrant whenNotPaused {
require(msg.value >= auctionFee, "Insufficient auction fee");
require(_startingPrice > 0, "Starting price must be greater than 0");
require(_duration >= 3600 && _duration <= 604800, "Duration must be between 1 hour and 7 days");

IERC721 nft = IERC721(_nftContract);
require(nft.ownerOf(_tokenId) == msg.sender, "Not the owner");
require(nft.getApproved(_tokenId) == address(this), "Marketplace not approved");

uint256 auctionId = nextAuctionId++;
auctions[auctionId] = Auction({
seller: msg.sender,
nftContract: _nftContract,
tokenId: _tokenId,
startingPrice: _startingPrice,
highestBid: _startingPrice,
highestBidder: address(0),
endTime: block.timestamp + _duration,
isActive: true
});

nft.safeTransferFrom(msg.sender, address(this), _tokenId);

emit AuctionCreated(auctionId, msg.sender, _nftContract, _tokenId, _startingPrice, block.timestamp + _duration);
}

// Place bid
function placeBid(uint256 _auctionId) external payable nonReentrant {
Auction storage auction = auctions[_auctionId];
require(auction.isActive, "Auction not active");
require(block.timestamp < auction.endTime, "Auction ended");
require(msg.value > auction.highestBid, "Bid too low");

if (auction.highestBidder != address(0)) {
pendingWithdrawals[auction.highestBidder] += auction.highestBid;
}

auction.highestBid = msg.value;
auction.highestBidder = msg.sender;

emit BidPlaced(_auctionId, msg.sender, msg.value);
}

// End auction
function endAuction(uint256 _auctionId) external nonReentrant {
Auction storage auction = auctions[_auctionId];
require(auction.isActive, "Auction not active");
require(block.timestamp >= auction.endTime, "Auction not ended");

auction.isActive = false;

if (auction.highestBidder != address(0)) {
uint256 fee = (auction.highestBid * marketplaceFee) / 10000;
uint256 sellerProceeds = auction.highestBid - fee;

pendingWithdrawals[auction.seller] += sellerProceeds;
pendingWithdrawals[owner()] += fee;

IERC721(auction.nftContract).safeTransferFrom(address(this), auction.highestBidder, auction.tokenId);

emit AuctionEnded(_auctionId, auction.highestBidder, auction.highestBid);
} else {
IERC721(auction.nftContract).safeTransferFrom(address(this), auction.seller, auction.tokenId);
}
}

// Withdraw funds
function withdraw() external nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No funds to withdraw");

pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}

// Admin functions
function setListingFee(uint256 _fee) external onlyOwner {
listingFee = _fee;
}

function setAuctionFee(uint256 _fee) external onlyOwner {
auctionFee = _fee;
}

function setMarketplaceFee(uint256 _fee) external onlyOwner {
require(_fee <= 1000, "Fee too high"); // Max 10%
marketplaceFee = _fee;
}

function pause() external onlyOwner {
_pause();
}

function unpause() external onlyOwner {
_unpause();
}

// Emergency functions
function emergencyWithdraw() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}

// IERC721Receiver implementation
function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) {
return this.onERC721Received.selector;
}

receive() external payable {}
}

2. 批量交易功能

contract NFTMarketplaceBatch is NFTMarketplace {
struct BatchListing {
address nftContract;
uint256[] tokenIds;
uint256[] prices;
}

event BatchNFTListed(uint256 indexed batchId, address indexed seller, uint256 totalItems);
event BatchNFTPurchased(uint256 indexed batchId, address indexed buyer, uint256 totalPrice);

function listBatch(BatchListing calldata batchListing) external payable nonReentrant whenNotPaused {
require(batchListing.tokenIds.length == batchListing.prices.length, "Arrays length mismatch");
require(batchListing.tokenIds.length > 0, "Empty batch");
require(msg.value >= listingFee * batchListing.tokenIds.length, "Insufficient listing fee");

IERC721 nft = IERC721(batchListing.nftContract);

for (uint i = 0; i < batchListing.tokenIds.length; i++) {
require(batchListing.prices[i] > 0, "Price must be greater than 0");
require(nft.ownerOf(batchListing.tokenIds[i]) == msg.sender, "Not the owner");

uint256 listingId = nextListingId++;
listings[listingId] = Listing({
seller: msg.sender,
nftContract: batchListing.nftContract,
tokenId: batchListing.tokenIds[i],
price: batchListing.prices[i],
isActive: true
});

nft.safeTransferFrom(msg.sender, address(this), batchListing.tokenIds[i]);
}

emit BatchNFTListed(nextListingId - 1, msg.sender, batchListing.tokenIds.length);
}

function purchaseBatch(uint256[] calldata listingIds) external payable nonReentrant {
uint256 totalPrice = 0;

for (uint i = 0; i < listingIds.length; i++) {
Listing storage listing = listings[listingIds[i]];
require(listing.isActive, "Listing not active");
totalPrice += listing.price;
}

require(msg.value >= totalPrice, "Insufficient payment");

for (uint i = 0; i < listingIds.length; i++) {
Listing storage listing = listings[listingIds[i]];
listing.isActive = false;

uint256 fee = (listing.price * marketplaceFee) / 10000;
uint256 sellerProceeds = listing.price - fee;

pendingWithdrawals[listing.seller] += sellerProceeds;
pendingWithdrawals[owner()] += fee;

IERC721(listing.nftContract).safeTransferFrom(address(this), msg.sender, listing.tokenId);
}

if (msg.value > totalPrice) {
pendingWithdrawals[msg.sender] += msg.value - totalPrice;
}

emit BatchNFTPurchased(listingIds[0], msg.sender, totalPrice);
}
}

前端集成

1. 连接钱包

import { ethers } from 'ethers';
import NFTMarketplace from './artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json';

const MARKETPLACE_ADDRESS = "0x...";

export async function connectWallet() {
if (typeof window.ethereum !== 'undefined') {
try {
await window.ethereum.request({ method: 'eth_requestAccounts' });
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const address = await signer.getAddress();

const marketplace = new ethers.Contract(
MARKETPLACE_ADDRESS,
NFTMarketplace.abi,
signer
);

return { provider, signer, address, marketplace };
} catch (error) {
console.error('Failed to connect wallet:', error);
throw error;
}
} else {
throw new Error('MetaMask not installed');
}
}

2. 列出NFT

export async function listNFT(marketplace, nftContract, tokenId, price) {
try {
const tx = await marketplace.listNFT(
nftContract,
tokenId,
ethers.utils.parseEther(price.toString()),
{ value: ethers.utils.parseEther("0.001") } // listing fee
);

await tx.wait();
console.log('NFT listed successfully');
return tx.hash;
} catch (error) {
console.error('Failed to list NFT:', error);
throw error;
}
}

3. 购买NFT

export async function purchaseNFT(marketplace, listingId, price) {
try {
const tx = await marketplace.purchaseNFT(listingId, {
value: ethers.utils.parseEther(price.toString())
});

await tx.wait();
console.log('NFT purchased successfully');
return tx.hash;
} catch (error) {
console.error('Failed to purchase NFT:', error);
throw error;
}
}

部署和测试

1. Hardhat配置

// hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan");
require("dotenv").config();

module.exports = {
solidity: "0.8.19",
networks: {
goerli: {
url: `https://goerli.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
accounts: [process.env.PRIVATE_KEY]
},
mainnet: {
url: `https://mainnet.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
accounts: [process.env.PRIVATE_KEY]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};

2. 部署脚本

// scripts/deploy.js
const hre = require("hardhat");

async function main() {
const NFTMarketplace = await hre.ethers.getContractFactory("NFTMarketplace");
const marketplace = await NFTMarketplace.deploy();

await marketplace.deployed();

console.log("NFTMarketplace deployed to:", marketplace.address);

// 验证合约
if (network.name !== "localhost" && network.name !== "hardhat") {
await hre.run("verify:verify", {
address: marketplace.address,
constructorArguments: [],
});
}
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

总结

通过构建这个NFT市场,我们学习了:

  1. ERC721标准: NFT的基本实现
  2. 市场合约: 买卖NFT的核心逻辑
  3. 拍卖功能: 竞价机制的实现
  4. 版税机制: 创作者收益的保障
  5. 批量交易: 提高用户体验
  6. 前端集成: Web3应用的完整流程

下一步

  1. 优化gas费用: 使用代理模式降低部署成本
  2. 添加更多功能: 报价、分期付款等
  3. 多链支持: 支持多个区块链网络
  4. IPFS集成: 去中心化存储NFT元数据
  5. 移动应用: 开发移动端DApp

继续探索NFT的无限可能!

参考资料

nftsoliditysmart-contractsweb3ethereum阅读需 6 分钟

去中心化金融(DeFi)是区块链技术最具革命性的应用之一。本文将带你从零开始构建一个简化版的去中心化交易所,理解AMM(自动做市商)的核心机制。

什么是AMM?

AMM(Automated Market Maker)自动做市商,通过算法自动为交易对提供流动性。不同于传统订单簿模式,AMM使用流动性池和定价公式来实现代币交换。

核心概念

  1. 流动性池(Liquidity Pool): 存放两种代币的智能合约
  2. 恒定乘积公式: x * y = k
  3. 流动性提供者(LP): 向池子提供代币的用户
  4. 滑点(Slippage): 交易对价格的影响程度

构建基础DEX

1. 核心合约结构

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}

contract SimpleDEX {
IERC20 public token0;
IERC20 public token1;

uint256 public reserve0; // 代币0的储备量
uint256 public reserve1; // 代币1的储备量
uint256 public totalSupply; // LP代币总供应量
mapping(address => uint256) public balanceOf; // 用户LP代币余额

event Swap(address indexed sender, uint amount0In, uint amount1In, uint amount0Out, uint amount1Out, address indexed to);
event LiquidityAdded(address indexed provider, uint amount0, uint amount1, uint liquidity);
event LiquidityRemoved(address indexed provider, uint amount0, uint amount1, uint liquidity);

constructor(address _token0, address _token1) {
token0 = IERC20(_token0);
token1 = IERC20(_token1);
}

// 获取流动性份额
function _mint(address _to, uint _amount) private {
balanceOf[_to] += _amount;
totalSupply += _amount;
}

// 销毁流动性份额
function _burn(address _from, uint _amount) private {
balanceOf[_from] -= _amount;
totalSupply -= _amount;
}

// 更新储备量
function _update(uint _reserve0, uint _reserve1) private {
reserve0 = _reserve0;
reserve1 = _reserve1;
}
}

2. 添加流动性

function addLiquidity(uint _amount0, uint _amount1) external returns (uint liquidity) {
// 转账代币到合约
token0.transferFrom(msg.sender, address(this), _amount0);
token1.transferFrom(msg.sender, address(this), _amount1);

uint _reserve0 = reserve0;
uint _reserve1 = reserve1;

if (_reserve0 == 0 && _reserve1 == 0) {
// 首次添加流动性
liquidity = sqrt(_amount0 * _amount1);
} else {
// 按比例添加
require(_amount0 * _reserve1 == _amount1 * _reserve0, "Invalid ratio");
liquidity = min((_amount0 * totalSupply) / _reserve0, (_amount1 * totalSupply) / _reserve1);
}

require(liquidity > 0, "Insufficient liquidity");

_mint(msg.sender, liquidity);
_update(_reserve0 + _amount0, _reserve1 + _amount1);

emit LiquidityAdded(msg.sender, _amount0, _amount1, liquidity);
}

3. 移除流动性

function removeLiquidity(uint _liquidity) external returns (uint amount0, uint amount1) {
require(balanceOf[msg.sender] >= _liquidity, "Insufficient balance");

uint _reserve0 = reserve0;
uint _reserve1 = reserve1;

// 计算可提取的代币数量
amount0 = (_liquidity * _reserve0) / totalSupply;
amount1 = (_liquidity * _reserve1) / totalSupply;

require(amount0 > 0 && amount1 > 0, "Invalid amounts");

_burn(msg.sender, _liquidity);

// 转账代币给用户
token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);

_update(_reserve0 - amount0, _reserve1 - amount1);

emit LiquidityRemoved(msg.sender, amount0, amount1, _liquidity);
}

4. 代币交换

function swap(uint _amount0Out, uint _amount1Out, address _to) external {
require(_amount0Out > 0 || _amount1Out > 0, "Invalid output");
require(_amount0Out == 0 || _amount1Out == 0, "Only one output allowed");

uint _reserve0 = reserve0;
uint _reserve1 = reserve1;

require(_amount0Out < _reserve0 && _amount1Out < _reserve1, "Insufficient liquidity");

uint amount0In = 0;
uint amount1In = 0;

if (_amount0Out > 0) {
token0.transfer(_to, _amount0Out);
amount1In = token1.balanceOf(address(this)) - _reserve1;
} else {
token1.transfer(_to, _amount1Out);
amount0In = token0.balanceOf(address(this)) - _reserve0;
}

require(amount0In > 0 || amount1In > 0, "Invalid input");

// 恒定乘积公式验证
uint balance0 = token0.balanceOf(address(this));
uint balance1 = token1.balanceOf(address(this));

require(balance0 * balance1 >= _reserve0 * _reserve1, "K invariant violated");

_update(balance0, balance1);

emit Swap(msg.sender, amount0In, amount1In, _amount0Out, _amount1Out, _to);
}

5. 价格计算

function getAmountOut(uint _amountIn, uint _reserveIn, uint _reserveOut) public pure returns (uint amountOut) {
require(_amountIn > 0, "Invalid amount");
require(_reserveIn > 0 && _reserveOut > 0, "Invalid reserves");

// 简化版,实际应该考虑手续费
uint amountInWithFee = _amountIn * 997; // 0.3% 手续费
uint numerator = amountInWithFee * _reserveOut;
uint denominator = (_reserveIn * 1000) + amountInWithFee;
amountOut = numerator / denominator;
}

function getAmountsOut(uint _amountIn, address[] calldata _path) public view returns (uint[] memory amounts) {
require(_path.length >= 2, "Invalid path");
amounts = new uint[](_path.length);
amounts[0] = _amountIn;

for (uint i; i < _path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(_path[i], _path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}

高级功能

1. 价格预言机

contract PriceOracle {
struct Observation {
uint timestamp;
uint price0Cumulative;
uint price1Cumulative;
}

Observation[] public observations;
uint public constant PERIOD = 3600; // 1小时

function update() external {
uint timeElapsed = block.timestamp - observations[observations.length - 1].timestamp;
require(timeElapsed >= PERIOD, "Period not elapsed");

uint price0Cumulative = reserve1 * 2**112 / reserve0;
uint price1Cumulative = reserve0 * 2**112 / reserve1;

observations.push(Observation(block.timestamp, price0Cumulative, price1Cumulative));
}

function getPrice() external view returns (uint price) {
require(observations.length >= 2, "Insufficient data");

Observation memory first = observations[observations.length - 2];
Observation memory last = observations[observations.length - 1];

uint timeElapsed = last.timestamp - first.timestamp;
require(timeElapsed >= PERIOD, "Period too short");

price = (last.price0Cumulative - first.price0Cumulative) / timeElapsed;
}
}

2. 闪电贷

interface IFlashLoanReceiver {
function executeOperation(address token, uint amount, uint fee, bytes calldata data) external;
}

contract FlashLoan {
function flashLoan(address _token, uint _amount, bytes calldata _data) external {
uint balanceBefore = IERC20(_token).balanceOf(address(this));
require(balanceBefore >= _amount, "Insufficient liquidity");

// 转账给借款人
IERC20(_token).transfer(msg.sender, _amount);

// 执行借款人的操作
IFlashLoanReceiver(msg.sender).executeOperation(_token, _amount, _fee, _data);

// 检查还款
uint balanceAfter = IERC20(_token).balanceOf(address(this));
require(balanceAfter >= balanceBefore + _fee, "Flash loan not repaid");
}
}

部署和测试

使用Hardhat进行测试

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("SimpleDEX", function () {
let dex, token0, token1, owner, user1, user2;

beforeEach(async () => {
[owner, user1, user2] = await ethers.getSigners();

// 部署测试代币
const Token = await ethers.getContractFactory("Token");
token0 = await Token.deploy("Token0", "TK0", ethers.utils.parseEther("1000"));
token1 = await Token.deploy("Token1", "TK1", ethers.utils.parseEther("1000"));

// 部署DEX
const SimpleDEX = await ethers.getContractFactory("SimpleDEX");
dex = await SimpleDEX.deploy(token0.address, token1.address);

// 授权DEX使用代币
await token0.approve(dex.address, ethers.constants.MaxUint256);
await token1.approve(dex.address, ethers.constants.MaxUint256);
});

it("Should add liquidity", async () => {
const amount0 = ethers.utils.parseEther("100");
const amount1 = ethers.utils.parseEther("100");

await dex.addLiquidity(amount0, amount1);

expect(await dex.reserve0()).to.equal(amount0);
expect(await dex.reserve1()).to.equal(amount1);
expect(await dex.balanceOf(owner.address)).to.equal(ethers.utils.parseEther("100"));
});

it("Should perform swap", async () => {
// 添加流动性
await dex.addLiquidity(
ethers.utils.parseEther("100"),
ethers.utils.parseEther("100")
);

// 执行交换
const amountIn = ethers.utils.parseEther("10");
await token0.connect(user1).approve(dex.address, amountIn);
await token0.connect(user1).transfer(dex.address, amountIn);

const amountOut = await dex.getAmountOut(
amountIn,
await dex.reserve0(),
await dex.reserve1()
);

await dex.swap(0, amountOut, user1.address);

expect(await token1.balanceOf(user1.address)).to.equal(amountOut);
});
});

总结

通过构建这个DEX,我们学习了:

  1. 恒定乘积公式: x * y = k 是AMM的核心
  2. 流动性提供: 如何添加和移除流动性
  3. 代币交换: 基于定价算法的交换机制
  4. 价格计算: 滑点和价格影响的计算
  5. 高级功能: 价格预言机和闪电贷

这只是一个基础实现,实际的DEX如Uniswap还包含更多复杂功能:

  • 多跳路由
  • 价格影响保护
  • 闪电交换
  • 协议费用
  • 治理代币

继续探索,构建更强大的DeFi协议!

下一步

  1. 实现更复杂的AMM算法(如Curve的稳定币交换)
  2. 添加治理机制
  3. 集成跨链功能
  4. 构建用户界面
  5. 进行安全审计

参考资料

defisolidityuniswapdexsmart-contracts阅读需 5 分钟

在区块链世界中,智能合约的安全性至关重要。一个微小的漏洞可能导致数百万美元的损失。作为Web3开发者,我们必须将安全性放在首位。

常见的智能合约漏洞

1. 重入攻击 (Reentrancy Attack)

重入攻击是最著名的智能合约漏洞之一,The DAO事件就是典型案例。

// 不安全的代码
contract VulnerableBank {
mapping(address => uint) public balances;

function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
// 先转账,后更新状态 - 危险!
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount;
}
}

解决方案:使用检查-生效-交互 (Checks-Effects-Interactions) 模式

// 安全的代码
contract SecureBank {
mapping(address => uint) public balances;

function withdraw(uint amount) public {
// 检查
require(balances[msg.sender] >= amount, "Insufficient balance");

// 生效
balances[msg.sender] -= amount;

// 交互
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}

2. 整数溢出和下溢

在Solidity 0.8之前,整数溢出是常见的安全问题。

// 不安全的代码 (Solidity < 0.8)
contract VulnerableToken {
mapping(address => uint) public balances;

function transfer(address to, uint amount) public {
// 可能导致下溢
balances[msg.sender] -= amount;
balances[to] += amount;
}
}

解决方案:

  • 使用Solidity 0.8+(内置溢出检查)
  • 或使用OpenZeppelin的SafeMath库

3. 访问控制问题

确保只有授权用户能调用敏感函数。

contract SecureContract {
address public owner;

modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}

function criticalFunction() public onlyOwner {
// 只有owner能调用
}
}

安全开发最佳实践

1. 使用成熟的开发框架

  • OpenZeppelin Contracts: 提供经过审计的安全合约
  • Hardhat: 专业的开发环境
  • Foundry: 快速测试框架

2. 全面的测试策略

// 使用Foundry进行模糊测试
contract CounterTest is Test {
Counter public counter;

function setUp() public {
counter = new Counter();
}

function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1);
}

// 模糊测试
function testFuzzIncrement(uint256 x) public {
counter.setNumber(x);
counter.increment();
assertEq(counter.number(), x + 1);
}
}

3. 代码审计和静态分析

使用工具进行自动化安全检查:

  • Slither: 静态分析工具
  • MythX: 安全分析平台
  • Echidna: 属性测试工具

4. 遵循已验证的设计模式

代理模式 (Proxy Pattern)

contract Proxy {
address public implementation;

function upgradeTo(address newImplementation) public {
implementation = newImplementation;
}

fallback() external payable {
address impl = implementation;
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), impl, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}

拉取模式 (Pull Pattern)

contract PullPayment {
mapping(address => uint) public payments;

function withdrawPayment() public {
uint payment = payments[msg.sender];
require(payment > 0, "No payment");
payments[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: payment}("");
require(success);
}
}

总结

智能合约安全是一个持续学习的过程。记住这些核心原则:

  1. 简单性: 保持代码简单明了
  2. 可升级性: 考虑未来的升级需求
  3. 测试: 编写全面的测试用例
  4. 审计: 定期进行安全审计
  5. 监控: 部署后持续监控

安全不是一次性的事情,而是整个开发生命周期的持续过程。保持学习,保持警惕!

参考资料

soliditysecurityblockchainsmart-contracts阅读需 2 分钟