热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

中查询一个文件夹下文件数量_gomicroV2从零开始(二)手写第一个微服务

本文相关代码:gitee前言上一章我们借助micro工具包创建了一个demo程序hello-service,并通过编写hello-cli.go成功实现了
5728f8c30fbaea884e1de6a24d80c4f6.png

本文相关代码:gitee


前言

上一章我们借助micro工具包创建了一个demo程序hello-service,并通过编写hello-cli.go成功实现了微服务调用。

接下来的若干章我们参考示例代码,编写一组自己的微服务,并逐步引入第三方插件,最终编写一个微服务版的todolist程序.

这一章,我们先来手写第一个微服务:task-srv

作为一个todolist服务,最基础的功能当然是对任务的增删改查等管理操作,考虑到实际业务复杂度,这里我们选择引入mongodb作为系统数据库保存task信息


具体步骤

一、创建目录

首先创建项目总的目录,这里我们起名为go-todolist

mkdir go-todolist && cd go-todolist
# 初始化go-todolist为go mod项目
go mod init go-todolist# 继续创建`task-srv`目录
mkdir task-srv && cd task-srv# 这部分可以参考上一章的官方demo程序
# 注意这里多出一个repository文件夹用于数据库相关操作
mkdir proto repository handler
cd proto
mkdir task

此服务只提供gRpc一种调用方式,不订阅消息,因此会发现我们并没有创建subscriber文件夹。


二、编写task.proto

新建并编辑task-srv/proto/task/task.proto,这里我们首先定义了task对象的增、删、改和分页查询接口。

需要注意的是,消息体定义使用了限定修饰符,限定修饰符包含requiredoptionalrepeated

  • Required: 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字 段的值,对于接收方,必须能够识别该字段的意思。 ~~这个修饰符在proto3中已废弃~~
  • Optional: 表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。 对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。 这个修饰符在proto3中是所有字段的默认修饰符
  • Repeated: 表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值。

//声明proto本版
syntax = "proto3";
//服务名
package go.micro.service.task;
//生成go文件的包路径
option go_package = "proto/task";//定义task服务的接口,主要是增删改查
//结构非常类似于go语言的interface定义,只是返回值必须用括号包裹,且不能使用基本类型作为参数或返回值
service TaskService {rpc Create(Task)returns (EditResponse){}rpc Delete(Task)returns (EditResponse){}rpc Modify(Task)returns (EditResponse){}rpc Finished(Task)returns (EditResponse){}rpc Search(SearchRequest)returns (SearchResponse){}
}//下面是消息体message的定义,可以暂时理解为go中的struct,其中的1,2,3...是每个变量唯一的编码
message Task {//每条任务的ID,本项目中对应mongodb记录的"_id"字段//@inject_tag: bson:"_id"string id = 1;//任务主体文字//@inject_tag: bson:"body"string body = 2;//用户设定的任务开始时间戳//@inject_tag: bson:"startTime"int64 startTime = 3;//用户设定的任务截止时间戳//@inject_tag: bson:"endTime"int64 endTime = 4;//任务是否已完成//@inject_tag: bson:"isFinished"int32 isFinished = 5;//用户实际完成时间戳//@inject_tag: bson:"finishTime"int64 finishTime = 6;//任务创建时间//@inject_tag: bson:"createTime"int64 createTime = 7;//任务修改时间//@inject_tag: bson:"updateTime"int64 updateTime = 8;
}//增删改接口返回参数
message EditResponse {//操作返回的消息string msg = 1;
}
//查询接口的参数
message SearchRequest{//分页查询页码,从第一页开始int64 pageSize = 1;//分页查询每页数量,默认20int64 pageCode = 2;// 排序字段string sortBy = 3;// 顺序 -1降序 1升序int32 order=4;//关键字模糊查询任务body字段string keyword = 5;
}message SearchResponse{//分页查询页码,从第一页开始int64 pageSize = 1;//分页查询每页数量,默认20int64 pageCode = 2;// 排序字段string sortBy = 3;// 顺序 -1降序 1升序int32 order=4;//数据总数int64 total = 5;//具体数据,这里repeated表示可以出现多条,类似于go中的slicerepeated Task rows = 6;
}

细心的朋友会发现,在proto文件中,除了必要的注释外还有一些类似java注解的注释@inject_tag,这是由于protobuf官方的插件不能很好的处理go struct的tag内容,而我们的数据库mongodb又需要在tag中配置bson信息,否则生成的struct无法正确接收_id,另外生成的表字段也会被全部小写不符合阅读习惯。 这里需要引入一个第三方的自动生成工具:

go get -u github.com/favadi/protoc-go-inject-tag

他的配置很简单,只需要在需要设置tag的字段上写类似//@inject_tag bson:"_id"这样的注解即可。 当然,要使用这个工具,也需要调用额外的命令,其中input参数的值是生成后go文件的相对路径。

protoc-go-inject-tag -input=proto/task/task.pb.go

为了便于使用,这里我们参考上一章demo中的makefile文件,在task目录下也创建可以makefile文件:

GOPATH:=$(shell go env GOPATH)
MODIFY=Mproto/imports/api.proto=github.com/micro/go-micro/v2/api/proto.PHONY: proto
proto:protoc --proto_path=. --micro_out=${MODIFY}:. --go_out=${MODIFY}:. proto/task/task.proto# 注意这里我们添加了tag控件的命令protoc-go-inject-tag -input=proto/task/task.pb.go.PHONY: build
build: protogo build -o task-srv main.go.PHONY: test
test:go test -v ./... -cover.PHONY: docker
docker:docker build . -t task-srv:latest

接下来,在task-srv文件夹下运行make proto即可以生成proto的go文件。

没有make命令的win用户也可以在task-srv目录下直接执行命令:

> protoc --proto_path=. --micro_out=. --go_out=. proto/task/task.proto
> protoc-go-inject-tag -input=proto/task/task.pb.go

这个时候你的两个go文件一定是满屏飘红的,因为缺少依赖,进入task-srv目录,执行go mod tidy下载基础依赖,因为之前demo项目已经下载过相关依赖,这个命令执行会很快。


三、实现服务

用编辑器打开task.pb.micro.go,我们首先能看到,protoc已经帮我生成了go语言版的TaskService服务相关代码,根据注释,我们需要实现接口TaskServiceHandler:

...// Server API for TaskService servicetype TaskService interface {Create(ctx context.Context, in *Task, opts ...client.CallOption) (*EditResponse, error)Delete(ctx context.Context, in *Task, opts ...client.CallOption) (*EditResponse, error)Modify(ctx context.Context, in *Task, opts ...client.CallOption) (*EditResponse, error)Finished(ctx context.Context, in *Task, opts ...client.CallOption) (*EditResponse, error)Search(ctx context.Context, in *SearchRequest, opts ...client.CallOption) (*SearchResponse, error)
}...

你会注意到,除了我们自己定义的 *Task参数外,go-micro还自动封装了上下文context.Context,原本我们定义的返回值EditResponse变成了一个指针参数。 接下来我们就要实现这四个接口,我习惯的开发顺序是 数据库操作->业务实现->注册服务。


3.1 数据库操作

新建并编辑task-srv/repository/task.go

package repositoryimport ("context""github.com/pkg/errors"pb "go-todolist/task-srv/proto/task""go.mongodb.org/mongo-driver/bson""go.mongodb.org/mongo-driver/bson/primitive""go.mongodb.org/mongo-driver/mongo""go.mongodb.org/mongo-driver/mongo/options""log""strings""time"
)const (// 默认数据库名DbName = "todolist"// 默认表名TaskCollection = "task"UnFinished = 0Finished = 1
)// 定义数据库基本的增删改查操作
type TaskRepository interface {InsertOne(ctx context.Context, task *pb.Task) errorDelete(ctx context.Context, id string) errorModify(ctx context.Context, task *pb.Task) errorFinished(ctx context.Context, task *pb.Task) errorCount(ctx context.Context, keyword string) (int64, error)Search(ctx context.Context, req *pb.SearchRequest) ([]*pb.Task, error)
}// 数据库操作实现类
type TaskRepositoryImpl struct {// 需要注入一个数据库连接客户端Conn *mongo.Client
}// 定义默认的操作表
func (repo *TaskRepositoryImpl) collection() *mongo.Collection {return repo.Conn.Database(DbName).Collection(TaskCollection)
}func (repo *TaskRepositoryImpl) InsertOne(ctx context.Context, task *pb.Task) error {_, err := repo.collection().InsertOne(ctx, bson.M{"body": task.Body,"startTime": task.StartTime,"endTime": task.EndTime,"isFinished": UnFinished,"createTime": time.Now().Unix(),})return err
}func (repo *TaskRepositoryImpl) Delete(ctx context.Context, id string) error {oid, err := primitive.ObjectIDFromHex(id)if err != nil {return err}_, err = repo.collection().DeleteOne(ctx, bson.M{"_id": oid})return err
}func (repo *TaskRepositoryImpl) Modify(ctx context.Context, task *pb.Task) error {id, err := primitive.ObjectIDFromHex(task.Id)if err != nil {return err}_, err = repo.collection().UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": bson.M{"body": task.Body,"startTime": task.StartTime,"endTime": task.EndTime,"updateTime": time.Now().Unix(),}})return err
}
func (repo *TaskRepositoryImpl) Finished(ctx context.Context, task *pb.Task) error {id, err := primitive.ObjectIDFromHex(task.Id)if err != nil {return err}now := time.Now().Unix()update := bson.M{"isFinished": int32(task.IsFinished),"updateTime": now,}if task.IsFinished == Finished {update["finishTime"] = now}log.Print(task)log.Println(update)_, err = repo.collection().UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": update})return err
}func (repo *TaskRepositoryImpl) Count(ctx context.Context, keyword string) (int64, error) {filter := bson.M{}if keyword != "" && strings.TrimSpace(keyword) != "" {filter = bson.M{"body": bson.M{"$regex": keyword}}}count, err := repo.collection().CountDocuments(ctx, filter)return count, err
}func (repo *TaskRepositoryImpl) Search(ctx context.Context, req *pb.SearchRequest) ([]*pb.Task, error) {filter := bson.M{}if req.Keyword != "" && strings.TrimSpace(req.Keyword) != "" {filter = bson.M{"body": bson.M{"$regex": req.Keyword}}}cursor, err := repo.collection().Find(ctx,filter,options.Find().SetSkip((req.PageCode-1)*req.PageSize),options.Find().SetLimit(req.PageSize),options.Find().SetSort(bson.M{req.SortBy: req.Order}))if err != nil {return nil, errors.WithMessage(err, "search mongo")}var rows []*pb.Taskif err := cursor.All(ctx, &rows); err != nil {return nil, errors.WithMessage(err, "parse data")}return rows, nil
}


3.2 业务实现

新建并编辑task-srv/handlertask.go这里注意,真正的返回值是通过操作resp参数返回的:

package handlerimport ("context""github.com/pkg/errors"pb "go-todolist/task-srv/proto/task""go-todolist/task-srv/repository"
)// 要实现接口,首先当然要定义一个结构体
type TaskHandler struct {TaskRepository repository.TaskRepository
}func (t *TaskHandler) Create(ctx context.Context, req *pb.Task, resp *pb.EditResponse) error {if req.Body &#61;&#61; "" || req.StartTime <&#61; 0 || req.EndTime <&#61; 0 {return errors.New("bad param")}if err :&#61; t.TaskRepository.InsertOne(ctx, req); err !&#61; nil {return err}resp.Msg &#61; "success"return nil
}
func (t *TaskHandler) Delete(ctx context.Context, req *pb.Task, resp *pb.EditResponse) error {if req.Id &#61;&#61; "" {return errors.New("bad param")}if err :&#61; t.TaskRepository.Delete(ctx, req.Id); err !&#61; nil {return err}resp.Msg &#61; req.Idreturn nil
}
func (t *TaskHandler) Modify(ctx context.Context, req *pb.Task, resp *pb.EditResponse) error {if req.Id &#61;&#61; "" || req.Body &#61;&#61; "" || req.StartTime <&#61; 0 || req.EndTime <&#61; 0 {return errors.New("bad param")}if err :&#61; t.TaskRepository.Modify(ctx, req); err !&#61; nil {return err}resp.Msg &#61; "success"return nil
}
func (t *TaskHandler) Finished(ctx context.Context, req *pb.Task, resp *pb.EditResponse) error {if req.Id &#61;&#61; "" || req.IsFinished !&#61; repository.UnFinished && req.IsFinished !&#61; repository.Finished {return errors.New("bad param")}if err :&#61; t.TaskRepository.Finished(ctx, req); err !&#61; nil {return err}resp.Msg &#61; "success"return nil
}
func (t *TaskHandler) Search(ctx context.Context, req *pb.SearchRequest, resp *pb.SearchResponse) error {count, err :&#61; t.TaskRepository.Count(ctx, req.Keyword)if err !&#61; nil {return errors.WithMessage(err, "count row number")}if req.PageCode <&#61; 0 {req.PageCode &#61; 1}if req.PageSize <&#61; 0 {req.PageSize &#61; 20}if req.SortBy &#61;&#61; "" {req.SortBy &#61; "createTime"}if req.Order &#61;&#61; 0 {req.Order &#61; -1}if req.PageSize*(req.PageCode-1) > count {return errors.New("There&#39;s not that much data")}rows, err :&#61; t.TaskRepository.Search(ctx, req)if err !&#61; nil {return errors.WithMessage(err, "search data")}*resp &#61; pb.SearchResponse{PageCode: req.PageCode,PageSize: req.PageSize,SortBy: req.SortBy,Order: req.Order,Rows: rows,}return nil
}


3.3 注册服务

最后的最后&#xff0c;我们参考上一章的main.go&#xff0c;注册我们的服务。 这里需要说明的是&#xff0c;在v2版本中go-micro默认使用mdns作为服务发现&#xff0c;他无需安装部署主要用于学习和简单场景开发&#xff0c;生产环境官方建议使用etcd&#xff0c;这个我们在后面的插件部分再套路。 另外&#xff0c;由于本服务暂时没有需要处理的消息&#xff0c;我们删除了消息相关接口。 最后&#xff0c;我们配置了mongo连接作为数据库&#xff08;关于mongo的搭建请自行搜索&#xff09;&#xff1a;

package mainimport ("context""github.com/micro/go-micro/v2""github.com/pkg/errors""go-todolist/task-srv/handler"pb "go-todolist/task-srv/proto/task""go-todolist/task-srv/repository""go.mongodb.org/mongo-driver/mongo""go.mongodb.org/mongo-driver/mongo/options""log""time"
)// 这里是我内网的mongo地址&#xff0c;请根据你得实际情况配置&#xff0c;推荐使用dockers部署
const MONGO_URI &#61; "mongodb://172.18.0.58:27017"func main() {// 在日志中打印文件路径&#xff0c;便于调试代码log.SetFlags(log.Llongfile)conn, err :&#61; connectMongo(MONGO_URI, time.Second)if err !&#61; nil {log.Fatal(err)}defer conn.Disconnect(context.Background())// New Serviceservice :&#61; micro.NewService(micro.Name("go.micro.service.task"),micro.Version("latest"),)// Initialise serviceservice.Init()// Register HandlertaskHandler :&#61; &handler.TaskHandler{TaskRepository: &repository.TaskRepositoryImpl{Conn: conn,},}if err :&#61; pb.RegisterTaskServiceHandler(service.Server(), taskHandler); err !&#61; nil {log.Fatal(errors.WithMessage(err, "register server"))}// Run serviceif err :&#61; service.Run(); err !&#61; nil {log.Fatal(errors.WithMessage(err, "run server"))}
}// 连接到MongoDB
func connectMongo(uri string, timeout time.Duration) (*mongo.Client, error) {ctx, cancel :&#61; context.WithTimeout(context.Background(), timeout)defer cancel()client, err :&#61; mongo.Connect(ctx, options.Client().ApplyURI(uri))if err !&#61; nil {return nil, errors.WithMessage(err, "create mongo connection session")}return client, nil
}


四、运行并校验

4.1 运行

运行make build或者go build -o task-srv main.go打包程序&#xff0c;这个时候你大概率会看到如下错误&#xff1a;

cf4c14e9f887c0362353782a6fa42d6c.png

这是因为etcd包的一个版本兼容性问题&#xff0c;为什么之前的hello服务没有遇到这个情况呢&#xff0c;查看micro工具自动生成的go.mod文件&#xff0c;会发现如下内容&#xff1a;

// This can be removed once etcd becomes go gettable, version 3.4 and 3.5 is not,
// see https://github.com/etcd-io/etcd/issues/11154 and https://github.com/etcd-io/etcd/issues/11931.
replace google.golang.org/grpc &#61;> google.golang.org/grpc v1.26.0

根据提示在我们的task-srv/go.mod中加入

replace google.golang.org/grpc &#61;> google.golang.org/grpc v1.26.0

再次运行&#xff1a;

> go run main.go
2020-09-10 17:42:46 file&#61;v2&#64;v2.9.1/service.go:200 level&#61;info Starting [service] go.micro.service.task
2020-09-10 17:42:46 file&#61;grpc/grpc.go:864 level&#61;info Server [grpc] Listening on [::]:63776
2020-09-10 17:42:46 file&#61;grpc/grpc.go:697 level&#61;info Registry [mdns] Registering node: go.micro.service.task-816e865d-7d43-4d02-94b4-be11cd475193

终于成功运行。


4.2 task-cli 调用服务

与上一章类似&#xff0c;新建并编辑task-srv/task-cli.go:

package mainimport ("context""github.com/micro/go-micro/v2""log"pb "task-srv/proto/task""task-srv/repository""time"
)func main() {// 在日志中打印文件路径&#xff0c;便于调试代码log.SetFlags(log.Llongfile)// 客户端也注册为服务server :&#61; micro.NewService(micro.Name("go.micro.client.task"))server.Init()taskService :&#61; pb.NewTaskService("go.micro.service.task", server.Client())// 调用服务生成三条任务now :&#61; time.Now()insertTask(taskService, "完成学习笔记&#xff08;一&#xff09;", now.Unix(), now.Add(time.Hour*24).Unix())insertTask(taskService, "完成学习笔记&#xff08;二&#xff09;", now.Add(time.Hour*24).Unix(), now.Add(time.Hour*48).Unix())insertTask(taskService, "完成学习笔记&#xff08;三&#xff09;", now.Add(time.Hour*48).Unix(), now.Add(time.Hour*72).Unix())// 分页查询任务列表page, err :&#61; taskService.Search(context.Background(), &pb.SearchRequest{PageCode: 1,PageSize: 20,})if err !&#61; nil {log.Fatal("search1", err)}log.Println(page)// 更新第一条记录为完成row :&#61; page.Rows[0]if _, err &#61; taskService.Finished(context.Background(), &pb.Task{Id: row.Id,IsFinished: repository.Finished,}); err !&#61; nil {log.Fatal("finished", row.Id, err)}// 修改查询到的第二条数据,延长截至日期row &#61; page.Rows[1]if _, err &#61; taskService.Modify(context.Background(), &pb.Task{Id: row.Id,Body: row.Body,StartTime: row.StartTime,EndTime: now.Add(time.Hour * 72).Unix(),}); err !&#61; nil {log.Fatal("modify", row.Id, err)}// 删除第三条记录row &#61; page.Rows[2]if _, err &#61; taskService.Delete(context.Background(), &pb.Task{Id: row.Id,}); err !&#61; nil {log.Fatal("delete", row.Id, err)}// 再次分页查询&#xff0c;校验修改结果page, err &#61; taskService.Search(context.Background(), &pb.SearchRequest{})if err !&#61; nil {log.Fatal("search2", err)}log.Println(page)
}
func insertTask(taskService pb.TaskService, body string, start, end int64) {_, err :&#61; taskService.Create(context.Background(), &pb.Task{Body: body,StartTime: start,EndTime: end,})if err !&#61; nil {log.Fatal("create", err)}log.Println("create task success! ")
}

运行后就可以看到如下结果&#xff1a;

>go run task-cli.go
2020-09-11 02:46:17.731409 I | E:/go-workspace/go-micro-study-notes/go-todolist1/task-srv/task-cli.go:79: create task success!
2020-09-11 02:46:17.732439 I | E:/go-workspace/go-micro-study-notes/go-todolist1/task-srv/task-cli.go:79: create task success!
2020-09-11 02:46:17.733434 I | E:/go-workspace/go-micro-study-notes/go-todolist1/task-srv/task-cli.go:79: create task success!
2020-09-11 02:46:17.734431 I | E:/go-workspace/go-micro-study-notes/go-todolist1/task-srv/task-cli.go:33: pageSize:20 pageCode:1 sortBy:"CreateTime" order:-1 rows:{id:"5f5a74791f01617808fef8b6" body:"完成学习笔记&#xff08;一&#xff09;
" startTime:1599763577 endTime:1599849977 createTime:1599763577} rows:{id:"5f5a74791f01617808fef8b7" body:"完成学习笔记&#xff08;二&#xff09;" startTime:1599849977 endTime:1599936377 createTime:1599763577} rows:{id:"5f5a74791f0161
7808fef8b8" body:"完成学习笔记&#xff08;三&#xff09;" startTime:1599936377 endTime:1600022777 createTime:1599763577}
2020-09-11 02:46:17.738421 I | E:/go-workspace/go-micro-study-notes/go-todolist1/task-srv/task-cli.go:68: pageSize:20 pageCode:1 sortBy:"CreateTime" order:-1 rows:{id:"5f5a74791f01617808fef8b6" body:"完成学习笔记&#xff08;一&#xff09;
" startTime:1599763577 endTime:1599849977 isFinished:1 finishTime:1599763577 createTime:1599763577 updateTime:1599763577} rows:{id:"5f5a74791f01617808fef8b7" body:"完成学习笔记&#xff08;二&#xff09;" startTime:1599849977 endTime:
1600022777 createTime:1599763577 updateTime:1599763577}


总结

本章我们参照demo示例&#xff0c;从头开始手写了一个完整的微服务task-srv&#xff0c;并且引入了数据库操作。 下一章我们将围绕这个核心服务再开发两个新的微服务&#xff0c;并尝引入之前被我们搁置的消息服务。



推荐阅读
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • Webpack5内置处理图片资源的配置方法
    本文介绍了在Webpack5中处理图片资源的配置方法。在Webpack4中,我们需要使用file-loader和url-loader来处理图片资源,但是在Webpack5中,这两个Loader的功能已经被内置到Webpack中,我们只需要简单配置即可实现图片资源的处理。本文还介绍了一些常用的配置方法,如匹配不同类型的图片文件、设置输出路径等。通过本文的学习,读者可以快速掌握Webpack5处理图片资源的方法。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 本文介绍了PhysioNet网站提供的生理信号处理工具箱WFDB Toolbox for Matlab的安装和使用方法。通过下载并添加到Matlab路径中或直接在Matlab中输入相关内容,即可完成安装。该工具箱提供了一系列函数,可以方便地处理生理信号数据。详细的安装和使用方法可以参考本文内容。 ... [详细]
  • 本文介绍了Perl的测试框架Test::Base,它是一个数据驱动的测试框架,可以自动进行单元测试,省去手工编写测试程序的麻烦。与Test::More完全兼容,使用方法简单。以plural函数为例,展示了Test::Base的使用方法。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • web.py开发web 第八章 Formalchemy 服务端验证方法
    本文介绍了在web.py开发中使用Formalchemy进行服务端表单数据验证的方法。以User表单为例,详细说明了对各字段的验证要求,包括必填、长度限制、唯一性等。同时介绍了如何自定义验证方法来实现验证唯一性和两个密码是否相等的功能。该文提供了相关代码示例。 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • 本文介绍了Python函数的定义与调用的方法,以及函数的作用,包括增强代码的可读性和重用性。文章详细解释了函数的定义与调用的语法和规则,以及函数的参数和返回值的用法。同时,还介绍了函数返回值的多种情况和多个值的返回方式。通过学习本文,读者可以更好地理解和使用Python函数,提高代码的可读性和重用性。 ... [详细]
  • 1.脚本功能1)自动替换jar包中的配置文件。2)自动备份老版本的Jar包3)自动判断是初次启动还是更新服务2.脚本准备进入ho ... [详细]
  • 精讲代理设计模式
    代理设计模式为其他对象提供一种代理以控制对这个对象的访问。代理模式实现原理代理模式主要包含三个角色,即抽象主题角色(Subject)、委托类角色(被代理角色ÿ ... [详细]
  • 后台自动化测试与持续部署实践
    后台自动化测试与持续部署实践https:mp.weixin.qq.comslqwGUCKZM0AvEw_xh-7BDA后台自动化测试与持续部署实践原创 腾讯程序员 腾讯技术工程 2 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • Android日历提醒软件开源项目分享及使用教程
    本文介绍了一款名为Android日历提醒软件的开源项目,作者分享了该项目的代码和使用教程,并提供了GitHub项目地址。文章详细介绍了该软件的主界面风格、日程信息的分类查看功能,以及添加日程提醒和查看详情的界面。同时,作者还提醒了读者在使用过程中可能遇到的Android6.0权限问题,并提供了解决方法。 ... [详细]
  • express工程中的json调用方法
    本文介绍了在express工程中如何调用json数据,包括建立app.js文件、创建数据接口以及获取全部数据和typeid为1的数据的方法。 ... [详细]
author-avatar
chingueen_306
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有