使用Node,Express和gRPC创建CRUD API

2020年12月30日11:06:40 发表评论 33 次浏览

本文概述

速度在网络世界中变得越来越有价值。

发布新版本软件, 框架和库的开发人员当然已经花了大量时间在减少加载时间, 请求处理和资源消耗方面。

HTTP / 2, 例如, 它是通过无数次优化而诞生的, 这些优化使网络比以往任何时候都更强大, 更快, 更轻巧。

RPC(代表"远程过程调用")是一种众所周知的获取牵引力的方法, 当你需要进行远程或分布式操作时。在企业服务器时代, 以及用于设置内容的复杂代码量, 它曾经占据统治地位。

经过多年的孤立, Google重新设计了它并为它提供了新的亮点。

gRPC是可以在任何环境中运行的现代开源高性能RPC框架。

它可以通过可插拔的支持来有效地连接数据中心内和跨数据中心的服务, 以实现负载平衡, 跟踪, 运行状况检查和身份验证。

它也适用于分布式计算的最后一英里, 以将设备, 移动应用程序和浏览器连接到后端服务。

它具有HTTP / 2, 跨平台和开放源代码的支持。它的尺寸也很紧凑。

gRPC可与Java, Go, Ruby, Python等许多编程语言一起使用。

我们为制作了一个自定义演示.
不完全是。点击这里查看.

使用Node,Express和gRPC创建CRUD API1

继续检查他们的官方文档链接(以及GitHub页面), 以查看是否有你的支持。

即使你的语言未在此处列出, 你也可以使用网络功能在Docker映像中

它的工作流程如下所示:

gRPC

gRPC工作流程

整个体系结构基于已知的客户端-服务器结构。

gRPC客户端应用程序可以直接向服务器应用程序发出请求。客户端和服务器都包含一个通用的接口(例如合同), 在该接口中, 它可以确定每种操作将要具有的方法, 类型和返回值。

服务器确保接口将由其服务提供, 而客户端具有存根以保证方法相同。

它还使用协议缓冲区例如, 序列化和反序列化请求和响应数据, 而不是JSON或XML。

协议缓冲区是Google的与语言无关, 与平台无关的可扩展机制, 用于对结构化数据进行序列化(例如XML), 但更小, 更快, 更简单。

你可以定义如何一次构造数据, 然后可以使用生成的特殊源代码轻松地以每种受支持的语言在各种数据流之间来回写入和读取结构化数据。

首先, 你需要创建和定义protobuf文件, 该文件将包含在协议本身指定的接口定义语言下创建的代码(稍后会详细介绍)。

有了文件, 你可以通过以下方式进行编译协议编译为所需的语言代码。

整个过程都是在后台进行的, 所以请放心, 你不会在周围看到很多样板代码。最后, 连同生成的代码, 你可以转到服务器和客户端的实现。

无需想象, 我们将构建一个具有Bootstrap接口的功能齐全的CRUD API应用程序, 以管理内存中客户列表的操作(由服务器应用程序管理)。

这就是我们的应用程序最终的外观:

Logrocket客户列出CRUD

客户的CRUD申请

设定

本教程的要求非常简单:

  • Node.js和npm(最新版本)
  • 你选择的IDE

为了简单起见, 我们将不使用任何类型的数据库-项目列表将保留在服务器应用程序的内存中。

这将非常接近地模拟数据库的使用, 因为当服务器启动时数据将在那里, 而客户端可以根据需要重新启动多次。随意合并所需的任何框架或功能。

接下来, 在你选择的文件夹中, 创建以下文件夹和文件结构:

项目结构

项目的结构

你还可以选择分别创建客户端和服务器应用程序。

我们将它们放在一起以简化最终结构。

现在, 在命令行的根文件夹中运行以下命令:

npm install --save grpc @grpc/proto-loader uuid express hbs body-parser

前两次安装将处理gRPC服务器, 并将protobuf文件的负载加载到客户端和服务器代码的实现中。Uuid这对于为我们的客户创建随机哈希ID很有用, 但是你也可以使用数字来简化代码(例如, 通过这种方式, 你的代码已经准备好切换到MongoDB)。

你可能想知道, 如果我们要在不同协议下开发API, 为什么在这里使用Express(用于HTTP处理)。

表现我们将只为路由系统提供服务。每个CRUD操作都需要到达客户端(顺便说一下, 这是一个HTTP服务器), 该客户端又将通过gRPC与服务器应用程序进行通信。

尽管你可以从网页调用gRPC方法, 但由于存在很多缺点, 因此我不建议你使用它。

记住, gRPC是为了加快后端的速度而设计的, 例如从微服务到另一个。为了提供给首页, 移动应用程序或任何其他类型的GUI, 你必须调整架构。

最后, 我们有车把用于页面模板(我们不会在此处介绍其详细信息, 但是你可以将EJS或其他任何模板系统用于Node应用), 以及人体解析器用于在处理程序之前在中间件中转换传入的请求主体, 该处理程序可在需求主体属性。

访问请求参数将使我们的生活更加轻松。

你的最后package.json文件应如下所示:

{
  "name": "notlogy_customers_grpc", "version": "1.0.0", "description": "notlogy CRUD with gRPC and Node", "main": "server.js", "scripts": {
    "start": "node server/server.js"
  }, "author": "Diogo Souza", "license": "MIT", "dependencies": {
    "@grpc/proto-loader": "^0.5.3", "body-parser": "^1.18.3", "express": "^4.17.1", "grpc": "^1.24.2", "hbs": "^4.1.0", "uuid": "^7.0.2"
  }
}

服务器

让我们转到代码, 从我们的protobuf文件开始, 客户协议:

syntax = "proto3";

service CustomerService {
    rpc GetAll (Empty) returns (CustomerList) {}
    rpc Get (CustomerRequestId) returns (Customer) {}
    rpc Insert (Customer) returns (Customer) {}
    rpc Update (Customer) returns (Customer) {}
    rpc Remove (CustomerRequestId) returns (Empty) {}
}

message Empty {}

message Customer {
    string id = 1;
    string name = 2;
    int32 age = 3;
    string address = 4;
}

message CustomerList {
    repeated Customer customers = 1;
}

message CustomerRequestId {
    string id = 1;
}

第一行指出我们将使用的protobuf版本-在本例中为最新版本。

内容的语法重新组合了许多JSON。该服务是我们已经讨论过的接口合同。在这里, 你将放置每个gRPC调用的方法名称, 参数和返回类型。

如果不是原始类型, 则必须通过message关键字声明类型。请参考docs查看所有允许的类型。

邮件的每个属性都必须接收一个数字值, 该值代表该属性在堆栈中的顺序, 从1开始。

最后, 对于数组, 你需要在声明属性之前使用重复关键字。

有了原型, 让我们创建我们的server.js代码如下:

const PROTO_PATH = "./customers.proto";

var grpc = require("grpc");
var protoLoader = require("@grpc/proto-loader");

var packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true, longs: String, enums: String, arrays: true
});

var customersProto = grpc.loadPackageDefinition(packageDefinition);

const { v4: uuidv4 } = require("uuid");

const server = new grpc.Server();
const customers = [
    {
        id: "a68b823c-7ca6-44bc-b721-fb4d5312cafc", name: "John Bolton", age: 23, address: "Address 1"
    }, {
        id: "34415c7c-f82d-4e44-88ca-ae2a1aaa92b7", name: "Mary Anne", age: 45, address: "Address 2"
    }
];

server.addService(customersProto.CustomerService.service, {
    getAll: (_, callback) => {
        callback(null, { customers });
    }, get: (call, callback) => {
        let customer = customers.find(n => n.id == call.request.id);

        if (customer) {
            callback(null, customer);
        } else {
            callback({
                code: grpc.status.NOT_FOUND, details: "Not found"
            });
        }
    }, insert: (call, callback) => {
        let customer = call.request;
        
        customer.id = uuidv4();
        customers.push(customer);
        callback(null, customer);
    }, update: (call, callback) => {
        let existingCustomer = customers.find(n => n.id == call.request.id);

        if (existingCustomer) {
            existingCustomer.name = call.request.name;
            existingCustomer.age = call.request.age;
            existingCustomer.address = call.request.address;
            callback(null, existingCustomer);
        } else {
            callback({
                code: grpc.status.NOT_FOUND, details: "Not found"
            });
        }
    }, remove: (call, callback) => {
        let existingCustomerIndex = customers.findIndex(
            n => n.id == call.request.id
        );

        if (existingCustomerIndex != -1) {
            customers.splice(existingCustomerIndex, 1);
            callback(null, {});
        } else {
            callback({
                code: grpc.status.NOT_FOUND, details: "Not found"
            });
        }
    }
});

server.bind("127.0.0.1:30043", grpc.ServerCredentials.createInsecure());
console.log("Server running at http://127.0.0.1:30043");
server.start();

由于它是服务器, 因此看起来很像Express代码的结构。你有一个IP和一个端口, 然后开始一些操作。

一些要点:

首先, 将原型文件路径导入常量。

然后, 要求都grpc和@ grpc / proto-loader包。他们是使魔术成真的人。为了将原型转换成JavaScript对象, 你需要首先设置其包定义。protoLoader将通过接收原型文件所在的路径作为第一个参数, 并将设置属性作为第二个参数来完成此任务。

拥有包定义对象后, 将其传递给loadPackageDefinition的功能grpc对象, 然后将其退还给你。然后, 你可以通过创建服务器服务器()功能。

的顾客数组是我们的内存数据库。

我们已经与两个客户进行了初始化, 因此你可以在应用启动时看到一些数据。在服务器上, 我们需要告诉服务器对象将要处理的服务(在我们的示例中, 客户服务我们已经在原始文件中创建了)。每个操作必须分别将其名称与原始名称匹配。它们的代码很简单, 也很简单, 所以继续看一下它们。

最后, 将服务器连接绑定到所需的IP和端口并启动它。的bind()函数将身份验证对象作为第二个参数, 但是为简单起见, 我们会不安全地使用它, 你可能会注意到(建议不要在生产环境中使用它)。

服务器已完成。很简单, 不是吗?现在, 你可以通过发出以下命令来启动它:

npm start

但是, 由于你需要了解服务器所服务的protobuf合同的适当客户, 因此无法对其进行测试。

客户端

让我们现在开始构建客户端应用程序, 从client.js代码如下:

const PROTO_PATH = "../customers.proto";

const grpc = require("grpc");
const protoLoader = require("@grpc/proto-loader");

var packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true, longs: String, enums: String, arrays: true
});

const CustomerService = grpc.loadPackageDefinition(packageDefinition).CustomerService;
const client = new CustomerService(
    "localhost:30043", grpc.credentials.createInsecure()
);

module.exports = client;

该文件将专门处理我们与gRPC服务器的通信。

请注意, 它的初始结构与服务器文件中的结构完全相同, 因为相同的gRPC对象处理客户端和服务器实例。

唯一的区别是没有这样的方法客户().

我们所需要做的就是通过相同的IP和端口加载程序包定义并创建一项新服务(与我们在服务器中创建的服务相同)。如果设置了凭据, 则第二个参数也必须满足设置。

而已。

要使用此服务合同, 我们首先需要实施Express代码。所以, 在index.js文件, 插入以下内容:

const client = require("./client");

const path = require("path");
const express = require("express");
const bodyParser = require("body-parser");

const app = express();

app.set("views", path.join(__dirname, "views"));
app.set("view engine", "hbs");

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.get("/", (req, res) => {
    client.getAll(null, (err, data) => {
        if (!err) {
            res.render("customers", {
                results: data.customers
            });
        }
    });
});

app.post("/save", (req, res) => {
    let newCustomer = {
        name: req.body.name, age: req.body.age, address: req.body.address
    };

    client.insert(newCustomer, (err, data) => {
        if (err) throw err;

        console.log("Customer created successfully", data);
        res.redirect("/");
    });
});

app.post("/update", (req, res) => {
    const updateCustomer = {
        id: req.body.id, name: req.body.name, age: req.body.age, address: req.body.address
    };

    client.update(updateCustomer, (err, data) => {
        if (err) throw err;

        console.log("Customer updated successfully", data);
        res.redirect("/");
    });
});

app.post("/remove", (req, res) => {
    client.remove({ id: req.body.customer_id }, (err, _) => {
        if (err) throw err;

        console.log("Customer removed successfully");
        res.redirect("/");
    });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log("Server running at port %d", PORT);
});

导入后要求, 创建了应用程式从表现()功能并设置每个CRUD HTTP功能, 剩下的只是接口合同提供的每个操作的调用。

另请注意, 对于所有这些, 我们正在从请求中恢复输入值身体(由人体解析器)。

别忘了每个客户函数必须与原型文件中定义的名称完全相同。

最后但并非最不重要的是, 这是客户文件:

<html lang="en">

<head>
    <meta charset="utf-8">
    <title>notlogy CRUD with gRPC and Node</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
        integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <style>
        .notlogy {
            background-color: #764abc;
            color: white;
        }
    </style>
</head>

<body>
    <div class="container">
        <div class="py-5 text-center">
            <img class="d-block mx-auto mb-4"
                src="https://blog.notlogy.com/wp-content/uploads/2020/01/notlogy-blog-logo.png" alt="使用Node,Express和gRPC创建CRUD API" alt="Logo"
                height="72">
            <h2>Customer's List</h2>
            <p class="lead">Example of CRUD made with Node.js, Express, Handlebars and gRPC</p>
        </div>

        <table class="table" id="customers_table">
            <thead>
                <tr>
                    <th>Customer ID</th>
                    <th>Customer Name</th>
                    <th>Age</th>
                    <th>Address</th>
                    <th>Action</th>
                </tr>
            </thead>
            <tbody>
                {{#each results}}
                <tr>
                    <td>{{ id }}</td>
                    <td>{{ name }}</td>
                    <td>{{ age }} years old</td>
                    <td>{{ address }}</td>
                    <td>
                        <a href="javascript:void(0);" class="btn btn-sm edit notlogy" data-id="{{ id }}"
                            data-name="{{ name }}" data-age="{{ age }}" data-address="{{ address }}">Edit</a>
                        <a href="javascript:void(0);" class="btn btn-sm btn-danger remove" data-id="{{ id }}">Remove</a>
                    </td>
                </tr>
                {{else}}
                <tr>
                    <td colspan="5" class="text-center">No data to display.</td>
                </tr>
                {{/each}}
            </tbody>
        </table>
        <button class="btn btn-success float-right" data-toggle="modal" data-target="#newCustomerModal">Add New</button>
    </div>

    <!-- New Customer Modal -->
    <form action="/save" method="post">
        <div class="modal fade" id="newCustomerModal" role="dialog">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">New Customer</h4>
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <div class="form-group">
                            <input type="text" name="name" class="form-control" placeholder="Customer Name"
                                required="required">
                        </div>

                        <div class="form-group">
                            <input type="number" name="age" class="form-control" placeholder="Age" required="required">
                        </div>

                        <div class="form-group">
                            <input type="text" name="address" class="form-control" placeholder="Address"
                                required="required">
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="submit" class="btn notlogy">Create</button>
                    </div>
                </div>
            </div>
        </div>
    </form>

    <!-- Edit Customer Modal -->
    <form action="/update" method="post">
        <div class="modal fade" id="editCustomerModal" role="dialog">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">Edit Customer</h4>
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <div class="form-group">
                            <input type="text" name="name" class="form-control name" placeholder="Customer Name"
                                required="required">
                        </div>

                        <div class="form-group">
                            <input type="number" name="age" class="form-control age" placeholder="Age"
                                required="required">
                        </div>

                        <div class="form-group">
                            <input type="text" name="address" class="form-control address" placeholder="Address"
                                required="required">
                        </div>
                    </div>
                    <div class="modal-footer">
                        <input type="hidden" name="id" class="customer_id">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="submit" class="btn notlogy">Update</button>
                    </div>
                </div>
            </div>
        </div>
    </form>

    <!-- Remove Customer Modal -->
    <form id="add-row-form" action="/remove" method="post">
        <div class="modal fade" id="removeCustomerModal" role="dialog" aria-labelledby="myModalLabel">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title"></h4>Remove Customer</h4>
                        <button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
                    </div>
                    <div class="modal-body">
                        Are you sure?
                    </div>
                    <div class="modal-footer">
                        <input type="hidden" name="customer_id" class="form-control customer_id_removal"
                            required="required">
                        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                        <button type="submit" class="btn notlogy">Remove</button>
                    </div>
                </div>
            </div>
        </div>
    </form>

    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
        integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
        crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
        integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
        crossorigin="anonymous"></script>
    <script>
        $(document).ready(function () {
            $('#customers_table').on('click', '.edit', function () {
                $('#editCustomerModal').modal('show');

                $('.customer_id').val($(this).data('id'));
                $('.name').val($(this).data('name'));
                $('.age').val($(this).data('age'));
                $('.address').val($(this).data('address'));
            }).on('click', '.remove', function () {
                $('#removeCustomerModal').modal('show');

                $('.customer_id_removal').val($(this).data('id'));
            });
        });
    </script>
</body>

</html>

这有点冗长, 尤其是因为我决定通过Bootstrap模式将整个CRUD用户界面创建到同一页面中, 而不是重定向和设置很多不同的页面。

在页面的开头和结尾, 我们分别找到了Bootstrap和jQuery的CSS和JS文件的导入。

主表通过以下方式使用Handlebars foreach指令:

{{#each results}}
…
{{else}}
…
{{/each}}

的其他当列表中没有可用元素时, 此处可帮助配置文本。关于编辑和删除操作的链接, 我们正在设置HTML数据属性以帮助打开模态。

每次我们打开模式修改时, 它的每个输入都必须用该行值的对应值填充。删除操作也是如此, 即使在这里我们只需要id。

在第一个div的结尾, 我们可以看到用于添加新客户的链接, 这也触发了相应的模式。

在下面, 有三种模式。

它们彼此非常相似, 因为它们仅保留HTML结构。

该逻辑实际上将放置在HTML末尾的JavaScript部分中。

在这里, 我们使用jQuery打开模式本身, 并简化了更改值的工作(通过值功能)的输入数据属性值。

完成了现在, 你可以通过发出以下命令在另一个命令行窗口中启动客户端:

node index

然后, 在服务器也启动的情况下, 转到http://本地主机:3000 /并测试一下。

总结

你可以找到该项目的最终源代码这里.

现在, 你可以将其部署到云或生产服务器, 或者在你自己的项目中以适度的POC开始, 以查看与REST API相比, 其执行速度如何。

但是gRPC可以做更多的事情。你可以插入身份验证以使其更加安全, 超时, 双向流, 强大的错误处理机制, 通道等。

请务必阅读docs检查其更多功能。

只有200 监视生产中失败和缓慢的网络请求

部署基于节点的Web应用程序或网站很容易。确保你的Node实例继续为你的应用程序提供资源是一件很困难的事情。如果你希望确保成功完成对后端或第三方服务的请求,

尝试notlogy

.

LogRocket网络请求监控

https://notlogy.com/signup/

日志火箭就像Web应用程序的DVR一样, 实际上记录了你网站上发生的一切。无需猜测问题发生的原因, 你可以汇总并报告有问题的网络请求, 以快速了解根本原因。

notlogy用你的应用程序记录基线性能计时, 例如页面加载时间, 到第一个字节的时间, 缓慢的网络请求, 并记录Redux, NgRx和Vuex的操作/状态。

免费开始监控

.

一盏木

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: