租辆酷车小程序开发(二)—— 接入微服务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 软件仓库,因此无法安装或更新软件包。 解决方案(…...
在Linux中备份msyql数据库和表的详细操作
目录 前情提要 一、备份mysql数据库 原库展示 (一)新建一个数据库 (二)在linux根目录下找个位置暂时存放 (三)临时sql还原真正存放到库中 (四)查看是否备份成功 备份库成功展示 二、备份表的操作 编辑 原表emp展示 (一)快速新建一个原结构相同的表 (二)原表所…...
实时数仓Kappa架构:从入门到实战
引言 随着大数据技术的不断发展,企业对实时数据处理和分析的需求日益增长。实时数仓(Real-Time Data Warehouse, RTDW)应运而生,其中Kappa架构作为一种简化的数据处理架构,通过统一的流处理框架,解决了传统…...
【老白学 Java】Warship v2.0(四)
Warship v2.0(四) 文章来源:《Head First Java》修炼感悟。 上一篇文章中,老白仔细分析了 v2.0 的设计思路以及实现手段,如果大家有好的设计方案也可以自行尝试。 本篇文章的主要内容是对 Warship 类进行最后的修改&a…...
LLM之学习笔记(一)
前言 记录一下自己的学习历程,也怕自己忘掉了某些知识点 Prefix LM 和 Causal LM区别是什么? Prefix LM (前缀语⾔模型)和 Causal LM(因果语言模型)是两者不同类型的语言模型,它们的区别在于生…...
C# 反射详解
反射是C#中的一个强大特性,允许程序在运行时检查和操作类型和对象的信息。 通过反射,你可以获取类型的属性、方法、构造函数等信息,并可以动态创建对象、调用方法或访问属性,甚至可以实现某些框架或库的核心功能。 反射的基本概念…...
pgadmin安装后运行不能启动界面的问题
在本人机器上安装了pgsql10后,自带的pgadmin安装后运行时能打开edge并显示数据库server和数据库的,后来又安装了pgsql17,结果安装后想打开pgadmin,结果一直在等待最后,爆出类似于下面的错误。 pgAdmin Runtime Enviro…...
跳表(Skip List)
跳表(Skip List) 跳表是一种用于快速查找、插入和删除的概率型数据结构,通常用于替代平衡二叉搜索树(如 AVL 树或红黑树)。跳表通过在有序链表的基础上增加多层索引,使得查找操作的平均时间复杂度降低&…...
前端实现把整个页面转成PDF保存到本地(DOM转PDF)
一、问题 遇到一个需求,就是要把整个看板页面导出成PDF用在汇报,也就是要把整个DOM生成一个PDF保存到本地。 二、解决方法 1、解决思路:使用插件 jspdf 和 html2canvas,我用的版本如下图 2、代码实现 import { jsPDF } from …...
Vue 3 学习文档(一)
最近打算做一个项目,涉及到一些前端的知识,因上一次接触前端已经是三四年前了,所以捡一些简单的功能做一下复习。 响应式函数:reactive 和 ref属性绑定:v-bind 和简写语法事件监听:v-on 和简写语法 双向绑…...
【适配】屏幕拖拽-滑动手感在不同分辨率下的机型适配
接到一个需求是类似下图的3D多房间视角,需要拖拽屏幕 问题 在做这种屏幕拖拽的时候发现,需要拖拽起来有跟手的感觉,会存在不同分辨率机型的适配问题。 即:美术调整好了机型1的手感,能做到手指按下顶层地板上下挪动&…...
广东外贸网站建设/企业查询免费
TCP可靠的传输服务并不是多余的。TCP协议是为了在不可靠的传输媒介(如互联网)上提供可靠的传输服务的。在TCP协议中,每个数据包都会被编号,并且接收方会确认收到的数据包。如果发送方没有收到确认,就会重新发送数据包。这样,即使传…...
做品牌网站哪个好用/深圳百度seo培训
1、Windows平台 在windows命令行窗口下执行: C:/>netstat -ano 我们可以知道某一端口被那个进程(对应PID)占用; 然后我们可以打开任务管理器;查看某一PID对应的进程名; 如果PID没有显示,菜单…...
asp.net获取网站地址/舆情报告范文
1.与运算符与运算符用符号“&”表示,其使用规律如下:两个操作数中位都为1,结果才为1,否则结果为0,例如下面的程序段。public class data13{public static void main(String[] args){int a129;int b128;…...
有网站代码怎么做网站/如何提高网站的搜索排名
Android自带Music播放器更新播放时间和进度条的代码在packages/appsMusic/MediaPlaybackActivity.java文件中。 源码参考:http://www.oschina.net/code/explore/android-2.2-froyo/com/android/music/MediaPlaybackActivity.java 1.在onStart()函数中,…...
广东门户网站建设/淘宝推广平台
author:skate time:2012/05/24 数据结构设计(表,索引的创建)的说明 表的设计 1.数据类型的选择 1) 数字类型:分为整数数据类型和浮点数据类型,它们之间的区别是取值范围不同,存储…...
创建网站/网页推广平台
嵌入式软件设计第7次实验报告 学号:140201234 姓名:王凯 组别:第四组 实验地点:D19 一、实验目的: 1.熟悉网线的制作(T568B标准直连线) 2.学会使用HTML语言…...