تم بالفعل كتابة الكثير من المقالات حول مزايا وعيوب REST (وأكثر من ذلك في التعليقات عليها)). وإذا حدث أن اضطررت إلى تطوير خدمة يجب أن يطبق فيها هذا الهيكل ، فستجد بالتأكيد وثائقها. بعد كل شيء ، وإنشاء كل طريقة ، نحن نفهم بالتأكيد أن المبرمجين الآخرين سوف تشير إلى هذه الأساليب. لذلك ، يجب أن تكون الوثائق شاملة ، والأهم من ذلك - ذات الصلة.
مرحبًا بك في القطة ، حيث سأشرح كيف قمنا بحل هذه المشكلة في فريقنا.
قليلا من السياق.
كلف فريقنا بإصدار منتج خلفي على
Node.js من التعقيد المتوسط في وقت قصير. كان من المفترض أن يتفاعل المبرمجون والمعبئون في Frontend مع هذا المنتج.
بعد بعض التفكير ، قررنا أن نحاول استخدام
TypeScript باعتباره
YaP .
ساعدنا كل من TSLint و
Prettier جيد التوليف على تحقيق نفس نمط الكود والتحقق
الدقيق في مرحلة الترميز / التجميع (
والأقشع حتى في مرحلة الالتزام). قادت الكتابة القوية الجميع إلى وصف الواجهات وأنواع جميع الكائنات بوضوح. أصبح من السهل قراءة وفهم ما تقوم به هذه الوظيفة بالضبط كمعلمة إدخال وما ستعود إليه في النهاية وأي من خصائص الكائن إلزامية وأيها ليست كذلك. بدأ الكود يشبه جافا إلى حد كبير). وبالطبع ، أضافت
TypeDoc إمكانية القراءة لكل وظيفة.
هذه هي الطريقة التي بدأ بها الكود:
export interface IResponseData<T> { nonce: number; code: number; message?: string; data?: T; } export class TransferObjectUtils { public static createResponseObject<T = object>(responseCode: number, message: string, data: T): IResponseData<T> { const result: IResponseData<T> = { code: responseCode || 200, nonce: Date.now() }; if (message) { result.message = message; } if (data) { result.data = data; } return result; } }
فكرنا في المتحدرين ، لن يكون من الصعب الحفاظ على الكود الخاص بنا ، فقد حان الوقت للتفكير في مستخدمي خادم REST الخاص بنا.
نظرًا لأن كل شيء تم بسرعة كبيرة ، فقد فهمنا أن كتابة التعليمات البرمجية بشكل منفصل والوثائق بشكل منفصل ستكون صعبة للغاية. لا سيما إضافة معلمات إضافية إلى الإجابات أو الطلبات وفقا لمتطلبات الواجهة الأمامية أو mobilchiki ولا تنس أن تحذر الآخرين من ذلك. هذا هو المكان الذي ظهرت فيه متطلبات واضحة:
يجب دائمًا مزامنة الرمز مع الوثائق . هذا يعني أنه يجب استبعاد العامل البشري والوثائق يجب أن تؤثر على الكود ، ويجب أن يؤثر الكود على الوثائق.
لقد بحثت هنا في البحث عن الأدوات المناسبة لهذا الغرض. لحسن الحظ ، فإن مستودع NPM هو مجرد مستودع لجميع أنواع الأفكار والحلول.
كانت متطلبات الأداة كما يلي:
- تزامن الوثائق مع رمز.
- دعم TypeScript
- التحقق من صحة الحزم الواردة / الصادرة ؛
- الحزمة الحية والمدعومة.
اضطررت إلى الكتابة على خدمة REST باستخدام العديد من الحزم المختلفة ، الأكثر شيوعًا منها: tsoa ، swagger-node-express ، express-openapi ، swagger-codegen.

ولكن في البعض ، لم يكن هناك دعم من نوع TypeScript ، وفي بعض عمليات التحقق من الحزمة ، وكان البعض قادرًا على إنشاء تعليمات برمجية تستند إلى الوثائق ، لكنهم لم يوفروا المزيد من المزامنة.
هذا هو المكان الذي صادفت فيه joi-to-swagger. حزمة رائعة يمكنها تحويل الوصف الموضح في مخطط Joi إلى وثائق مبهرة وحتى مع دعم TypeScript. يتم تنفيذ جميع العناصر باستثناء التزامن. هرعت لبعض الوقت ، وجدت مستودعًا مهجورًا لأحد الصينيين الذين استخدموا
joi-to-swagger مع إطار Koa. نظرًا لعدم وجود تحيزات ضد Koa في فريقنا ، ولم تكن هناك أسباب لاتباع اتجاه Express بصورة عمياء ، فقد قررنا محاولة الإقلاع في هذه المجموعة.
لقد قمت بتشكيل هذا المستودع ، والأخطاء الثابتة ، وأكملت بعض الأشياء ، والآن تم إصدار أول مساهمة لي في OpenSource Koa-Joi-Swagger-TS. لقد نجحنا في هذا المشروع وبعد ذلك كان هناك بالفعل العديد من الآخرين. لقد أصبح من المريح جدًا كتابة خدمات REST وصيانتها ، ولا يحتاج مستخدمو هذه الخدمات إلا إلى رابط إلى وثائق Swagger عبر الإنترنت. بعدها ، أصبح من الواضح أين يمكن تطوير هذه الحزمة وشهدت العديد من التحسينات.
الآن دعنا نرى كيف يمكنك باستخدام
Koa-Joi-Swagger-TS يمكنك كتابة خادم REST ذاتي التوثيق.
لقد نشرت الرمز النهائي هنا .
نظرًا لأن هذا المشروع عبارة عن عرض توضيحي ، فقد قمت بتبسيط ودمج العديد من الملفات في ملف واحد. بشكل عام ، من الجيد أن يقوم الفهرس بتهيئة التطبيق واستدعاء ملف app.ts ، والذي بدوره سيقرأ الموارد ، والمكالمات للاتصال بقاعدة البيانات ، إلخ. يجب أن يبدأ الخادم بأحدث أمر (فقط ما سيتم وصفه الآن أدناه).
بالنسبة للمبتدئين ، قم بإنشاء
index.ts باستخدام هذا المحتوى:
index.ts import * as Koa from "koa"; import { BaseContext } from "koa"; import * as bodyParser from "koa-bodyparser"; import * as Router from "koa-router"; const SERVER_PORT = 3002; (async () => { const app = new Koa(); const router = new Router(); app.use(bodyParser()); router.get("/", (ctx: BaseContext, next: Function) => { console.log("Root loaded!") }); app .use(router.routes()) .use(router.allowedMethods()); app.listen(SERVER_PORT); console.log(`Server listening on http://localhost:${SERVER_PORT} ...`); })();
عند بدء هذه الخدمة ، سيتم رفع خادم REST ، والذي لا يعرف حتى الآن كيف. الآن قليلا عن الهندسة المعمارية للمشروع. منذ أن تحولت إلى Node.JS من Java ، حاولت إنشاء خدمة بنفس الطبقات هنا.
لنبدأ ربط
Koa-Joi-Swagger-TS . تثبيته بشكل طبيعي.
npm install koa-joi-swagger-ts --save
قم
بإنشاء مجلد
"وحدات التحكم" ومجلد
"المخططات" فيه. في مجلد وحدات التحكم ، قم بإنشاء
قاعدة التحكم الأولى الخاصة
بنا.base.controller.ts import { BaseContext } from "koa"; import { controller, description, get, response, summary, tag } from "koa-joi-swagger-ts"; import { ApiInfoResponseSchema } from "./schemas/apiInfo.response.schema"; @controller("/api/v1") export abstract class BaseController { @get("/") @response(200, { $ref: ApiInfoResponseSchema }) @tag("GET") @description("Returns text info about version of API") @summary("Show API index page") public async index(ctx: BaseContext, next: Function): Promise<void> { console.log("GET /api/v1/"); ctx.status = 200; ctx.body = { code: 200, data: { appVersion: "1.0.0", build: "1001", apiVersion: 1, reqHeaders: ctx.request.headers, apiDoc: "/api/v1/swagger.json" } } }; }
كما ترون من الزخارف (التعليقات التوضيحية في جافا) ، سيتم ربط هذه الفئة بالمسار "/ api / v1" ، وستكون جميع الأساليب في الداخل مرتبطة بهذا المسار.
تحتوي هذه الطريقة على وصف لتنسيق الاستجابة ، الموضح في الملف "./schemas/apiInfo.response.schema":
apiInfo.response.schema import * as Joi from "joi"; import { definition } from "koa-joi-swagger-ts"; import { BaseAPIResponseSchema } from "./baseAPI.response.schema"; @definition("ApiInfo", "Information data about current application and API version") export class ApiInfoResponseSchema extends BaseAPIResponseSchema { public data = Joi.object({ appVersion: Joi.string() .description("Current version of application") .required(), build: Joi.string().description("Current build version of application"), apiVersion: Joi.number() .positive() .description("Version of current REST api") .required(), reqHeaders: Joi.object().description("Request headers"), apiDoc: Joi.string() .description("URL path to swagger document") .required() }).required(); }
إن إمكانيات هذا الوصف للمخطط في جوي واسعة جدًا
وموصوفة بمزيد من التفاصيل هنا:
www.npmjs.com/package/joi-to-swaggerوهنا هو جد الفئة الموصوفة (في الواقع هذه هي الفئة الأساسية لجميع إجابات خدمتنا):
baseAPI.response.schema import * as Joi from "joi"; import { definition } from "koa-joi-swagger-ts"; @definition("BaseAPIResponse", "Base response entity with base fields") export class BaseAPIResponseSchema { public code = Joi.number() .required() .strict() .only(200, 400, 500) .example(200) .description("Code of operation result"); public message = Joi.string().description("message will be filled in some causes"); }
سجل الآن هذه الدوائر ووحدات التحكم في نظام Koa-Joi-Swagger-TS.
بجانب index.ts ، قم بإنشاء ملف
routing.ts آخر:
routing.ts import { KJSRouter } from "koa-joi-swagger-ts"; import { BaseController } from "./controllers/base.controller"; import { BaseAPIResponseSchema } from "./controllers/schemas/baseAPI.response.schema"; import { ApiInfoResponseSchema } from "./controllers/schemas/apiInfo.response.schema"; const SERVER_PORT = 3002; export const loadRoutes = () => { const router = new KJSRouter({ swagger: "2.0", info: { version: "1.0.0", title: "simple-rest" }, host: `localhost:${SERVER_PORT}`, basePath: "/api/v1", schemes: ["http"], paths: {}, definitions: {} }); router.loadDefinition(ApiInfoResponseSchema); router.loadDefinition(BaseAPIResponseSchema); router.loadController(BaseController); router.setSwaggerFile("swagger.json"); router.loadSwaggerUI("/api/docs"); return router.getRouter(); };
هنا نقوم بإنشاء مثيل لفئة KJSRouter ، والتي هي في الأساس جهاز توجيه Koa ، ولكن مع إضافة برامج متوسطة ومعالجات.
لذلك ، في ملف
index.ts ،
نحن ببساطة نغير
const router = new Router();
في
const router = loadRoutes();
حسنًا ، احذف المعالج غير الضروري:
index.ts import * as Koa from "koa"; import * as bodyParser from "koa-bodyparser"; import { loadRoutes } from "./routing"; const SERVER_PORT = 3002; (async () => { const app = new Koa(); const router = loadRoutes(); app.use(bodyParser()); app .use(router.routes()) .use(router.allowedMethods()); app.listen(SERVER_PORT); console.log(`Server listening on http://localhost:${SERVER_PORT} ...`); })();
عند بدء هذه الخدمة ، تتوفر 3 طرق لنا:
1. / api / v1 - الطريق الموثق
الذي يظهر في حالتي:
http: // localhost: 3002 / api / v1 { code: 200, data: { appVersion: "1.0.0", build: "1001", apiVersion: 1, reqHeaders: { host: "localhost:3002", connection: "keep-alive", cache-control: "max-age=0", upgrade-insecure-requests: "1", user-agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36", accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", accept-encoding: "gzip, deflate, br", accept-language: "uk-UA,uk;q=0.9,ru;q=0.8,en-US;q=0.7,en;q=0.6" }, apiDoc: "/api/v1/swagger.json" } }
واثنين من طرق الخدمة:
2. /api/v1/swagger.jsonswagger.json { swagger: "2.0", info: { version: "1.0.0", title: "simple-rest" }, host: "localhost:3002", basePath: "/api/v1", schemes: [ "http" ], paths: { /: { get: { tags: [ "GET" ], summary: "Show API index page", description: "Returns text info about version of API", consumes: [ "application/json" ], produces: [ "application/json" ], responses: { 200: { description: "Information data about current application and API version", schema: { type: "object", $ref: "#/definitions/ApiInfo" } } }, security: [ ] } } }, definitions: { BaseAPIResponse: { type: "object", required: [ "code" ], properties: { code: { type: "number", format: "float", enum: [ 200, 400, 500 ], description: "Code of operation result", example: { value: 200 } }, message: { type: "string", description: "message will be filled in some causes" } } }, ApiInfo: { type: "object", required: [ "code", "data" ], properties: { code: { type: "number", format: "float", enum: [ 200, 400, 500 ], description: "Code of operation result", example: { value: 200 } }, message: { type: "string", description: "message will be filled in some causes" }, data: { type: "object", required: [ "appVersion", "apiVersion", "apiDoc" ], properties: { appVersion: { type: "string", description: "Current version of application" }, build: { type: "string", description: "Current build version of application" }, apiVersion: { type: "number", format: "float", minimum: 1, description: "Version of current REST api" }, reqHeaders: { type: "object", properties: { }, description: "Request headers" }, apiDoc: { type: "string", description: "URL path to swagger document" } } } } } } }
3. / api / docsهذه هي الصفحة التي تحتوي على Swagger UI - إنها تمثيل مرئي مناسب للغاية لنظام Swagger ، حيث يمكنك إنشاء طلبات والحصول على إجابات حقيقية من الخادم ، بالإضافة إلى كونها ملائمة لرؤية ذلك.

تتطلب واجهة المستخدم هذه الوصول إلى ملف swagger.json ، ولهذا السبب تم تضمين المسار السابق.
حسنًا ، يبدو أن كل شيء موجود وأن كل شيء يعمل ، لكن! ..
بمرور الوقت ، وضعنا في دائرة مثل هذا التنفيذ ، لدينا الكثير من الازدواجية في التعليمات البرمجية. في حالة عندما تحتاج وحدات التحكم إلى فعل الشيء نفسه. وبسبب هذا ، وضعت اللمسات الأخيرة على الحزمة وأضفت القدرة على وصف "المجمع" لوحدات التحكم.
النظر في مثال على مثل هذه الخدمة.
لنفترض أن لدينا وحدة تحكم "المستخدمين" مع العديد من الطرق.
الحصول على جميع المستخدمين @get("/") @response(200, { $ref: UsersResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Returns list of all users") @summary("Get all users") public async getAllUsers(ctx: BaseContext): Promise<void> { console.log("GET /api/v1/users"); let message = "Get all users error"; let code = 400; let data = null; try { let serviceResult = await getAllUsers(); if (serviceResult) { data = serviceResult; code = 200; message = null; } } catch (e) { console.log("Error while getting users list"); code = 500; } ctx.status = code; ctx.body = TransferObjectUtils.createResponseObject(code, message, data); };
تحديث المستخدم @post("/") @parameter("body", { $ref: UsersRequestSchema }, ENUM_PARAM_IN.body) @response(200, { $ref: BaseAPIResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Update user data") @summary("Update user data") public async updateUser(ctx: BaseContext): Promise<void> { console.log("POST /api/v1/users"); let message = "Update user data error"; let code = 400; let data = null; try { let serviceResult = await updateUser(ctx.request.body.data); if (serviceResult) { code = 200; message = null; } } catch (e) { console.log("Error while updating user"); code = 500; } ctx.status = code; ctx.body = TransferObjectUtils.createResponseObject(code, message, data); };
أدخل المستخدم @put("/") @parameter("body", { $ref: UsersRequestSchema }, ENUM_PARAM_IN.body) @response(200, { $ref: BaseAPIResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Insert new user") @summary("Insert new user") public async insertUser(ctx: BaseContext): Promise<void> { console.log("PUT /api/v1/users"); let message = "Insert new user error"; let code = 400; let data = null; try { let serviceResult = await insertUser(ctx.request.body.data); if (serviceResult) { code = 200; message = null; } } catch (e) { console.log("Error while inserting user"); code = 500; } ctx.status = code; ctx.body = TransferObjectUtils.createResponseObject(code, message, data); };
كما ترون ، تحتوي أساليب التحكم الثلاثة على رمز مكرر. لمثل هذه الحالات التي نستخدمها الآن هذه الفرصة.
أولاً ، قم بإنشاء دالة التفاف ، على سبيل المثال ، مباشرة في ملف
routing.ts .
const controllerDecorator = async (controller: Function, ctx: BaseContext, next: Function, summary: string): Promise<void> => { console.log(`${ctx.request.method} ${ctx.request.url}`); ctx.body = null; ctx.status = 400; ctx.statusMessage = `Error while executing '${summary}'`; try { await controller(ctx); } catch (e) { console.log(e, `Error while executing '${summary}'`); ctx.status = 500; } ctx.body = TransferObjectUtils.createResponseObject(ctx.status, ctx.statusMessage, ctx.body); };
ثم توصيله إلى وحدة تحكم لدينا.
استبدل
router.loadController(UserController);
في
router.loadController(UserController, controllerDecorator);
حسنًا ، دعنا نبسط طرق التحكم لدينا
تحكم المستخدم @get("/") @response(200, { $ref: UsersResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Returns list of all users") @summary("Get all users") public async getAllUsers(ctx: BaseContext): Promise<void> { let serviceResult = await getAllUsers(); if (serviceResult) { ctx.body = serviceResult; ctx.status = 200; ctx.statusMessage = null; } }; @post("/") @parameter("body", { $ref: UsersRequestSchema }, ENUM_PARAM_IN.body) @response(200, { $ref: BaseAPIResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Update user data") @summary("Update user data") public async updateUser(ctx: BaseContext): Promise<void> { let serviceResult = await updateUser(ctx.request.body.data); if (serviceResult) { ctx.status = 200; ctx.statusMessage = null; } }; @put("/") @parameter("body", { $ref: UsersRequestSchema }, ENUM_PARAM_IN.body) @response(200, { $ref: BaseAPIResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Insert new user") @summary("Insert new user") public async insertUser(ctx: BaseContext): Promise<void> { let serviceResult = await insertUser(ctx.request.body.data); if (serviceResult) { ctx.status = 200; ctx.statusMessage = null; } };
في هذا
controllerDecorator ، يمكنك إضافة أي منطق الشيكات أو سجلات مفصلة من المدخلات / المخرجات.
لقد نشرت الرمز النهائي هنا .
الآن لدينا تقريبا CRUD جاهزة. حذف يمكن كتابتها عن طريق القياس. في الواقع ، الآن لكتابة وحدة تحكم جديدة ، يجب علينا:
- إنشاء ملف تحكم
- إضافته إلى routing.ts
- وصف الطرق
- في كل طريقة ، استخدم دوائر الإدخال / الإخراج
- وصف هذه المخططات
- ربط هذه المخططات في routing.ts
إذا كانت الحزمة الواردة لا تتطابق مع المخطط ، فسيتلقى مستخدم خدمة REST لدينا خطأ 400 مع وصف الخطأ بالضبط. إذا كانت الحزمة الصادرة غير صالحة ، فسيتم إنشاء خطأ 500.
حسنا ولا يزال تافه لطيفا. في Swagger UI ، يمكنك استخدام وظيفة "
Try it out " في أي طريقة. سيتم إنشاء طلب عبر حليقة إلى الخدمة قيد التشغيل ، وبالطبع يمكنك رؤية النتيجة على الفور. ولهذا الغرض ، من المريح جدًا وصف المعلمة "
مثال " في الدائرة. لأنه سيتم إنشاء الطلب فورًا باستخدام حزمة جاهزة استنادًا إلى الأمثلة الموضحة.

النتائج
مريحة جدا ومفيدة في النهاية ، تحول الشيء. في البداية ، لم يرغبوا في التحقق من صحة الحزم الصادرة ، ولكن بمساعدة هذا التحقق ، اكتشفوا العديد من الأخطاء الهامة في جانبهم. بالطبع ، لا يمكنك استخدام جميع ميزات Joi بشكل كامل (نظرًا لأننا محدودون من قبل joi-to-swagger) ، ولكن تلك الميزات كافية تمامًا.
الآن أصبحت الوثائق متصلة دائمًا وتتوافق دائمًا مع الشفرة - وهذا هو الشيء الرئيسي.
ما هي الأفكار الأخرى هناك؟ ..
هل من الممكن إضافة دعم صريح؟
أنا فقط قرأته .
سيكون من الرائع حقًا وصف الكيانات مرة واحدة في مكان واحد. لأنه من الضروري الآن تحرير كل من الدوائر والواجهات.
ربما سيكون لديك بعض الأفكار المثيرة للاهتمام. الأفضل بعد سحب طلبات :)
مرحبا بكم في المساهمين.