GoodGoodsMore
本项目是为了探索如何使用各种前后端技术和托管平台,以低成本/零成本建站的各种可能性。
关于技术选择的说明:
- 前端使用 Nuxt 3:
在这个项目中会使用 Nuxt 3 的 SSG 技术生产静态网页,以降低频繁地访问数据库造成的流量费用
静态网页文件会部署到 Vercel 平台,还可以结合 Cloudflare 所提供的 CDN 缓存服务,对网站的带宽进行优化 - 后端使用 Strapi:
它是基于 Nodejs 开发的后端应用,所以可以部署到任何提供 Nodejs 环境中,本项目就尝试过将后端部署在不同的(提供免费额度) Paas 平台,一开始使用的是 Heroku,之后尝试用 Railway,目前使用 Render,迁移成本并不大
除了提供了开箱的代码模板之外,它还内置提供了一个设计精美易用的后台页面,可以在开发模式下通过这个后台页面构建数据模式 schema,然后在生产环境下让运营者上传数据
另外它是一个 Headless CMS,前端通过 API 所获取的数据并不包含样式相关的信息,所以有很大的自由度来构建网页外观样式 - 数据库使用 PostgreSQL:
由于后端应用使用 Strapi 构建,所以数据库的选择受到限制(需要选择 Strapi 支持的数据库类型),在这个项目中选择 PostgreSQL(这是考虑到 Strapi 只支持关系型数据库 SQL,而 PostgreSQL 是流行的 SQL 开源版本)
有一些 Paas 平台除了提供 VM 虚拟主机(以运行 Nodejs 后端应用),还会同时提供数据库(以实现数据的持久化),例如 Heroku 和 Railway,也可以使用一些专门提供数据库托管的平台,例如 Supabase
在这个项目中选择 Neon 平台,它提供了免费额度的 Postgres 数据库,虽然平台比较新,但是它作为 Vercel Postgres 功能的供应商,值得尝试(相对而言目前比较有名的 Supabase 提供的免费额度的数据库容量比较小,而且有 7 天不活跃就暂停运行的限制) - 图片、视频使用 Backblaze:
主流的服务器供应商都有提供对象存储服务,此外还可以考虑 Cloudinary 和 Backblaze 等专门托管媒体文件的服务
在本项目使用 Backblaze 的 B2 Cloud Storage 服务,它提供 10GB 的免费存储额度,还可以配合 Cloudflare 所提供的 CDN 缓存对带宽进行优化(主要是针对 B2 Cloud Storage 的 Transactions Class B 类型 API 调用优化)
提示
Strapi 需要依赖 Nodejs 环境运行,所以要部署到服务器上,不少平台有提供免费计划,但是都有各种限制
可以查看 SonicJs 这个项目,它是一个依赖 Cloudflare 技术栈的 Headless CMS,可以运行在 Cloudflare Worker 上,而无需部署在服务器,速度很快,而且费用极低(免费额度很大)
Strapi 初始化
根据官方文档的步骤在终端输入以下命令,启动交互式命令行来创建一个 Strapi 应用
# strapi-app 是一个 npm 包,作为它来创建 Strapi 应用
# strapi-backend 是项目的名称
yarn create strapi-app strapi-backend
说明
注意这里的命令并不包含 --quickstart
参数,由于设置这个参数后,命令会自动创建一个默认的 Strapi 应用,它会使用 SQLite 作为数据库,但是在这个项目中需要使用的数据库是 Postgres
然后根据提示逐步选择合适的配置:
- 选择
Custom (manual settings)
通过手动定制的方式来创建项目 - 选择
JavaScript
以 JS 方式来创建项目(也可以选择Typescript
) - 选择
postgre
作为数据库 - 输入
Database name
数据库名称 - 按
Enter
键直接采用默认值127.0.0.1
作为数据库的托管服务器(地址),之后可以通过环境变量进行更改 - 按
Enter
键直接采用默认值5432
作为数据库的端口(这是 Postgres 数据库的通用端口,之后也可以通过环境变量进行更改) - 输入
Username
作为数据库的用户名称,之后可以通过环境变量进行更改 - 输入
Password
输入数据库的密码 Enable SSL connection
设置为y
以加密的方式连接数据库
提示
以上的一些配置是写入项目的 .env
文件中,通过修改相应的环境变量的值,可以对其进行修改
然后会自动下载项目的依赖包
网络问题
如果在下载依赖包时失败,可能是网络问题引起的,可以在项目的根目录下添加一个 .yarnrc
文件,对网络进行配置,使用国内的镜像源进行下载
registry "https://registry.npm.taobao.org"
sass_binary_site "https://npm.taobao.org/mirrors/node-sass/"
phantomjs_cdnurl "http://cnpmjs.org/downloads"
electron_mirror "https://npm.taobao.org/mirrors/electron/"
sqlite3_binary_host_mirror "https://foxgis.oss-cn-shanghai.aliyuncs.com/"
profiler_binary_host_mirror "https://npm.taobao.org/mirrors/node-inspector/"
chromedriver_cdnurl "https://cdn.npm.taobao.org/dist/chromedriver"
sharp_binary_host "https://npm.taobao.org/mirrors/sharp"
sharp_libvips_binary_host "https://npm.taobao.org/mirrors/sharp-libvips"
记得把该文件添加到 .gitignore
忽略跟踪,只将这些网络配置作为本地开发使用,不同步到线上部署
参考自这一篇文章
通过以上命令创建一个简单的 Strapi 应用,接着还需要进行一些配置,添加额外的功能,并优化开发体验
JWT
Strapi 支持多用户且可以为不同用户设置不同的使用权限
由于 Strapi 通过 JWT 进行用户权限校验,所以需要在 .env
文件中提供 JWT_SECRET
环境变量
可以在终端输入以下命令生成一个密钥
openssl rand -base64 32
然后将生成的字符串添加到 .env
文件中
JWT_SECRET=xxx
数据库
在前面初始化 Strapi 时选择 Postgres 作为数据库,但是实际上在本地开发环境中更推荐使用 SQLite(Strapi 在官方模板中选择 better-sqlite3
数据库 作为 SQLite 数据库),只在线上生产环境切换为 PostgreSQL 数据库,所以对于数据库还需要进行额外的配置
提示
虽然在本地环境也可以使用 PostgreSQL 作为数据库,但是这需要在本地系统中安装相应的软件并进行繁琐的配置
而 SQLite 只需要安装一个 npm 依赖包即可运行,它所提供的功能也足够满足开发调试的需求
SQLite 和 PostgreSQL 两者使用相同的语法,所以同一套代码逻辑可以通用,实现无缝切换
在前面初始化 Strapi 时已经对 Postgres 进行了安装(相关的依赖包)和配置,所以只需要对 SQLite 进行额外的配置即可
首先在终端输入以下命令安装依赖包
yarn add better-sqlite3 -D
说明
Strapi 在项目中的 config/database.js
文件对数据库进行配置,该文件的内容在初始化时自动生成的,里面列出了所有 Strapi 支持的数据库的配置方法,默认使用 SQLite 作为数据库,可以通过环境变量进行切换
以下列出了关于 SQLite 和 Postgres 的相关核心代码
import path from 'path';
export default ({ env }) => {
const client = env('DATABASE_CLIENT', 'sqlite');
const connections = {
//...
// 配置 postgres
postgres: {
connection: {
connectionString: env('DATABASE_URL'),
host: env('DATABASE_HOST', 'localhost'),
port: env.int('DATABASE_PORT', 5432),
database: env('DATABASE_NAME', 'strapi'),
user: env('DATABASE_USERNAME', 'strapi'),
password: env('DATABASE_PASSWORD', 'strapi'),
ssl: env.bool('DATABASE_SSL', false) && {
key: env('DATABASE_SSL_KEY', undefined),
cert: env('DATABASE_SSL_CERT', undefined),
ca: env('DATABASE_SSL_CA', undefined),
capath: env('DATABASE_SSL_CAPATH', undefined),
cipher: env('DATABASE_SSL_CIPHER', undefined),
rejectUnauthorized: env.bool(
'DATABASE_SSL_REJECT_UNAUTHORIZED',
true
),
},
schema: env('DATABASE_SCHEMA', 'public'),
},
pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
},
// 配置 sqlite
sqlite: {
connection: {
filename: path.join(
__dirname,
'..',
'..',
env('DATABASE_FILENAME', '.tmp/data.db')
),
},
useNullAsDefault: true,
},
};
return {
connection: {
client,
...connections[client],
acquireConnectionTimeout: env.int('DATABASE_CONNECTION_TIMEOUT', 60000),
},
};
}
从以上文件可知对于 Postgres 数据库,需要通过以下的环境变量进行配置:
DATABASE_URL
DATABASE_HOST
DATABASE_PORT
DATABASE_NAME
DATABASE_USERNAME
DATABASE_PASSWORD
DATABASE_SSL
DATABASE_SSL_CERT
DATABASE_SSL_CA
DATABASE_SSL_CAPATH
DATABASE_SSL_CIPHER
DATABASE_SSL_REJECT_UNAUTHORIZED
现在很多数据库平台都以 URL 形式的字符串来提供连接参数,它包括了数据库的服务器、端口、用户名、密码等参数
相应地,Strapi 在 v4.6.2(及之后的)版本提供了 connectionString
属性,支持直接通过 URL 字符串来对数据库进行配置,该属性有较高的优先级,它会覆盖其他的属性(如 host
和 port
等属性),如果将其设置为 ''
空字符串就会禁用它
对于 SQLite 数据库则比较简单,只需要一个环境变量 DATABASE_FILENAME
就可以进行配置
所以在配置数据库时,其实并不需要对出现在 config/database.js
文件中的所有环境变量都进行设置,因为有些环境变量有默认值,可以直接采用,有些配置属性是冗余的,只需要配置其中一项即可
对于 MySQL
、MariaDB
、PostgreSQL
数据库,最少需要配置的环境变量只有两个 DATABASE_CLIENT
和 DATABASE_URL
(如果数据库需要以 SSL 加密的方式连接,则还需要将环境变量 DATABASE_SSL
设置为 true
)
对于 SQLite
数据库,最少需要需要配置的环境变量只有两个 DATABASE_CLIENT
(其值设置为 sqlite
)和 DATABASE_FILENAME
(该环境变量也具有默认值 '.tmp/data.db'
也可以直接采用)
本地环境变量
本项目在本地开发环境下使用 SQLite 作为数据库,所以在本地的 Strapi 项目的根目录下的 .env
文件中,关于数据库的配置如下
# Local Database
DATABASE_CLIENT=sqlite
DATABASE_FILENAME=.tmp/data.db
对象存储
本项目使用 Backblaze 的 B2 Cloud Storage 对象存储服务,用来托管图片、视频等多媒体文件。
提示
Backblaze 更改了条款,在首次创建公开存储桶 Public Bucket 时,需要账号中有付费记录或绑定信用卡并完成支付一次性费用(1 美元)以进行验证;而创建私有存储桶 Private Bucket 则没有该限制。
但是访问私有存储需要身份验证,即无法直接访问,可以结合 Cloudflare Worker 通过应用程序密钥实现认证访问 Backblaze B2 私密桶,并利用 Cloudflare 的缓存功能,可以实现类似的网站图片托管服务。
参考:
创建 Bucket 和 Application Key
根据 Backblaze 官方文档创建一个 Bucket 存储桶,用作存储 goodgoodsmore 项目的多媒体文件
说明
在创建存储桶时,需要填写表单以提供相关信息,注意其中两个表单项
- 表单项
Bucket Unique Name
是为存储桶命名(需要是唯一的名称,即与已创建的存储桶名称都不同) - 表单项
Files in Bucket are
是设置该存储桶中文件的访问权限,要设置为public
,即文件可以公开访问,以便在前端展示给其他用户
其他的表单项是关于安全性的,可以使用默认值
创建完成后可以在页面找到该存储桶的卡片,展示了一些相关信息,可以点击卡片的 Bucket Settings
按钮,在弹出表单对以上的配置进行修改,也可以删除该存储桶。点击 Upload/Download
按钮,可以向存储桶上传文件,或下载存储桶中的文件
此外还需要申请一个 Application Keys
应用程序密钥,以便 Strapi 应用可以连接并操作这个 Bucket
创建成功后会在页面找到该应用程序密钥的卡片,⚠️ 注意其中 applicationKey
密钥只会展示一次(刷新页面后会隐藏)所以要复制保持到本地(需要提供给 Strapi 应用)
提示
如果忘记了某个应用程序密钥,可以将它删除,新建一个新的
应用程序密钥的卡片列出了一些相关信息:
keyId
该密钥的 ID 号keyName
该密钥的名称/键名buckName
该密钥可以访问的 Buckets 存储桶capabilities
该密钥的权限expiration
该密钥的有效期限namePrefix
该密钥可以访问的文件带有哪个前缀
Upload Provider 插件
同时需要在 Strapi 项目里安装文件上传 upload provider 的相关插件,在终端输入以下命令安装
yarn add @strapi/provider-upload-aws-s3
说明
虽然 @strapi/provider-upload-aws-s3
插件 是针对 aws-s3 服务开发的,但是也适用于 Backblaze
因为 Backblaze 提供了兼容 S3 的 API,即可以通过同一套 API 语法来使用 Backblaze
然后进行相应的配置(具体参考自这一篇帖子和该插件的说明文档)
- 在 Strapi 项目中创建
config/plugins.js
文件,以设置该插件config/plugins.jsjsmodule.exports = ({ env }) => ({ // 对于文件上传功能进行配置 upload: { config: { // 所使用的 provider 插件(名称) // 记得安装相应的依赖包 @strapi/provider-upload-aws-s3 provider: 'aws-s3', // 对插件进行配置 providerOptions: { accessKeyId: env('B2_ID'), secretAccessKey: env('B2_KEY'), region: env('B2_REGION'), params: { Bucket: env('B2_BUCKET'), }, endpoint: env('B2_ENDPOINT') }, }, }, });
说明
一般在项目中通过环境变量的形式来提供 key 密钥,以便在开发环境和生产环境使用不同的配置
其中一些环境变量的值需要从 Backblaze 中获取,具体可以查看上一节中所创建的存储桶 Bucket 和应用程序密钥 Application key 的卡片
- 环境变量
B2_ID
用于设置应用程序密钥的 ID,就是指 Application key 卡片中的keyID
- 环境变量
B2_KEY
用于设置应用程序密钥,就是在创建完成后仅显示一次的applicationKey
- 环境变量
B2_REGION
用于设置存储桶所使用的服务器所在的地区,可以从 Bucket 卡片中的Endpoint
存储服务器终端地址中获取,它是在s3
后面的那一串字符,形式如us-west-004
- 环境变量
B2_BUCKET
用于设置存储桶的名称,就是指 Bucket 卡片中的名称 - 环境变量
B2_ENDPOINT
用于设置存储桶的(终端)地址,就是指 Bucket 卡片中的Endpoint
注意
由于新版的 Strapi upload provider 将依赖包 AWS SDK 从 V2 版本升级到 V3 版本,所以
B2_ENDPOINT
的格式需要作出相应的变更,要在 Backblaze 存储桶 Bucket 卡片的Endpoint
值前面加上https://
协议前缀更多介绍参考相关文档:
- 环境变量
- 更改
config/middlewares.js
文件中关于内容安全政策中间件的设置,以便在 Strapi 的 Media Library 媒体库中查看到媒体文件(图片)的缩略图,使用以下对象替代原有的'strapi::security'
元素config/middlewares.jsjsmodule.exports = [ // ... { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', // 根据 Backblaze 的相关信息来设置 // yourBucketName 是指存储桶 Bucket 的名称 // 后面的部分和环境变量 B2_ENDPOINT 的值一致 'yourBucketName.s3.yourRegion.backblazeb2.com', ], 'media-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', // 根据 Backblaze 的相关信息来设置 // yourBucketName 是指存储桶 Bucket 的名称 // 后面的部分和环境变量 B2_ENDPOINT 的值一致 'yourBucketName.s3.yourRegion.backblazeb2.com', ], upgradeInsecureRequests: null, }, }, }, }, // ... ];
Cloudflare 配置
通过在 CLoudflare 的设置可以实现缓存,并修改前端图片的 URL 格式
设置 URL 规则
参考
Backblaze 的免费套餐提供了 10GB 的存储容量,以及一些 API 调用额度
其中 Transactions Class B 的 API 调用(即调用该 API 会触发数据库执行 B 类型事务)免费额度是每天 2500 次,其中包括文件下载操作,且免费额度限制在每天 1GB 的下载流量,当网站的流量较大时,用户在前端浏览/加载图片时会下载图片文件,这个免费额度可能不够
可以利用 Cloudflare 所提供利用 CDN 缓存来解决这个流量限制问题
首先需要查看托管在 Backblaze 平台的图片的原始 URL,可以点开 Bucket 存储桶里的任何一个文件,查看它的相关信息
存储在 Backblaze(公开类型的存储桶 public bucket)的文件会有 3 种类型的 URL:
- Friendly URL 友好、易读的链接
- S3 URL 包含兼容性 S3 信息的链接
- Native URL 包含 B2 原生 API 信息的链接
以上 3 种类型的 URL 都可以访问到同一个文件,更推荐使用 Friendly URL,它更简短易记
说明
通过观察可以发现 Friendly URL 的命名规则是 https://f00<x>.backblazeb2.com/file/<bucket_name>/<img_name>
f00<x>.backblazeb2.com
是域名 hostname,其中<x>
是数字<bucket_name>
是该文件所在的存储桶<img_name>
是文件的名称
注意
以上 3 种链接在浏览器直接打开都可以加载展示相应图片
但是如果需要在其他域名下通过代理/映射的方式进行访问,则有所不同
S3 URL 并不能以其他域名(并且仅通过代理)获取图片,需要在请求中签名(在请求头配置 Backblaze 应用程序密钥)才可以跨域访问,这和 AWS S3 的协议相关
而 Friendly URL 和 Native URL 则能够以其他域名(且仅通过代理)获取到图片
虽然可以直接在前端采用上述的链接来获取图片,但是这会暴露了 Backblaze 相关信息(例如存储桶)
为了避免这种情况,可以在前端使用特定规则来的 URL(例如只包含图片的名称)来代替原有的图片链接,然后利用 Cloudflare 代理功能,当前端向服务器发送请求图片时,将相应的请求链接转向/修改为对应的图片原始 URL
- 首先进行域名的代理/映射
通过观察可以知道在同一个存储桶 Bucket 种的图片的 Friendly URL 都有相同的域名 hostname,其形式是f00<x>.backblazeb2.com
是域名 hostname(其中<x>
是数字),这个域名可以通过 Cloudflare 代理,这样在前端使用其他的域名来替代
可以一个子域名,例如assets.goodgoodsmore
(该项目的前端网站域名是goodgoodsmore.com
),专门用来表示要获取多媒体文件
在 Cloudflare 的 DNS 中添加一条相应的CNAME
配置子域名代理 注意
由于 Cloudflare 默认通过 HTTP 访问目标服务器,所以开启代理后可能会造成网站无法访问,这时需要将 SSL/TLS 加密模式设置为「完全(严格)」,这样就可以通过 HTTPS 与目标服务器连接
加密模式 如果不将其设置为「完全(严格)」模式,则浏览网页时会出现「重定向次数过多」的错误
那么当前端通过链接获取图片时,如https://assets.goodgoodsmore/file/<bucket_name>/img.png
,经过 Cloudflare 的(子)域名代理,实际访问的是https://f00<x>.backblazeb2.com/file/<bucket_name>/img.png
- 然后对链接的规则进行转变
前面通过设置 Cloudflare 的 DNS 规则可以实现域名的代理,但是替换的图片链接后半部分依然要和 Friendly URL 后半部分一致才行。而 URL 后半部分正是包含存储桶名称的敏感信息,所以最好将后半部分的 URL 也替换掉
可以通过 Cloudflare Transform Rules 转换规则进行处理,使用其中的 Rewrite URL 重写链接功能,对于从前端发出来的请求的 URL(例如只包含上述域名和图片文件)进行改写,将其重写(补充一些固定的路径)为符合 Friendly URL 规则,再向目标服务器发送说明
请求流量到达源服务器之前会经过 Cloudflare 一系列处理,其顺序如下
Cloudflare 处理顺序 其中 URL 重写位于较前的步骤
以下图示是具体的配置操作配置 Cloudflare 转换规则
通过以上在 Cloudflare 的配置,可以在前端使用 https://assets.goodgoodsmore.com/img.png
的形式加载保存在 Backblaze 的图片,其原链接形式是 https://f00<x>.backblazeb2.com/file/<bucket_name>/img.png
设置页面缓存
对于变更不频繁的内容,可以通过设置 Cloudflare 的 Page Rule 页面规则来优化缓存等级
而多媒体资源(如图片)一般是不会变的,所以可以对于这一类的 URL 设置较高的缓存等级
而对于整个网站则可以设置较短的缓存
注意
Cloudflare 的免费用户可以为一个域名设置 3 个页面规则,由于页面(URL)匹配是有顺序的(从上到下),且每个 URL 仅触发一个页面规则,所以整个网站的页面缓存规则应该放到最下面,而专门针对(多媒体文件相关)子域名的页面规则则应该先设置
迁移提示
Cloudflare 「规则 -> 页面规则」配置已经被弃用,需要使用「缓存 -> Cache Rules」进行缓存配置
数据模式
完成前面的设置,并将 Strapi 部署到 Render 平台后,后端应用就可以正常运行了
首先需要到 Strapi admin 页面(路径是 https://hostname/admin
)进行管理员注册,然后就可以为运营者注册账号,管理员可以为这些用户设置使用权限
提示
Strapi 为内容创作者/运营者提供了使用指南,可以参考官方文档
内容运营者在左侧菜单栏选择 Content Manager
就可以进行内容创作/输入
提示
通过 Strapi 输入的内容分为两类,一种输入的文本,另一种是上传的多媒体文件,它们实际上分别存储在不同的平台:
- 文本数据会保存到 Postgres 数据库中(目前使用 Neon 平台)
- 多媒体文件(如图片)会保存到 Backblaze 的 B2 Cloud Storage 云存储中
由于 Strapi 是 Headless CMS 它只包含内容数据(不包含页面的样式),前端需要通过 API 获取后端的数据再和渲染为自定义样式的页面。为了确保前后端以及内容运营者的协同工作,需要对数据模式 Schema 进行严格的限制,结构化的数据才可以在前后端顺利对接,例如规定一个商品使用哪些字段/属性进行描述
数据模式 Schema 在左侧菜单栏选择 Content-Type Builder
里进行创建,但是仅在开发环境 develop 才可以进行设置,所以需要先在本地运行 Strapi 应用创建完成需要使用的数据模式后,再将项目的更新 Push 到远端的服务器进行重新部署,这样内容运营者就可以在 Content Manager
里看到相应的数据类型以供选择
具体流程可以参考官方文档
提示
使用 Content-Type Builder
是以可视化的方式创建 Schema,实际上 Strapi 会自动在项目里创建一些相应的 *.json
、*.js
等文件
如果需要在项目之间迁移/共享数据模式时,可以直接复制这些 *.json
、*.js
等文件
Cloudflare 代理
前端部署到 Vercel 平台,并使用 Cloudflare 进行反向代理,利用其 CDN 缓存以节省带宽流量(包括访问托管在 Vercel 的前端静态页面,以及托管在 Backblaze 的多媒体文件)
为了方便操作,推荐直接在 Cloudflare 购买域名,这样就可以在设置 DNS 解析的同时一键开启代理
注意
由于 Cloudflare 默认通过 HTTP 访问目标服务器,所以开启代理后可能会造成网站无法访问,这时需要将 SSL/TLS 加密模式设置为「完全(严格)」,这样就可以通过 HTTPS 与目标服务器连接
如果不将其设置为「完全(严格)」模式,则浏览网页时会出现「重定向次数过多」的错误