在 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设置默认值. - 分别使用
useStateHook 设置了三个局部状态变量response,error,loading. - 在
useEffectHook 中使用 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'; |
在这段代码中, 实现的功能如下:
- 导入了前几步声明的
useAxiosHook, axios 实例, 以及 ExampleResponse.proto 文件. - 在 App 组件中使用
useAxiosHook 发送请求, 在参数对象中指定要求的参数值. 在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