在 React 项目中使用 Protobuf 协议
前言
本文总结了在 React App 中处理 ProtoBuf 协议的一种方式, 同时包含 React 自定义 Hook, axios interceptors 等内容, 共同组成了前后端交互的相关功能.
跟随本文的步骤, 可以完整的创建一个独立的示例 App.
本文主要按照以下几个步骤进行:
- 创建 React App
- 安装依赖
- 创建自定义 Hook
- 创建并配置 axios 实例
- 编写 Proto 定义
- 创建自定义组件组件
- 为 axios 实例添加 interceptors 函数
- 将组件添加到 App
- 修改 index.js
- 启动项目
本文假设你已经有了可以正常运行的 Node.js 环境. 如果还没有, 你可以从 官网 进行下载安装.
创建 React App
使用 npm 创建一个名为 proto-test 的项目:
1 | $ npx create-react-app proto-test |
按照文档的推荐, 无需预先安装 create-react-app 工具, 并且建议先卸载全局安装的 create-react-app
创建项目并安装依赖的过程可能会持续几分钟, 请耐心等待
安装依赖
首先进入 proto-test 目录
1 | $ cd proto-test |
由于 React 18 版本相关依赖存在一些原因未知的问题, 这里使用 React 17 版本.
通过修改 package.json 文件指定依赖版本, 将 package.json 文件中的 dependencies
值改为如下内容:
1 | "dependencies": { |
保存 package.json 文件后, 使用如下命令安装全部依赖
1 | $ npm i |
注意: Protobufjs 文档中给出的相关示例, 并不完全适用于 react app, 原文档仅供参考.
创建自定义 Hook
首先, 我们在项目的 src 目录下创建一个名为 hooks 子目录, 并在其中创建一个名为 useAxios.js 的文件.
1 | $ cd src |
使用文本编辑器打开 useAxios.js 文件, 向其中粘贴如下代码:
1 | import { useState, useEffect } from 'react'; |
这段代码主要实现的功能如下:
- 使用箭头函数语法声明一个函数, 并赋值给
useAxios
变量; - 对参数
configObj
进行解构赋值(Destructuring Assignment), 获得axiosInstance
,method
,url
,requestConfig
四个变量, 并为requestConfig
设置默认值. - 分别使用
useState
Hook 设置了三个局部状态变量response
,error
,loading
. - 在
useEffect
Hook 中使用 axios 执行请求, 并更新局部状态变量. 返回一个使用controller
清除状态的函数. - 返回
response
,error
,loading
三个变量. - 使用
export default
对外暴露刚刚声明的useAxios
变量.
创建并配置 axios 实例
在 src 目录下创建一个名为 apis 的子目录, 并在其中创建一个名为 base.js 的文件
1 | $ mkdir apis |
向 base.js 中粘贴如下代码:
1 | import axios from 'axios'; |
在这段代码中, 实现的功能如下:
- 设置了全局常量
BASE_URL
, 这是你将要请求的接口 url 的通用前缀. - 创建了一个 axios 实例并赋值给
instance
变量, 并在传入的 config 参数对象中设置请求头的Accept
字段和响应实体将要使用的数据类型arraybuffer
. - 使用
export default
对外暴露刚刚创建的 axios 实例instance
.
编写 Proto 定义
在 src 目录中创建一个名为 protos 的子目录, 并在其中创建一个名为 ExampleResponse.proto 的文件.
1 | $ mkdir protos |
在 ExampleResponse.proto 文件中, 粘贴如下代码:
1 | syntax = "proto3"; |
在这段代码中, 你定义了:
- 一个
ExampleResponse
类型的 message, 其中包含一个自定义枚举类型的status
字段和一个 string 类型的msg
字段. - 将 message 名称与文件名设计为同名, 以便后续可以更方便的加载.
你还需要实现一个使用这个消息定义的接口服务, 使用相同的 proto 定义对数据进行编码
创建自定义组件
在 src 目录中, 创建名为 components 的子目录, 并在其中创建一个名为 ExampleComponent.js 的文件.
1 | $ mkdir components |
打开 ExampleComponent.js 文件, 向其中粘贴如下代码:
1 | import useAxios from '../hooks/useAxios'; |
在这段代码中, 实现的功能如下:
- 导入了前几步声明的
useAxios
Hook, axios 实例, 以及 ExampleResponse.proto 文件. - 在 App 组件中使用
useAxios
Hook 发送请求, 在参数对象中指定要求的参数值. 在requestConfig
中添加一个自定义字段pb
, 里面指定响应所使用的 proto 文件. url 的值和之前声明的 BASE_URL 拼接到一起组成了完整的 api 地址. - 返回组件, 根据
response
,error
和loading
的值决定页面中展示的数据.
为 axios 实例添加 interceptors 函数
在 apis/base.js 文件中, export default instance
语句的上方, 粘贴如下代码:
1 | /** |
在这段代码中, 实现的功能如下:
- 声明了一个
omit
函数, 用来移除对象中指定的键值对. - 声明了一个
getProtoName
函数, 用于从导入的 proto 文件中获取文件名, 用于加载 message 类型. - 使用
instance.interceptors.response.use
为 axios 实例添加一个拦截器, 用于在response
返回给请求者之前对其进行处理. 这里使用了 async 函数, 主要是为了处理protobuf.load
相关的逻辑. 这里与文档中的用法稍有区别. - 在拦截器函数内部, 使用对象解构的方式获得
response.config
和response.config.pb
对象, 分别赋值给config
和pb
变量.pb
对象是在组件中发起请求时, 添加到 requestConfig 中的自定义对象, 其中包含响应数据所对应的 proto 文件. - 使用 protobufjs 对响应数据进行处理, 在返回前对数据进行解码.
有两点需要注意:
在 protobufjs 的文档中, 示例代码使用
protobuf.load
加载 proto 文件时直接使用字符串类型的文件名作为参数, 但是在 react 的项目中, 实际的文件路径和文件系统中的路径并不一致, 所以直接使用文件系统中的文件路径做参数会导致异常. 这里先在组件中把 proto 文件导入为模块, 再使用模块路径进行加载可以正常运行.在使用
msgType.decode
对响应实体数据进行解码时, 要求参数使用Uint8Array
类型, 首先需要进行一次类型转换. 其次, 使用Uint8Array.from
进行转换时, 无返回值, 会导致后续没有数据. 这里需要使用new Uint8Array
的方式进行类型转换.
将组件添加到 App
将 src/app.js 中的代码替换为如下内容:
1 | import * as React from 'react'; |
在这段代码中, 实现的功能如下:
- 声明了 App 组件, 这个组件将作为整个应用的基础
- 导入刚刚创建的
ExampleComponent
组件, 并将其添加到 App 中.
修改 index.js
打开 src/index.js, 使用如下代码替换文件中已有的内容:
1 | import React from 'react'; |
这段代码, 实现了如下功能:
- 渲染 App 组件
启动项目
打开命令行, 进入到项目的根目录, 使用如下命令启动项目:
1 | $ npm start |
项目启动完成后, 就可以看到后端返回的 Protobuf 协议数据, 解码后展示在了页面中.
完整代码
示例代码中使用的 api 需要替换为你自己实现的后端 api
参考资料
[1] [YouTube]Use Axios with React Hooks for Async-Await Requests
[2] [Issue]illegal token ‘<’ (/demo.proto, line 1)
[3] [Issue]Uncaught Error: illegal buffer