From 8cfccbe854b4fd26c0a812b7f03316a34b6db435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8A=B3=E8=B5=84=E8=9C=80=E9=81=93=E5=B1=B1?= <1493170339@qq.com> Date: Sun, 28 Apr 2024 23:40:17 +0800 Subject: [PATCH 1/3] complete Dubbo for Node.js ->Getting started & Implementing services pages --- docs/guide/dubboForNode/GettingStarted.md | 296 +++++++++++++++++- .../dubboForNode/ImplementingServices.md | 233 ++++++++++++++ 2 files changed, 528 insertions(+), 1 deletion(-) diff --git a/docs/guide/dubboForNode/GettingStarted.md b/docs/guide/dubboForNode/GettingStarted.md index aa0c9abd..45d5963f 100644 --- a/docs/guide/dubboForNode/GettingStarted.md +++ b/docs/guide/dubboForNode/GettingStarted.md @@ -1 +1,295 @@ -# GettingStarted +# Getting started + +Dubbo-Node is a library for serving Dubbo, gRPC, and gRPC-Web compatible HTTP APIs using Node.js. It brings the Dubbo Protocol to Node with full TypeScript compatibility and support for all four types of remote procedure calls: unary and the three variations of streaming. + +This ten-minute walkthrough helps you create a small Dubbo service in Node.js. It demonstrates what you'll be writing by hand, what Connect generates for you, and how to call your new API. + + +# Prerequisites +We'll set up a project from scratch and then augment it to serve a new endpoint. + +- You'll need [Node.js](https://nodejs.org/en/download) installed - we recommend the most recent long-term support version (LTS). +- We'll use the package manager `npm`, but we are also compatible with `yarn` and `pnpm`. +- We'll also use [cURL](https://curl.se/). It's available from Homebrew and most Linux package managers. + + +# Project setup + +Let's initialize a project with TypeScript, and install some code generation tools: + +```shell +mkdir dubbo-example +cd dubbo-example +npm init -y +npm install typescript tsx +npx tsc --init +npm install @bufbuild/buf @bufbuild/protoc-gen-es @bufbuild/protobuf @apachedubbo/protoc-gen-apache-dubbo-es @apachedubbo/dubbo +``` + +# Define a service +First, we need to add a Protobuf file that includes our service definition. For this tutorial, we are going to construct a unary endpoint for a service that is a stripped-down implementation of [ELIZA](https://en.wikipedia.org/wiki/ELIZA), the famous natural language processing program. + +```shell +mkdir -p proto && touch proto/eliza.proto +``` + +Open up the above file and add the following service definition: + +``` +syntax = "proto3"; + +package connectrpc.eliza.v1; + +message SayRequest { + string sentence = 1; +} + +message SayResponse { + string sentence = 1; +} + +service ElizaService { + rpc Say(SayRequest) returns (SayResponse) {} +} +``` + + +# Generate code + +We're going to generate our code using [Buf](https://www.npmjs.com/package/@bufbuild/buf), a modern replacement for Google's protobuf compiler. We installed Buf earlier, but we also need a configuration file to get going. (If you'd prefer, you can skip this section and use `protoc` instead — `protoc-gen-apache-dubbo-es` behaves like any other plugin.) + +First, tell Buf how to generate code with a `buf.gen.yaml` file: + +```yaml +version: v1 +plugins: + - plugin: es + opt: target=ts + out: gen + - plugin: dubbo-es + opt: target=ts + out: gen +``` + +With this file in place, you can generate code from the schema in the `proto` directory: + +```shell +npx buf generate proto +``` + +You should now see two generated TypeScript files: + +```markdown{3-5} +├── buf.gen.yaml +├── gen +│   ├── eliza_dubbo.ts +│   └── eliza_pb.ts +├── node_modules +├── package-lock.json +├── package.json +├── proto +│   └── eliza.proto +└── tsconfig.json +``` + +Next, we are going to use these files to implement our service. + + +# Implement the service + +We defined the `ElizaService` - now it's time to implement it, and register it with the `DubboRouter`. First, let's create a file where we can put the implementation: + +Create a new file `dubbo.ts` with the following contents: + +```tsx +import type { ConnectRouter } from "@apachedubbo/dubbo"; +import { ElizaService } from "./gen/eliza_dubbo"; + +export default (router: DubboRouter) => + // registers dubborpc.eliza.v1.ElizaService + router.service(ElizaService, { + // implements rpc Say + async say(req) { + return { + sentence: `You said: ${req.sentence}` + } + }, + }); +``` + +That's it! There are many other alternatives to implementing a service, and you have access to a context object for headers and trailers, but let's keep it simple for now. + + +# Start a server + +Dubbo services can be plugged into vanilla Node.js servers, [Next.js](https://nextjs.org/), [Express](https://expressjs.com/), or [Fastify](https://fastify.dev/). We are going to use Fastify here. Let's install it, along with our plugin for Fastify: + +```shell +npm install fastify @apachedubbo/dubbo-node @apachedubbo/dubbo-fastify +``` + +Create a new file `server.ts` with the following contents: + +```tsx +import { fastify } from "fastify"; +import { fastifyDubboPlugin } from "@apachedubbo/dubbo-fastify"; +import routes from "./connect"; + +async function main() { + const server = fastify(); + await server.register(fastifyDubboPlugin, { + routes, + }); + server.get("/", (_, reply) => { + reply.type("text/plain"); + reply.send("Hello World!"); + }); + await server.listen({ host: "localhost", port: 8080 }); + console.log("server is listening at", server.addresses()); +} +// You can remove the main() wrapper if you set type: module in your package.json, +// and update your tsconfig.json with target: es2017 and module: es2022. +void main(); +``` + +Congratulations. Your endpoint is ready to go! You can start your server with: + +```shell +npx tsx server.ts +``` + +# Make requests + +The simplest way to consume your new API is an HTTP/1.1 POST with a JSON payload. If you have a recent version of cURL installed, it's a one-liner: + +```shell +curl \ + --header 'Content-Type: application/json' \ + --data '{"sentence": "I feel happy."}' \ + http://localhost:8080/dubborpc.eliza.v1.ElizaService/Say +``` + +--- + +```markdown +Output +{"sentence":"You said: I feel happy."} +``` + +You can also make requests using a Dubbo client. Create a new file client.ts with the following contents: + +```tsx +import { createPromiseClient } from "@apachedubbo/dubbo"; +import { ElizaService } from "./gen/eliza_dubbo"; +import { createDubboTransport } from "@apachedubbo/dubbo-node"; + +const transport = createDubboTransport({ + baseUrl: "http://localhost:8080", + httpVersion: "1.1" +}); + +async function main() { + const client = createPromiseClient(ElizaService, transport); + const res = await client.say({ sentence: "I feel happy." }); + console.log(res); +} +void main(); +``` + +With your server still running in a separate terminal window, you can now run your client: + +```shell +npx tsx client.ts +``` + +```markdown +Output +SayResponse { sentence: 'You said: I feel happy.' } +``` + +Congratulations — you've built your first Connect service! 🎉 + + +# From the browser + +You can run the same client from a web browser, just by swapping out the Transport: + +```tsx +import { createPromiseClient } from "@apachedubbo/dubbo"; +import { ElizaService } from "./gen/eliza_dubbo"; +import { createDubboTransport } from "@apachedubbo/dubbo-web"; + +const transport = createDubboTransport({ + baseUrl: "http://localhost:8080", + // Not needed. Web browsers use HTTP/2 automatically. + // httpVersion: "1.1" +}); + +async function main() { + const client = createPromiseClient(ElizaService, transport); + const res = await client.say({ sentence: "I feel happy." }); + console.log(res); +} +void main(); +``` + + +# Use the gRPC protocol instead of the Dubbo protocol + +On Node.js, we support three protocols: + +* The gRPC protocol that is used throughout the gRPC ecosystem. +* The gRPC-Web protocol used by [grpc/grpc-web](https://github.com/grpc/grpc-web), allowing servers to interop with `grpc-web` frontends without the need for an intermediary proxy (such as Envoy). +* The new [Dubbo protocol](https://cn.dubbo.apache.org/zh-cn/overview/reference/protocols/), a simple, HTTP-based protocol that works over HTTP/1.1 or HTTP/2. It takes the best portions of gRPC and gRPC-Web, including streaming, and packages them into a protocol that works equally well in browsers, monoliths, and microservices. The Connect protocol is what we think the gRPC protocol should be. By default, JSON- and binary-encoded Protobuf is supported. + +So far, we have been using the `http://` scheme in our examples. We were not using TLS (Transport Layer Security). If you want to use gRPC and browser clients during local development, you need TLS. + +Actually, that only takes a minute to set up! We will use `mkcert` to make a certificate. If you don't have it installed yet, please run the following commands: + +```shell +brew install mkcert +mkcert -install +mkcert localhost 127.0.0.1 ::1 +export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" +``` + +If you don't use macOS or `brew`, see the [mkcert docs](https://github.com/FiloSottile/mkcert#installation) for instructions. You can copy the last line to your `~/.zprofile` or `~/.profile`, so that the environment variable for Node.js is set every time you open a terminal. + +Let's update our `server.ts` to use this certificate: + +```tsx{4,8-12,17} +import { fastify } from "fastify"; +import { fastifyDubboPlugin } from "@apachedubbo/dubbo-fastify"; +import routes from "./connect"; +import { readFileSync } from "fs"; + +async function main() { + const server = fastify({ + http2: true, + https: { + key: readFileSync("localhost+2-key.pem", "utf8"), + cert: readFileSync("localhost+2.pem", "utf8"), + } + }); + await server.register(fastifyDubboPlugin, { + routes, + }); + await server.listen({ host: "localhost", port: 8443 }); + console.log("server is listening at", server.addresses()); +} +void main(); +``` + +That's it! After you restarted the server, you can still open [https://localhost:8443/](https://localhost:8443/) in your browser, but along with gRPC-Web and Connect, any gRPC client can access it too. Here's an example using `buf curl`: + +```shell +npx buf curl --protocol grpc --schema . -d '{"sentence": "I feel happy."}' \ + https://localhost:8443/dubborpc.eliza.v1.ElizaService/Say +``` + +In your `client.ts`, update the URL and use HTTP version `2` and you're set. It will pick up the locally-trusted certificate authority, just like your web browser and other apps. + + +# So what? + +With just a few lines of hand-written code, you've built a real API server that supports both the gRPC and Dubbo protocols. Unlike a hand-written REST service, you didn't need to design a URL hierarchy, hand-write request and response objects, or parse typed values out of query parameters. More importantly, your users got an idiomatic, type-safe client without any extra work on your part. diff --git a/docs/guide/dubboForNode/ImplementingServices.md b/docs/guide/dubboForNode/ImplementingServices.md index 20bf1086..696636e0 100644 --- a/docs/guide/dubboForNode/ImplementingServices.md +++ b/docs/guide/dubboForNode/ImplementingServices.md @@ -1 +1,234 @@ # ImplementingServices + +Dubbo handles HTTP routes and most plumbing for you, but implementing the actual business logic is still up to you. + +You always register your implementation on the `DubboRouter`. We recommend to create a file `connect.ts` with a registration function in your project: + + +```ts +import { DubboRouter } from "@apachedubbo/dubbo"; + +export default (router: DubboRouter) => {} +``` + + +# Register a service + +Let's say you have defined a simple service in Protobuf: + +``` +message SayRequest { + string sentence = 1; +} +message SayResponse { + string sentence = 1; +} +service ElizaService { + rpc Say(SayRequest) returns (SayResponse) {} +} +``` + +To register this service, call `router.service()`: + +```ts +import { DubboRouter, HandlerContext } from "@apachedubbo/dubbo"; +import { ElizaService } from "./gen/eliza_dubbo"; +import { SayRequest, SayResponse } from "./gen/eliza_pb"; + +export default (router: DubboRouter) => + router.service(ElizaService, { + async say(req: SayRequest, context: HandlerContext) { + return new SayResponse({ + sentence: `You said ${req.sentence}`, + }); + } + }); +``` + +Your method `say()` receives the request message and a context object, and returns a response message. It is a plain function! + + +# Plain functions + +Your function can return a response message, or a promise for a response message, or just an initializer for a response message: + +```ts +function say(req: SayRequest) { + return new SayResponse({ sentence: `You said ${req.sentence}` }); +} +``` + +```ts +async function say(req: SayRequest) { + return { sentence: `You said ${req.sentence}` }; +} +``` + +```ts +const say = (req: SayRequest) => ({ sentence: `You said ${req.sentence}` }); +``` + +You can register any of these functions for the ElizaService. + + +# Context + +The context argument gives you access to headers and service metadata: + +```ts +import { HandlerContext } from "@apachedubbo/dubbo"; +import { SayRequest } from "./gen/eliza_pb"; + +function say(req: SayRequest, context: HandlerContext) { + ctx.service.typeName; // the protobuf type name "ElizaService" + ctx.method.name; // the protobuf rpc name "Say" + context.requestHeader.get("Foo"); + context.responseHeader.set("Foo", "Bar"); + return new SayResponse({ sentence: `You said ${req.sentence}` }); +} +``` + +It can also be used to access arbitrary values that are passed from either server plugins or interceptors. Please refer to the docs on [interceptors](Interceptors.md) for learn more. + + +# Errors + +Instead of returning a response, your method can also raise an error: + +```ts +import { Code, DubboError } from "@apachedubbo/dubbo"; + +function say() { + throw new DubboError("I have no words anymore.", Code.ResourceExhausted); +} +``` + +`Code` is one of Connects [error codes](). Besides the code and a message, errors can also contain metadata (a Headers object) and error details. + + +# Error details + +Error details are a powerful feature. Any protobuf message can be transmitted as an error detail. Let's use `google.rpc.LocalizedMessage` to localize our error message: + +```shell +buf generate buf.build/googleapis/googleapis +``` + +```ts +import { Code, DubboError } from "@apachedubbo/dubbo"; +import { ElizaService } from "./gen/eliza_dubbo"; +import { LocalizedMessage } from "./gen/google/rpc/error_details_pb"; + +function say() { + const details = [ + new LocalizedMessage({ + locale: "fr-CH", + message: "Je n'ai plus de mots.", + }), + new LocalizedMessage({ + locale: "ja-JP", + message: "もう言葉がありません。", + }), + ]; + const metadata = new Headers({ + "words-left": "none" + }); + throw new DubboError( + "I have no words anymore.", + Code.ResourceExhausted, + metadata, + details + ); +} +``` + + +# Streaming + +Before showing the various handlers for streaming endpoints, we'd like to reference the [Streaming]() page from Dubbo-Go as a caveat. Because while Dubbo for Node.js does support all three variations of streaming endpoints, there are tradeoffs that should be considered before diving in. + +Streaming can be a very powerful approach to APIs in the right circumstances, but it also requires great care. Remember, with great power comes great responsibility. + +In **client streaming**, the client sends multiple messages. Once the server receives all the messages, it responds with a single message. In Protobuf schemas, client streaming methods look like this: + +``` +service ElizaService { + rpc Vent(stream VentRequest) returns (VentResponse) {} +} +``` + +In TypeScript, client streaming methods receive an asynchronous iterable of request messages (you can iterate over them with a for [await...of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) loop): + +```ts +async function vent(reqs: AsyncIterable): Promise {} +``` + +In **server streaming**, the client sends a single message, and the server responds with multiple messages. In Protobuf schemas, server streaming methods look like this: + +``` +service ElizaService { + rpc Introduce(IntroduceRequest) returns (stream IntroduceResponse) {} +} +``` + +In TypeScript, server streaming methods receive a request message, and return an asynchronous iterable of response messages, typically with a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*). + +```ts +async function *introduce(req: IntroduceRequest) { + yield { sentence: `Hi ${req.name}, I'm eliza` }; + yield { sentence: `How are you feeling today?` }; +} +``` + +In **bidirectional streaming** (often called bidi), the client and server may both send multiple messages. Often, the exchange is structured like a conversation: the client sends a message, the server responds, the client sends another message, and so on. Keep in mind that this always requires end-to-end HTTP/2 support (regardless of RPC protocol)! + + +# Helper Types + +Service implementations are type-safe. The `service()` method of the `DubboRouter` accepts a `ServiceImpl`, where `T` is a service type. A `ServiceImpl` has a method for each RPC, typed as `MethodImp`, where `M` is a method info object. + +You can use these types to compose your service without registering it right away: + +```ts +import type { MethodImpl, ServiceImpl } from "@apachedubbo/dubbo"; + +export const say: MethodImpl = ... + +export const eliza: ServiceImpl = { + // ... +}; + +export class Eliza implements ServiceImpl { + async say(req: SayRequest) { + return { + sentence: `You said ${req.sentence}`, + }; + } +} +``` + +Registering the examples above: + +```ts +import { DubboRouter } from "@apachedubbo/dubbo"; +import { ElizaService } from "./gen/eliza_dubbo"; +import { say, eliza, Eliza } from "./other-file"; + +export default (router: DubboRouter) => { + // using const say + router.service(ElizaService, { say }); + + // alternative for using const say + router.rpc( + ElizaService, + ElizaService.methods.say, + say + ); + + // using const eliza + router.service(ElizaService, eliza); + + // using class Eliza + router.service(ElizaService, new Eliza()); +} +``` From 658d96fd9a40618ed985b759d36116452379e346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8A=B3=E8=B5=84=E8=9C=80=E9=81=93=E5=B1=B1?= <1493170339@qq.com> Date: Fri, 3 May 2024 19:24:56 +0800 Subject: [PATCH 2/3] fix GettingStarted --- docs/guide/dubboForNode/GettingStarted.md | 209 +++++------------- .../dubboForNode/ImplementingServices.md | 2 +- 2 files changed, 56 insertions(+), 155 deletions(-) diff --git a/docs/guide/dubboForNode/GettingStarted.md b/docs/guide/dubboForNode/GettingStarted.md index 45d5963f..8e3d3b3f 100644 --- a/docs/guide/dubboForNode/GettingStarted.md +++ b/docs/guide/dubboForNode/GettingStarted.md @@ -23,22 +23,22 @@ cd dubbo-example npm init -y npm install typescript tsx npx tsc --init -npm install @bufbuild/buf @bufbuild/protoc-gen-es @bufbuild/protobuf @apachedubbo/protoc-gen-apache-dubbo-es @apachedubbo/dubbo +npm install @bufbuild/protoc-gen-es @bufbuild/protobuf @apachedubbo/protoc-gen-apache-dubbo-es @apachedubbo/dubbo ``` # Define a service First, we need to add a Protobuf file that includes our service definition. For this tutorial, we are going to construct a unary endpoint for a service that is a stripped-down implementation of [ELIZA](https://en.wikipedia.org/wiki/ELIZA), the famous natural language processing program. ```shell -mkdir -p proto && touch proto/eliza.proto +mkdir -p proto && touch proto/example.proto ``` Open up the above file and add the following service definition: -``` +```Protobuf syntax = "proto3"; -package connectrpc.eliza.v1; +package apache.dubbo.demo.example.v1; message SayRequest { string sentence = 1; @@ -48,7 +48,7 @@ message SayResponse { string sentence = 1; } -service ElizaService { +service ExampleService { rpc Say(SayRequest) returns (SayResponse) {} } ``` @@ -56,40 +56,30 @@ service ElizaService { # Generate code -We're going to generate our code using [Buf](https://www.npmjs.com/package/@bufbuild/buf), a modern replacement for Google's protobuf compiler. We installed Buf earlier, but we also need a configuration file to get going. (If you'd prefer, you can skip this section and use `protoc` instead — `protoc-gen-apache-dubbo-es` behaves like any other plugin.) - -First, tell Buf how to generate code with a `buf.gen.yaml` file: - -```yaml -version: v1 -plugins: - - plugin: es - opt: target=ts - out: gen - - plugin: dubbo-es - opt: target=ts - out: gen +Create the gen directory as the target directory for generating file placement: +```Shell +mkdir -p gen ``` +Run the following command to generate a code file in the gen directory: -With this file in place, you can generate code from the schema in the `proto` directory: - -```shell -npx buf generate proto +```Shell +PATH=$PATH:$(pwd)/node_modules/.bin \ + protoc -I proto \ + --es_out gen \ + --es_opt target=ts \ + --apache-dubbo-es_out gen \ + --apache-dubbo-es_opt target=ts \ + example.proto ``` -You should now see two generated TypeScript files: +After running the command, the following generated files should be visible in the target directory: -```markdown{3-5} -├── buf.gen.yaml +```Plain Text ├── gen -│   ├── eliza_dubbo.ts -│   └── eliza_pb.ts -├── node_modules -├── package-lock.json -├── package.json +│ ├── example_dubbo.ts +│ └── example_pb.ts ├── proto -│   └── eliza.proto -└── tsconfig.json +│ └── example.proto ``` Next, we are going to use these files to implement our service. @@ -101,20 +91,20 @@ We defined the `ElizaService` - now it's time to implement it, and register it w Create a new file `dubbo.ts` with the following contents: -```tsx -import type { ConnectRouter } from "@apachedubbo/dubbo"; -import { ElizaService } from "./gen/eliza_dubbo"; +```typescript +import { DubboRouter } from "@apachedubbo/dubbo"; +import { ExampleService } from "./gen/example_dubbo"; export default (router: DubboRouter) => - // registers dubborpc.eliza.v1.ElizaService - router.service(ElizaService, { + // registers apache.dubbo.demo.example.v1 + router.service(ExampleService, { // implements rpc Say async say(req) { return { - sentence: `You said: ${req.sentence}` - } + sentence: `You said: ${req.sentence}`, + }; }, - }); + }, { serviceGroup: 'dubbo', serviceVersion: '1.0.0' }); ``` That's it! There are many other alternatives to implementing a service, and you have access to a context object for headers and trailers, but let's keep it simple for now. @@ -125,15 +115,16 @@ That's it! There are many other alternatives to implementing a service, and you Dubbo services can be plugged into vanilla Node.js servers, [Next.js](https://nextjs.org/), [Express](https://expressjs.com/), or [Fastify](https://fastify.dev/). We are going to use Fastify here. Let's install it, along with our plugin for Fastify: ```shell -npm install fastify @apachedubbo/dubbo-node @apachedubbo/dubbo-fastify +npm install fastify @apachedubbo/dubbo-fastify ``` -Create a new file `server.ts` with the following contents: +Create a new file `server.ts` with the following contents and register the `ExampleService` implemented in the previous step with it. +Next, you can directly initialize and start the server, which will receive requests on the specified port: -```tsx +```typescript import { fastify } from "fastify"; import { fastifyDubboPlugin } from "@apachedubbo/dubbo-fastify"; -import routes from "./connect"; +import routes from "./dubbo"; async function main() { const server = fastify(); @@ -147,14 +138,13 @@ async function main() { await server.listen({ host: "localhost", port: 8080 }); console.log("server is listening at", server.addresses()); } -// You can remove the main() wrapper if you set type: module in your package.json, -// and update your tsconfig.json with target: es2017 and module: es2022. + void main(); ``` Congratulations. Your endpoint is ready to go! You can start your server with: -```shell +```Shell npx tsx server.ts ``` @@ -162,134 +152,45 @@ npx tsx server.ts The simplest way to consume your new API is an HTTP/1.1 POST with a JSON payload. If you have a recent version of cURL installed, it's a one-liner: -```shell +```Shell curl \ - --header 'Content-Type: application/json' \ - --data '{"sentence": "I feel happy."}' \ - http://localhost:8080/dubborpc.eliza.v1.ElizaService/Say + --header 'Content-Type: application/json' \ + --header 'TRI-Service-Version: 1.0.0' \ + --header 'TRI-Service-group: dubbo' \ + --data '{"sentence": "Hello World"}' \ + http://localhost:8080/apache.dubbo.demo.example.v1.ExampleService/Say ``` ---- - -```markdown -Output -{"sentence":"You said: I feel happy."} -``` +You can also use the standard Dubbo client request service. First, we need to obtain the service proxy from the generated code, which is the Dubbo node package, specify the server address for it, and initialize it. Then, we can initiate an RPC call. -You can also make requests using a Dubbo client. Create a new file client.ts with the following contents: +Create a `client.ts` file. -```tsx +```typescript import { createPromiseClient } from "@apachedubbo/dubbo"; -import { ElizaService } from "./gen/eliza_dubbo"; +import { ExampleService } from "./gen/example_dubbo"; import { createDubboTransport } from "@apachedubbo/dubbo-node"; const transport = createDubboTransport({ baseUrl: "http://localhost:8080", - httpVersion: "1.1" + httpVersion: "1.1", }); async function main() { - const client = createPromiseClient(ElizaService, transport); - const res = await client.say({ sentence: "I feel happy." }); + const client = createPromiseClient(ExampleService, transport, { serviceVersion: '1.0.0', serviceGroup: 'dubbo' }); + const res = await client.say({ sentence: "Hello World" }); console.log(res); } void main(); ``` -With your server still running in a separate terminal window, you can now run your client: +Run client: -```shell +```Shell npx tsx client.ts ``` -```markdown -Output -SayResponse { sentence: 'You said: I feel happy.' } -``` - -Congratulations — you've built your first Connect service! 🎉 - - -# From the browser - -You can run the same client from a web browser, just by swapping out the Transport: - -```tsx -import { createPromiseClient } from "@apachedubbo/dubbo"; -import { ElizaService } from "./gen/eliza_dubbo"; -import { createDubboTransport } from "@apachedubbo/dubbo-web"; - -const transport = createDubboTransport({ - baseUrl: "http://localhost:8080", - // Not needed. Web browsers use HTTP/2 automatically. - // httpVersion: "1.1" -}); - -async function main() { - const client = createPromiseClient(ElizaService, transport); - const res = await client.say({ sentence: "I feel happy." }); - console.log(res); -} -void main(); -``` - - -# Use the gRPC protocol instead of the Dubbo protocol - -On Node.js, we support three protocols: - -* The gRPC protocol that is used throughout the gRPC ecosystem. -* The gRPC-Web protocol used by [grpc/grpc-web](https://github.com/grpc/grpc-web), allowing servers to interop with `grpc-web` frontends without the need for an intermediary proxy (such as Envoy). -* The new [Dubbo protocol](https://cn.dubbo.apache.org/zh-cn/overview/reference/protocols/), a simple, HTTP-based protocol that works over HTTP/1.1 or HTTP/2. It takes the best portions of gRPC and gRPC-Web, including streaming, and packages them into a protocol that works equally well in browsers, monoliths, and microservices. The Connect protocol is what we think the gRPC protocol should be. By default, JSON- and binary-encoded Protobuf is supported. - -So far, we have been using the `http://` scheme in our examples. We were not using TLS (Transport Layer Security). If you want to use gRPC and browser clients during local development, you need TLS. - -Actually, that only takes a minute to set up! We will use `mkcert` to make a certificate. If you don't have it installed yet, please run the following commands: - -```shell -brew install mkcert -mkcert -install -mkcert localhost 127.0.0.1 ::1 -export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" -``` - -If you don't use macOS or `brew`, see the [mkcert docs](https://github.com/FiloSottile/mkcert#installation) for instructions. You can copy the last line to your `~/.zprofile` or `~/.profile`, so that the environment variable for Node.js is set every time you open a terminal. - -Let's update our `server.ts` to use this certificate: - -```tsx{4,8-12,17} -import { fastify } from "fastify"; -import { fastifyDubboPlugin } from "@apachedubbo/dubbo-fastify"; -import routes from "./connect"; -import { readFileSync } from "fs"; - -async function main() { - const server = fastify({ - http2: true, - https: { - key: readFileSync("localhost+2-key.pem", "utf8"), - cert: readFileSync("localhost+2.pem", "utf8"), - } - }); - await server.register(fastifyDubboPlugin, { - routes, - }); - await server.listen({ host: "localhost", port: 8443 }); - console.log("server is listening at", server.addresses()); -} -void main(); -``` - -That's it! After you restarted the server, you can still open [https://localhost:8443/](https://localhost:8443/) in your browser, but along with gRPC-Web and Connect, any gRPC client can access it too. Here's an example using `buf curl`: - -```shell -npx buf curl --protocol grpc --schema . -d '{"sentence": "I feel happy."}' \ - https://localhost:8443/dubborpc.eliza.v1.ElizaService/Say -``` - -In your `client.ts`, update the URL and use HTTP version `2` and you're set. It will pick up the locally-trusted certificate authority, just like your web browser and other apps. - +--- -# So what? +# Others -With just a few lines of hand-written code, you've built a real API server that supports both the gRPC and Dubbo protocols. Unlike a hand-written REST service, you didn't need to design a URL hierarchy, hand-write request and response objects, or parse typed values out of query parameters. More importantly, your users got an idiomatic, type-safe client without any extra work on your part. +Refer to [Developing Web Applications Running on Browsers](../dubboForWEB/GettingStarted.md) to learn how to develop browser pages that can access Dubbo backend services. diff --git a/docs/guide/dubboForNode/ImplementingServices.md b/docs/guide/dubboForNode/ImplementingServices.md index 696636e0..6d9cf112 100644 --- a/docs/guide/dubboForNode/ImplementingServices.md +++ b/docs/guide/dubboForNode/ImplementingServices.md @@ -2,7 +2,7 @@ Dubbo handles HTTP routes and most plumbing for you, but implementing the actual business logic is still up to you. -You always register your implementation on the `DubboRouter`. We recommend to create a file `connect.ts` with a registration function in your project: +You always register your implementation on the DubboRouter. We recommend to create a file dubbo.ts with a registration function in your project: ```ts From 2fadb7c3515222264d24ea91cc994a2cb95ab157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8A=B3=E8=B5=84=E8=9C=80=E9=81=93=E5=B1=B1?= <1493170339@qq.com> Date: Tue, 21 May 2024 21:53:45 +0800 Subject: [PATCH 3/3] solved some errors --- docs/guide/dubboForNode/GettingStarted.md | 49 ++++++++----- .../dubboForNode/ImplementingServices.md | 70 +++++++++++-------- 2 files changed, 74 insertions(+), 45 deletions(-) diff --git a/docs/guide/dubboForNode/GettingStarted.md b/docs/guide/dubboForNode/GettingStarted.md index 8e3d3b3f..202832c8 100644 --- a/docs/guide/dubboForNode/GettingStarted.md +++ b/docs/guide/dubboForNode/GettingStarted.md @@ -1,18 +1,21 @@ # Getting started -Dubbo-Node is a library for serving Dubbo, gRPC, and gRPC-Web compatible HTTP APIs using Node.js. It brings the Dubbo Protocol to Node with full TypeScript compatibility and support for all four types of remote procedure calls: unary and the three variations of streaming. - -This ten-minute walkthrough helps you create a small Dubbo service in Node.js. It demonstrates what you'll be writing by hand, what Connect generates for you, and how to call your new API. +Dubbo-js is a library for serving Dubbo, gRPC, and gRPC-Web compatible HTTP APIs using Node.js. It brings the Dubbo +Protocol to Node with full TypeScript compatibility and support for all four types of remote procedure calls: unary and +the three variations of streaming. +This ten-minute walkthrough helps you create a small Dubbo service in Node.js. It demonstrates what you'll be writing by +hand, what Connect generates for you, and how to call your new API. # Prerequisites + We'll set up a project from scratch and then augment it to serve a new endpoint. -- You'll need [Node.js](https://nodejs.org/en/download) installed - we recommend the most recent long-term support version (LTS). +- You'll need [Node.js](https://nodejs.org/en/download) installed - we recommend the most recent long-term support + version (LTS). - We'll use the package manager `npm`, but we are also compatible with `yarn` and `pnpm`. - We'll also use [cURL](https://curl.se/). It's available from Homebrew and most Linux package managers. - # Project setup Let's initialize a project with TypeScript, and install some code generation tools: @@ -27,7 +30,10 @@ npm install @bufbuild/protoc-gen-es @bufbuild/protobuf @apachedubbo/protoc-gen-a ``` # Define a service -First, we need to add a Protobuf file that includes our service definition. For this tutorial, we are going to construct a unary endpoint for a service that is a stripped-down implementation of [ELIZA](https://en.wikipedia.org/wiki/ELIZA), the famous natural language processing program. + +First, we need to add a Protobuf file that includes our service definition. For this tutorial, we are going to construct +a unary endpoint for a service that is a stripped-down implementation of [ELIZA](https://en.wikipedia.org/wiki/ELIZA), +the famous natural language processing program. ```shell mkdir -p proto && touch proto/example.proto @@ -53,13 +59,14 @@ service ExampleService { } ``` - # Generate code Create the gen directory as the target directory for generating file placement: + ```Shell mkdir -p gen ``` + Run the following command to generate a code file in the gen directory: ```Shell @@ -84,15 +91,15 @@ After running the command, the following generated files should be visible in th Next, we are going to use these files to implement our service. - # Implement the service -We defined the `ElizaService` - now it's time to implement it, and register it with the `DubboRouter`. First, let's create a file where we can put the implementation: +We defined the `ElizaService` - now it's time to implement it, and register it with the `DubboRouter`. First, let's +create a file where we can put the implementation: Create a new file `dubbo.ts` with the following contents: ```typescript -import { DubboRouter } from "@apachedubbo/dubbo"; +import type { DubboRouter } from "@apachedubbo/dubbo"; import { ExampleService } from "./gen/example_dubbo"; export default (router: DubboRouter) => @@ -107,18 +114,21 @@ export default (router: DubboRouter) => }, { serviceGroup: 'dubbo', serviceVersion: '1.0.0' }); ``` -That's it! There are many other alternatives to implementing a service, and you have access to a context object for headers and trailers, but let's keep it simple for now. - +That's it! There are many other alternatives to implementing a service, and you have access to a context object for +headers and trailers, but let's keep it simple for now. # Start a server -Dubbo services can be plugged into vanilla Node.js servers, [Next.js](https://nextjs.org/), [Express](https://expressjs.com/), or [Fastify](https://fastify.dev/). We are going to use Fastify here. Let's install it, along with our plugin for Fastify: +Dubbo services can be plugged into vanilla Node.js +servers, [Next.js](https://nextjs.org/), [Express](https://expressjs.com/), or [Fastify](https://fastify.dev/). We are +going to use Fastify here. Let's install it, along with our plugin for Fastify: ```shell npm install fastify @apachedubbo/dubbo-fastify ``` -Create a new file `server.ts` with the following contents and register the `ExampleService` implemented in the previous step with it. +Create a new file `server.ts` with the following contents and register the `ExampleService` implemented in the previous +step with it. Next, you can directly initialize and start the server, which will receive requests on the specified port: ```typescript @@ -150,7 +160,8 @@ npx tsx server.ts # Make requests -The simplest way to consume your new API is an HTTP/1.1 POST with a JSON payload. If you have a recent version of cURL installed, it's a one-liner: +The simplest way to consume your new API is an HTTP/1.1 POST with a JSON payload. If you have a recent version of cURL +installed, it's a one-liner: ```Shell curl \ @@ -161,7 +172,9 @@ curl \ http://localhost:8080/apache.dubbo.demo.example.v1.ExampleService/Say ``` -You can also use the standard Dubbo client request service. First, we need to obtain the service proxy from the generated code, which is the Dubbo node package, specify the server address for it, and initialize it. Then, we can initiate an RPC call. +You can also use the standard Dubbo client request service. First, we need to obtain the service proxy from the +generated code, which is the Dubbo node package, specify the server address for it, and initialize it. Then, we can +initiate an RPC call. Create a `client.ts` file. @@ -180,6 +193,7 @@ async function main() { const res = await client.say({ sentence: "Hello World" }); console.log(res); } + void main(); ``` @@ -193,4 +207,5 @@ npx tsx client.ts # Others -Refer to [Developing Web Applications Running on Browsers](../dubboForWEB/GettingStarted.md) to learn how to develop browser pages that can access Dubbo backend services. +Refer to [Developing Web Applications Running on Browsers](../dubboForWEB/GettingStarted.md) to learn how to develop +browser pages that can access Dubbo backend services. diff --git a/docs/guide/dubboForNode/ImplementingServices.md b/docs/guide/dubboForNode/ImplementingServices.md index 6d9cf112..5a4c7170 100644 --- a/docs/guide/dubboForNode/ImplementingServices.md +++ b/docs/guide/dubboForNode/ImplementingServices.md @@ -2,16 +2,16 @@ Dubbo handles HTTP routes and most plumbing for you, but implementing the actual business logic is still up to you. -You always register your implementation on the DubboRouter. We recommend to create a file dubbo.ts with a registration function in your project: - +You always register your implementation on the `DubboRouter`. We recommend to create a file `dubbo.ts` with a registration +function in your project: ```ts -import { DubboRouter } from "@apachedubbo/dubbo"; +import type { DubboRouter } from "@apachedubbo/dubbo"; -export default (router: DubboRouter) => {} +export default (router: DubboRouter) => { +} ``` - # Register a service Let's say you have defined a simple service in Protobuf: @@ -45,12 +45,13 @@ export default (router: DubboRouter) => }); ``` -Your method `say()` receives the request message and a context object, and returns a response message. It is a plain function! - +Your method `say()` receives the request message and a context object, and returns a response message. It is a plain +function! # Plain functions -Your function can return a response message, or a promise for a response message, or just an initializer for a response message: +Your function can return a response message, or a promise for a response message, or just an initializer for a response +message: ```ts function say(req: SayRequest) { @@ -70,7 +71,6 @@ const say = (req: SayRequest) => ({ sentence: `You said ${req.sentence}` }); You can register any of these functions for the ElizaService. - # Context The context argument gives you access to headers and service metadata: @@ -88,8 +88,8 @@ function say(req: SayRequest, context: HandlerContext) { } ``` -It can also be used to access arbitrary values that are passed from either server plugins or interceptors. Please refer to the docs on [interceptors](Interceptors.md) for learn more. - +It can also be used to access arbitrary values that are passed from either server plugins or interceptors. Please refer +to the docs on [interceptors](Interceptors.md) for learn more. # Errors @@ -103,12 +103,13 @@ function say() { } ``` -`Code` is one of Connects [error codes](). Besides the code and a message, errors can also contain metadata (a Headers object) and error details. - +`Code` is one of Connects [error codes](). Besides the code and a message, errors can also contain metadata (a Headers +object) and error details. # Error details -Error details are a powerful feature. Any protobuf message can be transmitted as an error detail. Let's use `google.rpc.LocalizedMessage` to localize our error message: +Error details are a powerful feature. Any protobuf message can be transmitted as an error detail. Let's +use `google.rpc.LocalizedMessage` to localize our error message: ```shell buf generate buf.build/googleapis/googleapis @@ -142,14 +143,17 @@ function say() { } ``` - # Streaming -Before showing the various handlers for streaming endpoints, we'd like to reference the [Streaming]() page from Dubbo-Go as a caveat. Because while Dubbo for Node.js does support all three variations of streaming endpoints, there are tradeoffs that should be considered before diving in. +Before showing the various handlers for streaming endpoints, we'd like to reference the [Streaming]() page from Dubbo-Go +as a caveat. Because while Dubbo for Node.js does support all three variations of streaming endpoints, there are +tradeoffs that should be considered before diving in. -Streaming can be a very powerful approach to APIs in the right circumstances, but it also requires great care. Remember, with great power comes great responsibility. +Streaming can be a very powerful approach to APIs in the right circumstances, but it also requires great care. Remember, +with great power comes great responsibility. -In **client streaming**, the client sends multiple messages. Once the server receives all the messages, it responds with a single message. In Protobuf schemas, client streaming methods look like this: +In **client streaming**, the client sends multiple messages. Once the server receives all the messages, it responds with +a single message. In Protobuf schemas, client streaming methods look like this: ``` service ElizaService { @@ -157,13 +161,17 @@ service ElizaService { } ``` -In TypeScript, client streaming methods receive an asynchronous iterable of request messages (you can iterate over them with a for [await...of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) loop): +In TypeScript, client streaming methods receive an asynchronous iterable of request messages (you can iterate over them +with a for [await...of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) +loop): ```ts -async function vent(reqs: AsyncIterable): Promise {} +async function vent(reqs: AsyncIterable): Promise { +} ``` -In **server streaming**, the client sends a single message, and the server responds with multiple messages. In Protobuf schemas, server streaming methods look like this: +In **server streaming**, the client sends a single message, and the server responds with multiple messages. In Protobuf +schemas, server streaming methods look like this: ``` service ElizaService { @@ -171,28 +179,34 @@ service ElizaService { } ``` -In TypeScript, server streaming methods receive a request message, and return an asynchronous iterable of response messages, typically with a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*). +In TypeScript, server streaming methods receive a request message, and return an asynchronous iterable of response +messages, typically with +a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*). ```ts -async function *introduce(req: IntroduceRequest) { +async function* introduce(req: IntroduceRequest) { yield { sentence: `Hi ${req.name}, I'm eliza` }; yield { sentence: `How are you feeling today?` }; } ``` -In **bidirectional streaming** (often called bidi), the client and server may both send multiple messages. Often, the exchange is structured like a conversation: the client sends a message, the server responds, the client sends another message, and so on. Keep in mind that this always requires end-to-end HTTP/2 support (regardless of RPC protocol)! - +In **bidirectional streaming** (often called bidi), the client and server may both send multiple messages. Often, the +exchange is structured like a conversation: the client sends a message, the server responds, the client sends another +message, and so on. Keep in mind that this always requires end-to-end HTTP/2 support (regardless of RPC protocol)! # Helper Types -Service implementations are type-safe. The `service()` method of the `DubboRouter` accepts a `ServiceImpl`, where `T` is a service type. A `ServiceImpl` has a method for each RPC, typed as `MethodImp`, where `M` is a method info object. +Service implementations are type-safe. The `service()` method of the `DubboRouter` accepts a `ServiceImpl`, where `T` +is a service type. A `ServiceImpl` has a method for each RPC, typed as `MethodImp`, where `M` is a method info +object. You can use these types to compose your service without registering it right away: ```ts import type { MethodImpl, ServiceImpl } from "@apachedubbo/dubbo"; -export const say: MethodImpl = ... +export const say: MethodImpl = +... export const eliza: ServiceImpl = { // ... @@ -210,7 +224,7 @@ export class Eliza implements ServiceImpl { Registering the examples above: ```ts -import { DubboRouter } from "@apachedubbo/dubbo"; +import type { DubboRouter } from "@apachedubbo/dubbo"; import { ElizaService } from "./gen/eliza_dubbo"; import { say, eliza, Eliza } from "./other-file";