需求背景是这样的
在 GraphQL (Apollo Server)服务中对接 gRPC 服务,对接服务中包含多个在不同目录层级的 proto 文件,并且存在互相引用,以及对 google proto文件的引用。同时,在开发过程中 Apollo Server 会使用 Mock 模式,在使用 Mock 模式时不希望初始化 gRPC 客户端实例,因为本地没有 gRPC 服务。
抽象后的要求是
- 支持 动态引用 proto文件
- 支持 proto 的 import 语法
- 支持 proto 的 package 语法
- 支持指定 proto 的引入目录
- 支持直到使用时才创建实例的 延迟初始化
const grpc = require("@grpc/grpc-js");
const loader = require("@grpc/proto-loader");
const path = require("path");
function client(filename, package, service) {
    const definition = loader.loadSync(`${filename}`, {
        keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true,
        // CAUTION: proto files import path relative from 2 places
        includeDirs: [
            path.join(__dirname, "./current/path"),
            path.join(__dirname, "./path/to/proto/root"),
            path.join(__dirname, "./path/to/other/extra"),
        ],
    });
    const proto = grpc.loadPackageDefinition(definition);
    let Client = proto;
    for (let i of [...package.split("."), service]) {
        if (i) Client = Client[i];
    }
    return new Client(
        process.env.GRPC_PEDESTAL_URI,
        grpc.credentials.createInsecure()
    );
}
let _clients = {};
function proxy(file, package, service) {
    if (!file || !package)
        throw Error("Target must contain file and service field");
    function getCachedlient() {
        const K = file + package;
        if (!_clients[K]) _clients[K] = client(file, package, service);
        return _clients[K];
    }
    const handler = {
        get: function (target, prop, receiver) {
            let client = getCachedlient();
            return function () {
                let [params, callback] = arguments;
                if (callback) {
                    client[prop](...arguments);
                } else {
                    return new Promise((resolve, reject) => {
                        client[prop](params, (err, res) => {
                            if (err) reject(err);
                            else resolve(res);
                        });
                    });
                }
            };
        },
    };
    return new Proxy({ file, package, service }, handler);
}
const clients = {
    ClientA: proxy(
        "path/to/a.proto",
        "EntryService"
    ),
    ClientB: proxy(
        "path/to/some_within_a_package.proto",
        "com.company.project.demo.v1",
        "EntryService"
    ),
};
module.exports = clients;
注意事项:
- 加载 proto 文件的 includeDirs 要与 proto 文件的保存目录匹配
- proto 的 package 需要手动指定,否则无法使用
- 延迟加载的机制,即使是在变量被引用后,也不会初始化,直到真正被当做方法使用时才会初始化
参考内容