前言
相信不少IT卷友都研究过如何部署在线API,如何白piao云数据库服务,仔细阅读本文,你将有惊人发现!本文重点讲述如何利用Golang/NodeJS、Vercel提供的免费资源托管服务和MongoDb Cloud提供的免费版数据服务实现一个在线版CRUD接口服务,希望对各位卷友有所帮助!
正文
1 准备工作
(1)vercel账号申请,请移步官方连接,完成注册之后可以新建项目备用(温馨提示:有需要托管静态资源比如博客、图片加速的小伙伴可以使用vercel,有100G免费流量可用,国内资源托管需要备案或者要手持身份证明如Gitee Pages)
(2)MongoDB Cloud Atlas注册,前往注册链接完成注册,完成注册之后登录到数据管理平台,基本按照新建组织->新建项目->新建集群数据库的傻瓜式操作进行,需要注意的是,新建数据库集群类型需要选择共享集群(只有这个是免费的),如下所示:
(3)新建数据库,选中进入创建的数据库集群,新建数据库,完成之后记得不要导入示例数据,点击"Browser Collections"进入数据浏览界面,新建数据库和Collection,手动添加数据(还是自己添加的放心,导入的分分钟几百兆空间没了, 免费的数据库大小仅512M ,这点请注意!!!)
(4)集成Vercel和MongoDB,可以参照官方傻瓜式文档
(5)编写Serverless Funtion,可以参考官方文档了解一下Golang和Nodejs基本流程,以便理解文章后续部分内容
2 Serverless CRUD服务实现步骤
2.1 Golang版
首先,新建项目文件夹,并参照MongoDB官方文档完成依赖安装工作,浏览CRUD部分并思考如何作适配,详见本节第5部分。
(1)项目根目录新建api目录,新建db.go编写Handler函数,本文项目结构如下:
(2)定义响应数据格式
// utils/http.go
type Response struct {
Data any `json:"data"`
Error any `json:"error"`
}
func GetResponse(data Response) string {
rst, err := json.Marshal(data)
if err != nil {
panic(err)
}
return string(rst)
}
(3) 跨域设置 再db.go中的Handler函数中统一处理
// api/db.go
func Handler(w http.ResponseWriter, r *http.Request) {
...
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
...
}
(4)controller层统一异常处理
// api/db.go
func Handler(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, utils.GetResponse(utils.Response{
Data: nil,
Error: err,
}))
}
}()
...
}
(5) 适配处理 为了统一适配处理,并考虑请求CRUD接口规范性,本文分别使用POST、DELETE、PUT和GET完成CRUD操作,MongoDB的查询条件使用url传入,新增和修改数据则通过Body传入,
#1 查询条件转换规则:
">=" -> "$gte",
"<=" -> "$lte",
"!=" -> "$ne",
"==" -> "$eq",
"!@" -> "$nin",
">" -> "$gt",
"<" -> "$lt",
"@" -> "$in",
"and : []" -> "$and: bson.A{}"
"or : []" -> "$or: bson.A{}"
如"{\"and\": [\"age==15\"]}" 将转换为
bson.D{
{"$and",
bson.A{
bson.D{{"age",
bson.D{{"$eq", 15}},
}},
},
},
}
核心代码:
// utils/db.go
type MogoCriteria struct { // 查询参数数据格式
And []string `json:"and"`
Or []string `json:"or"`
}
func getCriteriaVal(val string) any { // 解析条件值,支持数字、字符传、布尔以及切片
if val == "true" {
return true
}
if val == "false" {
return false
}
switch val[0] {
case '"':
{
return strings.Trim(val, "\"")
}
case '[':
{
strs := strings.Split(val[1:len(val)-1], ",")
rst := make([]any, len(strs))
for i, val := range strs {
if val == "true" {
rst[i] = true
} else if val == "false" {
rst[i] = false
} else if val[0] == '"' {
rst[i] = strings.Trim(val, "\"")
} else {
fVal, err := strconv.ParseFloat(val, 64)
if err != nil {
panic(err)
}
rst[i] = fVal
}
}
return rst
}
default:
{
fVal, err := strconv.ParseFloat(val, 64)
if err != nil {
panic(err)
}
return fVal
}
}
}
var symbolMap = map[string]string{ //转换映射结构体
">=": "$gte",
"<=": "$lte",
"!=": "$ne",
"==": "$eq",
"!@": "$nin",
">": "$gt",
"<": "$lt",
"@": "$in",
}
func getBsonOper(oper string, localSym string) bson.D {
bsonSym := symbolMap[localSym]
b, a, _ := strings.Cut(oper, localSym)
if a != "" {
return bson.D{{
Key: b,
Value: bson.D{{
Key: bsonSym,
Value: getCriteriaVal(a),
}}}}
} else {
return bson.D{}
}
}
func parseLogicOper(oper string) bson.D {
switch {
case strings.Contains(oper, ">="):
return getBsonOper(oper, ">=")
case strings.Contains(oper, "<="):
return getBsonOper(oper, "<=")
case strings.Contains(oper, "!="):
return getBsonOper(oper, "!=")
case strings.Contains(oper, "=="):
return getBsonOper(oper, "==")
case strings.Contains(oper, "!@"):
return getBsonOper(oper, "!@")
case strings.Contains(oper, ">"):
return getBsonOper(oper, ">")
case strings.Contains(oper, "<"):
return getBsonOper(oper, "<")
case strings.Contains(oper, "@"):
return getBsonOper(oper, "@")
default:
panic("不支持的查询条件符号,限于>=、<=、!=、==、!@、@、>、<")
}
}
func ParseCriteria(query string) any {
if query == "" {
return bson.D{}
}
var criteria MogoCriteria
if err := json.Unmarshal([]byte(query), &criteria); err != nil {
panic(err)
}
rst := bson.D{}
if len(criteria.And) > 0 {
var arr bson.A = make(bson.A, len(criteria.And))
for i, val := range criteria.And {
arr[i] = parseLogicOper(val)
}
rst = append(rst, bson.E{Key: "$and", Value: arr})
}
if len(criteria.Or) > 0 {
var arr bson.A = make(bson.A, len(criteria.Or))
for i, val := range criteria.Or {
arr[i] = parseLogicOper(val)
}
rst = append(rst, bson.E{Key: "$or", Value: arr})
}
return rst
}
#2 请求体转换规则:
// utils/http.go
func map2BsonD(m map[string]any) bson.D { // map到bson.D转换
var rst bson.D
for k, v := range m {
rst = append(rst, bson.E{
Key: k,
Value: v,
})
}
return rst
}
func ParseBodyArr(reader io.ReadCloser) []any { //用于新增,新增仅支持对象数组
var rst []map[string]any
defer reader.Close()
res, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)
}
err = json.Unmarshal(res, &rst)
if err != nil {
panic(err)
}
if kind := reflect.TypeOf(rst).Kind(); kind == reflect.Slice {
var docs []any
for _, v := range rst {
docs = append(docs, map2BsonD(v))
}
return docs
} else {
panic("请求体解析失败,限于数组类型")
}
}
func ParseBodyObj(reader io.ReadCloser) bson.D { // 用于编辑
var rst map[string]any
defer reader.Close()
res, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)
}
err = json.Unmarshal(res, &rst)
if err != nil {
panic(err)
}
if kind := reflect.TypeOf(rst).Kind(); kind == reflect.Map {
return bson.D{{
Key: "$set",
Value: map2BsonD(rst),
}}
} else {
panic("请求内容解析失败,限于映射类型")
}
}
2.2 NodeJS版
NodeJS版的实现要比Golang简洁的多,不用定义条件转换规则,利用序列化可反序列化可以有效地传输数据库参数和选项。而且,NodeJS版支持Path Segments,Path Segments文档内容详见链接,Golang则不支持这种做法。NodeJS版的项目结构如下:
NodeJS版实现思路和Golang版雷同,感兴趣的卷友请自行源码查看!
3 源码贡献
4 国际惯例
欢迎访问原文链接