租辆酷车小程序开发(二)—— 接入微服务GRPC
vscode中golang的配置
- 设置依赖管理
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
GO111MODULE=auto 在$GOPATH/src 外面且根目录有go.mod 文件时,开启模块支持
GO111MODULE=off 无模块支持,go会从GOPATH 和 vendor 文件夹寻找包
GO111MODULE=on 模块支持,go会忽略GOPATH和vendor文件夹,只根据go.mod下载依赖
-
安装go插件
-
view —— Command Palatte —— 选择Go: Install/Update Tools 安装所有工具
-
在coolcar目录下新建server目录,使用File-Add Folder to Workspace添加server目录到vscode
这里不能直接打开coolcar目录,因为go的插件与项目根目录有关,所以我们在这里分别将wx和server目录添加到我们的workspace
- file —— preference —— setting
- server目录下输入go mod init coolcar 新建go文件测试能否正常运行
三种执行方法:
Run —— Run Without Debugging
命令行—— go run hello.go
Code Runner插件
断点调试:
添加断点 —— Run —— Start Debugging
GRPC
gRPC 是Google公司开发的一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。
RPC(Remote Procedure Call)远程过程调用,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,简单的理解是一个节点请求另一个节点提供的服务。RPC只是一套协议,基于这套协议规范来实现的框架都可以称为 RPC 框架,比较典型的有 Dubbo、Thrift 和 gRPC。
向服务器发送请求需要关注的问题:
- 协议:http/http2/TCP
- 服务器地址:api.coolcar.cn
- 路径:/trip
- 参数
- 数据类型
- 数据编码:JSON/XML/Protobuf
- 安全性:token
- 错误处理:http status
GRPC:
- 协议:HTTP/2
- 方法:POST
- 路径:服务器地址/Service/Method(Ps:地址/TripService/GetTrip)
- 参数:body
- 安全性:HTTP2协议/header中存放token
- 数据:二进制
- 数据结构:二进制数据使用ProtoBuf进行编码
优点:
- 高效的数据传输
- 语言无关的领域模型定义
其他DSL/IDL:
- Thrift
- Swagger
- 使用yaml描述
- 描述REST API
- Goa
ProtoBuf编译器的安装
-
安装protobuf编译器
https://github.com/protocolbuffers/protobuf/releases
下载解压后,将bin目录添加到环境变量,在终端执行protoc命令可以看到对应输出 -
安装go语言插件,(本项目使用的grpc gateway是v1版本,目前grpc gateway已经有较大的更新),这三个插件安装在GOPATH的bin目录下
go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway@latest
go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger@latest
go install github.com/golang/protobuf/protoc-gen-go@latest
protoc-gen-go:生成go语言代码
protoc-gen-grpc-gateway:生成grpc-gateway代码
protoc-gen-swagger:从protoc生成swagger‘
ProtoBuf的使用
.proto文件
vscode中安装vscode-proto3插件
server目录下新建proto目录,在proto目录下新建trip.proto文件
proto文件只是定义了一种传输的数据格式,不包含方法
// 语法
syntax = "proto3";
// proto文件的package,用处不大
package coolcar;option go_package="coolcar/proto/gen/go;trippb";// 定义数据格式(不存在方法只是定义一种数据结构)
message Trip {// (二进制数据流不知道字段在哪里开始在哪里结束所以要在变量后声明字段顺序,与json不同的是json中:后面跟的是对应的数据)string start = 1; // 第一个字段string end = 2; // 第二个字段int64 duration_sec = 3; // 注明单位int64 fee_cent = 4;
}
option go_package="coolcar/proto/gen/go;trippb";
:定义了引用的格式,生成的go代码的package为trippb,导入go代码时使用import trippb "coolcar/proto/gen/go"
导入
生成go语言代码
protoc -I =. --go_out=paths=source_relative:gen/go trip.proto
-I=.
:输入目录为当前目录--go_out=paths=source_relative:gen/go
go_out
:生成go语言代码paths=source_relative
:相对路径,xx=xx是送给go_out插件的参数gen/go
:生成go语言的路径
trip.proto
:源文件
生成文件如下,这里的package为.proto文件中go_package所定义
使用生成的代码
导入使用该生成的文件,路径在.proto文件的option go_package="coolcar/proto/gen/go;trippb
中定义
fmt.Println(&trip)
:输出要取地址,否则会有警告,这里建议在使用proto时一律取地址
b, err := proto.Marshal(&trip)
:编码为二进制流
err = proto.Unmarshal(b, &trip2)
:将二进制流解码
服务器将Marshal的二进制数据流通过grpc服务的tcp端口发给客户端,客户端收到后Unmarshal就可以获得原始数据
b, err = json.Marshal(&trip2)
:编码为json格式方便交互(在trip.pb.go中已经定义了对应的json字段)
微服务之间通过grpc传递二进制流互相通信,暴露给前端小程序使用json进行通信
package mainimport (trippb "coolcar/proto/gen/go""encoding/json""fmt""google.golang.org/protobuf/proto"
)func main() {trip := trippb.Trip{Start: "abc",End: "def",DurationSec: 3600,FeeCent: 10000,}fmt.Println(&trip)// 二进制流编码b, err := proto.Marshal(&trip)if err != nil {panic(err)}fmt.Printf("%X\n", b)// 二进制流解码var trip2 trippb.Trip// 需要加地址才能写入err = proto.Unmarshal(b, &trip2)if err != nil {panic(err)}// 凡是引用一律加地址fmt.Println(&trip2)// 转jsonb, err = json.Marshal(&trip2)if err != nil {panic(err)}fmt.Printf("%s\n", b)
}
proto的数据类型
Trigger Suggest 快捷键可以查看可选类型
复合类型
message Location {double latiture = 1;double longitude = 2;
}
message Trip {string start = 1;Location start_pos = 5;string end = 2;Location end_pos = 6;int64 duration_sec = 3;int64 fee_cent = 4;
}### repeate
repeated类型
定义go语言中的切片
repeated Location path_locations = 7;
=> PathLocations []*Location
message Location{double latitude = 1;double longitude =2;
}
message Trip {string start = 1; Location start_pos = 5; //新加字段序号往后排repeated Location path_locations = 7;string end = 2; Location end_pos = 6;int64 duration_sec = 3; int64 fee_cent = 4;
}
生成的go语言代码如下:
使用方法:
枚举类型
message Location{double latitude = 1;double longitude =2;
}
enum TripStatus{TS_NOT_SPECIFIED = 0;NOT_STARTED = 1;IN_PROGRESS = 2;FINISHED = 3;PAID = 4;
}
message Trip {string start = 1; Location start_pos = 5; //新加字段序号往后排repeated Location path_locations = 7;string end = 2; Location end_pos = 6;int64 duration_sec = 3; int64 fee_cent = 4;TripStatus status = 8;
}
生成的go语言代码
使用方法:
ProtoBuf字段的可选性
问题:软件更新后新老版本的message字段不同,新老版本同时存在,如何处理新老版本之间的数据传输
假设新版本新加了TripStatus字段,此时当新版本向老版本传输数据时,老版本会忽略该字段
老版本向新版本传输数据时,缺少了TripStatus字段,但是ProtoBuf中字段都是可选的,这里TripStatus的值会被赋为零值
go语言中每个类型都有一个零值,0,“”,false等
ProtoBuf中对字段赋零值和不填的效果是一样的(这里无法区分该值是赋值为0还是根本没有填,如果必须区分可以新加一个bool字段表明是否有值),若将这里DurationSec赋值为0,则跟没填的效果一样
打印结果为:
这里的DurationSec不会被打印出来
在系统设计的时候需要为每个字段设计合适的零值:
例如:假设要新增一个功能可以让没有登陆的用户也可以使用我们的产品,我们需要区分这个行程是已登录的用户还是未登录的用户
假设在message新加了一个字段bool isFromLoggedInUser
,判断是否为登录的用户,这里老版本没有这个字段在向新版本传输数据时会赋值为false,即将所有老版本的用户都视为未登录的用户,这是错误的,因为老系统只允许登录的用户,也就是说老系统的用户都是已登录的用户。
应该添加的字段是bool isFromGuestUser
,这样老版本的用户该字段都为false,都不是未登录的用户,只有在新版本添加该功能后才会有用户被设置为true
GRPC服务器及客户端
定义服务
proto文件中添加如下定义
message GetTripRequest {string id = 1;
}message GetTripResponse {string id = 1;Trip trip = 2;
}service TripService{rpc GetTrip (GetTripRequest) returns (GetTripResponse);
}
使用protoc -I =. --go_out=plugins=grpc,paths=source_relative:gen/go trip.proto
命令生成代码,这里新加了一个plugins=grpc参数
plugins=grpc, paths=source_relative 是两个参数
gen/go 是生成代码的路径
在生成的go语言代码中可以使用Go to Symbol in Editor搜索TripService
可以看到TripServiceClient是一个接口,其中包括GetTripRequest、GetTripResponse等
同样TripServiceServer也是一个接口
要实现这两个功能只需要实现这个接口即可
实现TripServiceServer接口
在server下新建tripservice目录,在目录下新建trip.go
定义Service结构体实现GetTrip方法,这样Service就实现了TripServiceServer接口
package tripimport ("context"trippb "coolcar/proto/gen/go"
)type Service struct{}func (*Service) GetTrip(c context.Context, req *trippb.GetTripRequest) (*trippb.GetTripResponse, error) {return &trippb.GetTripResponse{Id: req.Id,Trip: &trippb.Trip{Start: "abc",End: "def",DurationSec: 3600,FeeCent: 10000,StartPos: &trippb.Location{Latitude: 30,Longitude: 120,},EndPos: &trippb.Location{Latitude: 35,Longitude: 115,},PathLocations: []*trippb.Location{{Latitude: 31,Longitude: 119,},{Latitude: 32,Longitude: 118,},},Status: trippb.TripStatus_IN_PROGRESS,},}, nil
}
启动服务
1. 设置监听端口
2. 创建grpc.server
3. 将之前实现的TripServiceServer接口注册到server服务器
4. 服务器与监听端口绑定
package mainimport (trippb "coolcar/proto/gen/go"trip "coolcar/tripservice""log""net""google.golang.org/grpc"
)func main() {// 监听端口lis, err := net.Listen("tcp", ":8081")if err != nil {// 不使用panic,Fatalf:输出之后程序退出log.Fatalf("failed to listen: %v", err)}s := grpc.NewServer()trippb.RegisterTripServiceServer(s, &trip.Service{})// 如果s.Serve没有出错的话会一直监听不会退出,如果出错则直接将返回的error输出log.Fatal(s.Serve(lis))
}
建立客户端向服务器发送请求
在server下新建client目录,目录下新建main.go
1. 建立到服务端的连接
2. 由连接建立client
3. 通过新建的client调用GetTrip方法,传入方法参数,交给server实现的方法处理
grpc.Dial如果不加第二个参数直接运行会报错提示不安全,我们按照错误的提示添加了第二个参数即可正常运行
这里调用的GetTrip方法为client调用的服务器上的方法,这个方法正是在之前实现接口时所实现的方法
package mainimport ("context"trippb "coolcar/proto/gen/go""fmt""log""google.golang.org/grpc""google.golang.org/grpc/credentials/insecure"
)func main() {// 设置日志格式log.SetFlags(log.Lshortfile)// 建立连接conn, err := grpc.Dial("localhost:8081", grpc.WithTransportCredentials(insecure.NewCredentials()))if err != nil {log.Fatalf("cannot connect server: %v", err)}// 新建clienttsClient := trippb.NewTripServiceClient(conn)// 调用GetTrip方法r, err := tsClient.GetTrip(context.Background(), &trippb.GetTripRequest{Id: "trip456"})if err != nil {log.Fatalf("cannot call GetTrip: %v", err)}fmt.Println(r)
}
执行结果:
完整流程:
- 在proto文件中定义server接口要实现GetTrip方法,在这里只给出了GetTrip的定义没有给出具体的实现
- 然后我们在第一步中实现了TripServiceServer接口,在这里第一次实现了GetTrip方法
- 随后在建立服务端的时候将实现了TripServiceServer接口的Service结构体注册到服务器
- 在客户端向服务器发送请求调用GetTrip方法,传入方法参数
REST vs RPC
HTTP协议
HTTP协议是基于TCP传输的,TCP协议只负责可靠的传输数据没有规定数据传输的格式,HTTP负责规定TCP协议传输的数据
Method:GET、PUT、POST、DELETE…
URL
DATA
RPC
RPC(Remote Procedure Call),远程过程调用,其中Procedure通常认为就是一个函数,RPC不需要指定远程服务器的地址,因为RPC是在已经建立连接的TCP服务之上的,只需要给定函数名称和函数参数就可以调用。
RPC在前后端之间的通信仍需借用HTTP协议,因为浏览器或小程序最方便的仍是发送HTTP请求,将RPC服务暴露在网上有两种风格的接口
RPC
- Method
- POST
- URL
- api.service.com/GetTrip
- api.service.com/CreateTrip
- Data
- GetTripRequest
- CreateTripRequest
REST
method是一个动词,动作的对象是url是一个名词
- C
- POST api.service.com/trip (POST是动词,后面跟的url是一个名词)
- Data:JSON
- R
- GET api.service.com/trip/{id}
- U
- PUT api.service.com/trip/{id}
- Data:JSON
- D
- DELETE api.service.com/trip/{id}
前端请求GRPC服务的两种方案架构
要想把GRPC服务直接给前端(小程序/web)使用,还需要GRPC over HTTP,小程序不能直接发GRPC请求,只能发HTTP请求。
假设项目中有一个微服务的架构其中有很多GRPC的server,在系统内部GRPC之间都通过TCP连接进行通信,这里的问题在于怎样把内部的GRPC服务暴露给前端的小程序(Web)
- GRPC Gateway
- GRPC Web Proxy
GRPC Web Proxy
该方案是给Web端使用的,Web端与GRPC Web Proxy建立HTTP连接,传输的是二进制数据流(之前protobuf序列化得到的二进制数据流),Web Proxy接收到二进制数据后,会分析其调用的GRPC服务的具体参数,然后将该二进制数据送到后台具体的GRPC服务,这里的GRPC Web Proxy也称为反向代理,外部请求发到服务集群时一律是发到GRPC Web Proxy,然后再由GRPC Web Proxy将请求分发到内网不同的服务上,在整个过程中传输的数据都是二进制数据。这个方案需要在Web端就将数据编码为二进制数据,这在go语言中比较容易,但是在html和js中比较麻烦。
GRPC Gateway
gRPC Gateway 是一个用于将 gRPC 服务暴露为 RESTful API 的代理工具。它通过在 gRPC 服务和 HTTP 请求之间提供一个转换层,使得使用 HTTP 和 RESTful 接口的客户端也可以与 gRPC 服务进行通信。简而言之,gRPC Gateway 允许你通过 HTTP REST 请求访问 gRPC 服务,从而使得你可以同时享受 gRPC 的高效性和 HTTP/JSON 的兼容性。
GRPC Gateway在小程序和web端都可以使用,前端通过HTTP协议向GRPC Gateway传输JSON格式的字符串,GRPC Gateway也是反向代理,将请求分发给内网的服务,但是GRPC Gateway在分发之前需要首先将JSON转为二进制数据流。
为什么要使用GRPC需要复杂的代理转发还要用GRPC进行开发呢?直接用REST API 内部都用HTTP协议直接进行传输不是更方便吗?
无论用GRPC还是REST API都要搭建一个类似的结构,内网各种服务之间畅通通信,内网到外网都需要反向代理向外暴露一个服务接口,不可能由微服务本身直接向外界暴露接口,对内对外总是要分成两部分来考虑,中间代理层无法省略,所以我们不如在代理层做一个GRPC的转换使得内网的服务都在GRPC中完成,GRPC的服务开发是非常方便的
GRPC Gateway的实现
定义接口
定义http rest层面上暴露的接口,在proto目录下新建trip.yaml文件
selector中coolcar为proto文件的package,TripService为服务名,GetTrip为定义的方法
对应selector的服务,暴露的接口为method为get,url为 /trip/{id}
type: google.api.Service
config_version: 3http:rules:- selector: coolcar.TripService.GetTripget: /trip/{id}
当收到的请求url方法为get并且请求路径为/trip/{id}就可以解析出请求的方法为TripService.GetTrip
生成代码
执行命令:
protoc -I =. --go_out=plugins=grpc,paths=source_relative:gen/go trip.proto
protoc -I =. --grpc-gateway_out=paths=source_relative,grpc_api_configuration=trip.yaml:gen/go trip.proto
在proto/gen/go目录下生成了trip.pb.go和trip.pb.gw.go文件,trip.pb.gw.go在一开始就说明了该文件是一个反向代理,将gRPC转换为RESTful接口
开启gateway服务
在main.go(服务端)中新建函数
- context:在后面仔细讲,这里生成了一个没有具体内容的上下文,在这个上下文中连接后端的grpc服务,并在之后为上下文添加cancel的能力,在服务结束后断开连接
- mux: multiplexer 一对多,分发器
- “localhost:8081”:tripservice的地址
- []grpc.DialOption:连接方式,这里选择不安全的方式,不加这个可能会提示连接不安全报错
- trippb.RegisterTripServiceHandlerFromEndpoint:注册服务
这里的trippb.RegisterTripServiceHandlerFromEndpoint函数就是建立了下图红色箭头所示的连接
在context的基础上建立连接,连接注册在mux上,连接对象是localhost:8081 - http.ListenAndServe(“:8080”, mux):gateway的监听端口
func startGRPCGateway() {// context在后面仔细讲,这里生成了一个没有具体内容的上下文,在这个上下文中连接后端的grpc服务c := context.Background()// 为上下文添加cancel的能力,cancel是一个函数,调用cancel连接就会被断开c, cancel := context.WithCancel(c)// 服务结束断开连接defer cancel()// mux: multiplexer 一对多,分发器mux := runtime.NewServeMux()// 通过context c连接,c在函数返回后就会被cancel掉,这个连接注册在NewServeMux上:8081,连接方式为insecure通过tcp明文连接err := trippb.RegisterTripServiceHandlerFromEndpoint(c,mux,"localhost:8081",[]grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())},)if err != nil {log.Fatalf("cannot start grpc gateway: %v", err)}// http监听地址,8081是tripservice的地址与gateway的地址是不同的err = http.ListenAndServe(":8080", mux)if err != nil {log.Fatalf("cannot listen and server: %v", err)}
}
go 关键字用于启动一个新的并发执行的goroutine。Go语言中的goroutine是轻量级的线程,它们在Go运行时中被多路复用到线程上,当你在一个函数调用前加上go关键字时,Go运行时会为该函数创建一个新的goroutine,并立即开始执行,而不会阻塞调用它的goroutine。这意味着你可以同时运行多个函数,它们可以独立地执行,并且可以相互通信。
启动该服务,在浏览器(http协议) 访问http://localhost:8080/trip/123,可以看到返回数据
- 此处的流程为从Web端发送请求/trip/123到GRPC Gateway,GRPC Gateway在初始化的时候就建立了到8081端口的连接
- 根据yaml文件的配置将该请求翻译为针对GetTrip的请求,并且可以得到url中的id就是真正送给gettrip的id,注意yaml中的id对应的就是GetTripRequest中的id这里需要对应(这里具体的实现可以看trip.pb.gw.go的源码进一步理解)
- 接收到请求后GetTrip就可以获取tripid为123的行程返回二进制数据到Gateway,Gateway将数据转为JSON形式返回给Web
小程序访问GRPC Gateway
直接在app.ts中发送request请求
// app.ts
App<IAppOption>({globalData: {},onLaunch() {wx.request({url: 'http://localhost:8080/trip/trip123',method: 'GET',success: console.log,fail: console.error})// 登录wx.login({success: res => {console.log(res.code)// 发送 res.code 到后台换取 openId, sessionKey, unionId},})},
})
每个微信小程序需要事先设置通讯域名,小程序只可以跟指定的域名进行网络通信。
https://developers.weixin.qq.com/miniprogram/dev/framework/ability/network.html
我们在本地调试的时候可以设置为不校验合法域名
访问返回结果:
数据类型填坑
小程序的返回中duration和fee都是string的形式,但是我们希望是number的形式,考虑原因是duration和fee在proto文件中的数据类型是int64,因为太大了所以转为了字符串,我们尝试将类型改为int32重新生成就是number的格式
枚举类型传输的本质是一个数值,这里的IN_PROGRESS实际上是2,考虑兼容性我们将这里的status设置为数值
在定义gateway服务的mux时添加转换方式
mux := runtime.NewServeMux(runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{EnumsAsInts: true,OrigName: true,},))
小程序项目结构调整
为了给小程序加入proto类型需要引入第三方包,为了引入第三方包需要首先调整小程序的目录结构,把小程序所有的内容都移动到miniprogram目录下
- 在miniprogram目录下新建appoption.ts,将typeings目录下的index.d.ts的内容复制到apption.ts中,删除typings目录(可以生成的文件都可以删掉,使用npm install都可以再生成)
- 删除package-lock.json(npm安装时生成的文件)
- 剩下的文件移动到miniprogram目录下
- project.config.json中删除miniprogramRoot,此时该文件已经在miniprogram目录下了
- tsconfig.json中删除typeRoots,typings我们已经删除了
- miniprogram目录下运行npm install命令
- 在tsconfig.json中添加"types": [“miniprogram-api-typings”]。这样就可以在小程序中正常使用type了,此时的目录为wx/miniprogram/node_modules/miniprogram-api-typings/types
- 运行npm run tsc,解决报错
- 重新在开发者工具中导入项目,项目根目录为wx/miniprogram
调整后的项目如下所示:
https://github.com/shn-1/coolcar2024/tree/0d606611189fa9cf222859a0ade14608635ee9f8
小程序请求的强类型化
把protobuf转为ts代码,可以获取接收数据的类型
-
小程序目录miniprogram下运行
npm install protobufjs
,注意要把package.json中的安装版本设置为6.11.4
-
新建miniprogram/service/proto_gen目录,在server/proto目录下运行命令,从protobuf生成js文件
../../wx/miniprogram/node_modules/.bin/pbjs -t static -w es6 trip.proto --no-create --no-decode --no-verify --no-delimited -o ../../wx/miniprogram/service/proto_gen/trip_pb.js
-
server/proto目录下运行命令,从js文件生成ts文件
../../wx/miniprogram/node_modules/.bin/pbts -o ../../wx/miniprogram/service/proto_gen/trip_pb.d.ts ../../wx/miniprogram/service/proto_gen/trip_pb.js
在app.ts中获取返回值这里直接编译运行会报错,需要在小程序开发工具的本地设置中开启将JS编译成ES5,并且在trip_pb.js的开头添加
import * as $protobuf from "protobufjs";
这里只拿到了部分的返回值,这是因为在传参的时候存在驼峰命名和下划线命名,在生成的Trip的接口定义中使用的都是驼峰命名,因此需要把下划线命名改为驼峰命名才能识别
一种方式是在后端将mux的OrigName设置为false,这样传递的参数都是驼峰命名
但是约定在通常网络传输中都是使用下划线命名
第二种方法,我们将OrigName改回false,在小程序上安装npm install camelcase-keys
,这里的版本为6.2.2
在传递参数的时候将其转换为驼峰命名,在开发工具中重新构建npm
此时接收到的就是驼峰命名
这里接收到的status是2,我们需要解析出2对应的实际字符串
console.log('status is', coolcar.TripStatus[getTripRes.trip?.status!])
通过以上的方法我们可以.出接受数据的内容有哪些,方便开发
通过proto文件定义,既可以在go语言中实现也可以在typescript中实现,两端都通过proto文件定死,保证了前后端对接口的理解一致
项目目录中的miniprogram_npm是通过开发者工具构建npm生成的,它是node_modules的子集,只有在dependencies中的库才会被编译进miniprogram_npm,当小程序打包发布时miniprogram_npm会被打包发给所有的用户,node_modules不会打包,这里的protobufjs文件有些大,暂时可以接受,后期可以再优化
我们在trip_pb.js中添加了一行代码import * as $protobuf from "protobufjs";
当重新生成这个文件时这段代码又没有了,这里需要通过shell脚本实现,将生成的文件暂存,然后将代码先输出到js文件,再将暂存的文件追加到js文件后,再删除临时文件
$PBTS_BIN_DIR/pbjs -t static -w es6 trip.proto --no-create --no-encode --no-decode --no-verify --no-delimited -o $PBTS_OUT_DIR/trip_pb_tmp.js
echo 'import * as $protobuf from "protobufjs";\n' > $PBTS_OUT_DIR/trip_pb.js
cat $PBTS_OUT_DIR/trip_pb_tmp.js >> $PBTS_OUT_DIR/trip_pb.js
rm $PBTS_OUT_DIR/trip_pb_tmp.js
miniprogram_npm是通过开发工具构建npm生成的,我们不希望将它也上传到git仓库,在.gitignore中添加miniprogram_npm (在vscode终端直接使用code ..\..\.gitignore
就可以在vscode中打开该文件)
trip_pb.js等虽然是生成的文件,但是执行的命令比较复杂,所以我们将它也上传到git仓库,在.gitignore中添加!**/wx/miniprogram/service/proto_gen/*.js
修改README
相关文章:

租辆酷车小程序开发(二)—— 接入微服务GRPC
vscode中golang的配置 设置依赖管理 go env -w GO111MODULEon go env -w GOPROXYhttps://goproxy.cn,direct GO111MODULEauto 在$GOPATH/src 外面且根目录有go.mod 文件时,开启模块支持 GO111MODULEoff 无模块支持,go会从GOPATH 和 vendor 文件夹寻找包…...

如何在 Ubuntu 22.04 上安装 Metabase 数据可视化分析工具
简介 Metabase 提供了一个简单易用的界面,让你能够轻松地对数据进行探索和分析。通过本文的指导,你将能够在 Ubuntu 22.04 系统上安装并配置 Metabase,并通过 Nginx 进行反向代理以提高安全性。本教程假设你已经拥有了一个非 root 用户&…...
MySQL 用户与权限管理
MySQL 是一种广泛使用的关系型数据库管理系统,支持多用户访问和权限控制。在多用户环境下,数据库安全至关重要,而用户和权限管理是数据库管理中最基础也是最重要的一部分。通过合理地创建和管理用户、分配和管理权限、使用角色权限,可以有效地保护数据库,确保数据的安全性…...

【Web前端】如何构建简单HTML表单?
HTML 表单是 Web 开发中非常重要的组成部分。它们是与用户交互的主要方式,能够收集用户输入的数据。表单的灵活性使它们成为 HTML 中最复杂的结构之一,但若使用正确的结构和元素,可以确保其可用性和无障碍性。 表单的基本结构 HTML 表单使用…...

Spring Boot 3 集成 Spring Security(3)数据管理
文章目录 准备工作新建项目引入MyBatis-Plus依赖创建表结构生成基础代码 逻辑实现application.yml配置SecurityConfig 配置自定义 UserDetailsService创建测试 启动测试 在前面的文章中我们介绍了 《Spring Boot 3 集成 Spring Security(1)认证》和 《…...

书生大模型实战营第四期-入门岛-4. maas课程任务
书生大模型实战营第四期-入门岛-4. maas课程任务 任务一、模型下载 任务内容 使用Hugging Face平台、魔搭社区平台(可选)和魔乐社区平台(可选)下载文档中提到的模型(至少需要下载config.json文件、model.safetensor…...
Spring ApplicationListener监听
【JavaWeb】Spring ApplicationListener-CSDN博客 ApplicationEvent以及Listener是Spring为我们提供的一个事件监听、订阅的实现,内部实现原理是观察者设计模式,设计初衷也是为了系统业务逻辑之间的解耦,提高可扩展性以及可维护性。事件发布…...

K8s调度器扩展(scheduler)
1.K8S调度器 筛选插件扩展 为了熟悉 K8S调度器扩展步骤,目前只修改 筛选 插件 准备环境(到GitHub直接下载压缩包,然后解压,解压要在Linux系统下完成) 2. 编写调度器插件代码 在 Kubernetes 源代码目录下编写调度插件…...

IntelliJ IDEA 中,自动导包功能
在 IntelliJ IDEA 中,自动导包功能可以极大地提高开发效率,减少手动导入包所带来的繁琐和错误。以下是如何在 IntelliJ IDEA 中设置和使用自动导包功能的详细步骤: 一、设置自动导包 打开 IntelliJ IDEA: 启动 IntelliJ IDEA 并打…...

Spring事务笔记
目录 1.Spring 编程式事务 2.Transactional 3.事务隔离级别 4.Spring 事务传播机制 什么是事务? 事务是⼀组操作的集合, 是⼀个不可分割的操作. 事务会把所有的操作作为⼀个整体, ⼀起向数据库提交或者是撤销操作请求. 所以这组操作要么同时成 功, 要么同时失败 1.Spri…...

SQLite 管理工具 SQLiteStudio 3.4.5 发布
SQLiteStudio 3.4.5 版本现已发布,它带来了大量的 bug 修复,并增加了一些小功能。SQLiteStudio 是一个跨平台的 SQLite 数据库的管理工具。 具体更新内容包括: 现在可以使用 Collations Editor 窗口在数据库中注册 Extension-based collatio…...

QT 实现组织树状图
1.实现效果 在Qt中使用QGraphicsItem和QGraphicsScene实现树状图,你需要创建自定义的QGraphicsItem类来表示树的节点,并管理它们的位置和连接,以下是实现效果图。 2.实现思路 可以看见,上图所示,我们需要自定义连线类和节点类。 每个节点类Node,需要绘制矩形框体文字…...
go-学习
文章目录 简介标识符字符串的拼接,关键字数据类型声明变量常量算术运算符关系运算符逻辑运算符位运算赋值运算符其他运算符 简介 Go 语言的基础组成有以下几个部分: 1.包声明 2.引入包 3.函数 4.变量 5.语句 & 表达式 6.注释 package main import &q…...
【面试分享】主流编程语言的内存回收机制及其优缺点
以下是几种主流编程语言的内存回收机制及其优缺点: 一、Java 内存回收机制: Java 使用自动内存管理,主要通过垃圾回收器(Garbage Collector,GC)来回收不再被使用的对象所占用的内存。Java 的垃圾回收器会定…...
STM32-- 串口发送数据
while(USART_GetFlagStatus(USART2,USART_FLAG_TXE)RESET);?? 答: 这行代码: while(USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET);的作用是等待串口 USART2 的发送数据寄存器(TXE,Transmit Dat…...

数据结构 (13)串的应用举例
前言 数据结构中的串(String),也称为字符串,是一种常见且重要的数据结构,在计算机科学中被广泛应用于各种场景。 一、文本处理 文本编辑器:在文本编辑器中,字符串被用来表示和存储用户输入的文本…...

qt-- - 版本和下载介绍
qt版本很多,每个大版本都有几个版本是长期支持的(LTS),最好使用长期支持的。 例如qt5.15 qt6.2 qt6.8 都是LTS版本的。 qt在线安装需要提供账号,之前安装qt6.8因为账号问题试了很长时间,密码错了。 …...

解锁 Vue 项目中 TSX 配置与应用简单攻略
在 Vue 项目中配置 TSX 写法 在 Vue 项目中使用 TSX 可以为我们带来更灵活、高效的开发体验,特别是在处理复杂组件逻辑和动态渲染时。以下是详细的配置步骤: 一、安装相关依赖 首先,我们需要在命令行中输入以下命令来安装 vitejs/plugin-v…...

ShuffleNet:一种为移动设备设计的极致高效的卷积神经网络
摘要 https://arxiv.org/pdf/1707.01083 我们介绍了一种名为ShuffleNet的计算效率极高的卷积神经网络(CNN)架构,该架构专为计算能力非常有限的移动设备(例如10-150 MFLOPs)而设计。新架构利用两种新操作:逐…...

yum源问题的解决方案
linux课堂作业 问题描述 yum 直接安装tree的问题截图 这个错误表明你的系统没有正确注册到 Red Hat Subscription Management(这个问题不用管),也没有配置有效的 YUM 软件仓库,因此无法安装或更新软件包。 解决方案(…...

手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...

大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架,支持"一次开发,多端部署",可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务,为旅游应用带来…...
日常一水C
多态 言简意赅:就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过,当子类和父类的函数名相同时,会隐藏父类的同名函数转而调用子类的同名函数,如果要调用父类的同名函数,那么就需要对父类进行引用&#…...

认识CMake并使用CMake构建自己的第一个项目
1.CMake的作用和优势 跨平台支持:CMake支持多种操作系统和编译器,使用同一份构建配置可以在不同的环境中使用 简化配置:通过CMakeLists.txt文件,用户可以定义项目结构、依赖项、编译选项等,无需手动编写复杂的构建脚本…...
【实施指南】Android客户端HTTPS双向认证实施指南
🔐 一、所需准备材料 证书文件(6类核心文件) 类型 格式 作用 Android端要求 CA根证书 .crt/.pem 验证服务器/客户端证书合法性 需预置到Android信任库 服务器证书 .crt 服务器身份证明 客户端需持有以验证服务器 客户端证书 .crt 客户端身份…...

Linux基础开发工具——vim工具
文章目录 vim工具什么是vimvim的多模式和使用vim的基础模式vim的三种基础模式三种模式的初步了解 常用模式的详细讲解插入模式命令模式模式转化光标的移动文本的编辑 底行模式替换模式视图模式总结 使用vim的小技巧vim的配置(了解) vim工具 本文章仍然是继续讲解Linux系统下的…...

Appium下载安装配置保姆教程(图文详解)
目录 一、Appium软件介绍 1.特点 2.工作原理 3.应用场景 二、环境准备 安装 Node.js 安装 Appium 安装 JDK 安装 Android SDK 安装Python及依赖包 三、安装教程 1.Node.js安装 1.1.下载Node 1.2.安装程序 1.3.配置npm仓储和缓存 1.4. 配置环境 1.5.测试Node.j…...

Linux【5】-----编译和烧写Linux系统镜像(RK3568)
参考:讯为 1、文件系统 不同的文件系统组成了:debian、ubuntu、buildroot、qt等系统 每个文件系统的uboot和kernel是一样的 2、源码目录介绍 目录 3、正式编译 编译脚本build.sh 帮助内容如下: Available options: uboot …...