diff --git a/.github/workflows/beta_site.yml b/.github/workflows/beta_site.yml index 676296c87128..44ef019dd710 100644 --- a/.github/workflows/beta_site.yml +++ b/.github/workflows/beta_site.yml @@ -4,6 +4,10 @@ on: push: branches: - v4-next + paths: + - .github/workflows/beta_site.yml + - site/** + - CHANGELOG.md # Review gh actions docs if you want to further define triggers, paths, etc # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 8dfa7fb5bcf6..5bf34dd26821 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -4,12 +4,14 @@ on: push: branches: [v4-next] paths-ignore: - - '.github/workflows/site.yml' + - '.github/workflows/**' + - '!.github/workflows/nodejs.yml' - 'site/**' - '*.md' pull_request: paths-ignore: - - '.github/workflows/site.yml' + - '.github/workflows/**' + - '!.github/workflows/nodejs.yml' - 'site/**' - '*.md' diff --git a/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/deployment.md b/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/deployment.md new file mode 100644 index 000000000000..0519ecba6ea9 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/deployment.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/docs/extensions/egg.md b/site/docs/extensions/egg.md index fcc4cec63026..62c256e24b7f 100644 --- a/site/docs/extensions/egg.md +++ b/site/docs/extensions/egg.md @@ -551,7 +551,7 @@ Copy - `--daemon` 是否允许在后台模式,无需 nohup。若使用 Docker 建议直接前台运行。 - `--env=prod` 框架运行环境,默认会读取环境变量 process.env.EGG_SERVER_ENV, 如未传递将使用框架内置环境 prod。 - `--workers=2` 框架 worker 线程数,默认会创建和 CPU 核数相当的 app worker 数,可以充分的利用 CPU 资源。 -- `--title=egg-server-showcase` 用于方便 ps 进程时 grep 用,默认为 egg-server-${appname}。 +- `--title=egg-server-showcase` 用于方便 ps 进程时 grep 用,默认为 `egg-server-${appname}`。 - `--framework=yadan` 如果应用使用了[自定义框架](https://eggjs.org/zh-cn/advanced/framework.html),可以配置 package.json 的 egg.framework 或指定该参数。 - `--ignore-stderr` 忽略启动期的报错。 - `--https.key` 指定 HTTPS 所需密钥文件的完整路径。 diff --git a/site/docusaurus.config.js b/site/docusaurus.config.js index 6f3b0c61e176..9163f2e3789a 100644 --- a/site/docusaurus.config.js +++ b/site/docusaurus.config.js @@ -56,10 +56,10 @@ const config = { editUrl: 'https://github.com/midwayjs/midway/tree/main/site/', versions: { current: { - label: '3.0.0', + label: '4.0.0 🚧', }, }, - lastVersion: 'current', + lastVersion: '3.0.0', sidebarCollapsed: false, }, blog: { diff --git a/site/i18n/en/docusaurus-plugin-content-docs/current/controller.md b/site/i18n/en/docusaurus-plugin-content-docs/current/controller.md index 955d0a18d744..291fa25e1865 100644 --- a/site/i18n/en/docusaurus-plugin-content-docs/current/controller.md +++ b/site/i18n/en/docusaurus-plugin-content-docs/current/controller.md @@ -1,4 +1,4 @@ -​ import Tabs from '@theme/Tabs'; +​import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Routing and controller @@ -207,7 +207,7 @@ The following are these decorators and the corresponding equivalent frame values :::caution **Note** @Queries decorator is **different from** @Query. -Queries will aggregate the same keys together and become an array. When the interface parameter accessed by the user is `/? When name = a & name = B`, @Queries will return {name: [a, B] }, while Query will only return {name: B} +Queries will aggregate the same keys together and become an array. When the interface parameter accessed by the user is `/? When name = a & name = B`, @Queries will return `{name: [a, B] }`, while Query will only return `{name: B}`. ::: diff --git a/site/i18n/en/docusaurus-plugin-content-docs/current/deployment.md b/site/i18n/en/docusaurus-plugin-content-docs/current/deployment.md index bffcc2b5282f..d0f3f527f12e 100644 --- a/site/i18n/en/docusaurus-plugin-content-docs/current/deployment.md +++ b/site/i18n/en/docusaurus-plugin-content-docs/current/deployment.md @@ -318,13 +318,13 @@ Bootstrap | Property | Type | Description | | -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -| appDir | string | Optional. The project root directory is `process.cwd()` by default. | -| baseDir | string | Optional. The directory of the project code, which is `src` in R & D and `dist` in R & D. | -| imports | Component [] | Optional, explicit component reference | -| moduleDetector | 'file' \| IFileDetector \| false | Optional. The module loading method used. Default value: `file`. You can use the dependency injection local file scanning method. You can explicitly specify a scanner or disable scanning. | -| logger | Boolean \| ILogger | optional. the logger used in the bootstrap. the default value is consoleLogger | -| ignore | string [] | optional. the path ignored by the dependent injection container scan. the moduleDetector is invalid if false | -| globalConfig | Array<{ [environmentName: string]: Record }> \| Record | Optionally, if the global incoming configuration is an object, it is directly merged into the current configuration in the form of an object. If you want to pass in the configuration of different environments, it is passed in in the form of an array with the same structure and `importConfigs`. | +| appDir | `string` | Optional. The project root directory is `process.cwd()` by default. | +| baseDir | `string` | Optional. The directory of the project code, which is `src` in R & D and `dist` in R & D. | +| imports | `Component[]` | Optional, explicit component reference | +| moduleDetector | `'file' \| IFileDetector \| false` | Optional. The module loading method used. Default value: `file`. You can use the dependency injection local file scanning method. You can explicitly specify a scanner or disable scanning. | +| logger | `boolean \| ILogger` | Optional. The logger used in the bootstrap. The default value is consoleLogger | +| ignore | `string[]` | Optional. The path ignored by the dependent injection container scan. The moduleDetector is invalid if false | +| globalConfig | `Record \| Array>` | Optionally, if the global incoming configuration is an object, it is directly merged into the current configuration in the form of an object. If you want to pass in the configuration of different environments, it is passed in in the form of an array with the same structure and `importConfigs`. | diff --git a/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/egg.md b/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/egg.md index 1970f6c4e0b3..9868afaabf55 100644 --- a/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/egg.md +++ b/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/egg.md @@ -551,7 +551,7 @@ As shown in the above example, the following parameters are supported: - Whether `--daemon` is allowed in the background mode without nohup. If Docker is used, it is recommended to run directly at the foreground. - `--env=prod` running environment of the framework. By default, the environment variable process.env.EGG_SERVER_ENV will be read. If it is not passed, the built-in environment prod of the framework will be used. - `--workers=2` Number of Worker threads in the framework. By default, the number of app workers equivalent to the number of CPU cores will be created, which can make full use of CPU resources. -- `--title=egg-server-showcase` is used to facilitate grep in ps processes. the default value is egg-server-${appname}. +- `--title=egg-server-showcase` is used to facilitate grep in ps processes. the default value is `egg-server-${appname}`. - `--framework=yadan` If the application uses a [custom framework](https://eggjs.org/zh-cn/advanced/framework.html), you can configure the egg.framework of the package.json or specify this parameter. - `--ignore-stderr`. - `--https.key` specifies the full path of the key file that is required for HTTPS. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/koa.md b/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/koa.md index 71037e58871a..67e683597d18 100644 --- a/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/koa.md +++ b/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/koa.md @@ -217,20 +217,20 @@ All attributes are described as follows: | Property | Type | Description | | ------------ | ----------------------------------------- | ------------------------------------------------------- | -| port | Number | Optional, port to start | -| globalPrefix | string | optional. the global http prefix | -| keys | string[] | Optional, Cookies signature, if the upper layer does not write keys, you can also set it here | -| hostname | string | Optional. The hostname to listen to. Default 127.1 | -| Key | string \| Buffer \| Array | Optional, Https key, server private key | -| cert | string \| Buffer \| Array | Optional, Https cert, server certificate | -| ca | string \| Buffer \| Array | Optional, Https ca | -| http2 | boolean | Optional, supported by http2, default false | -| proxy | boolean | Optional, whether to enable the proxy. If it is true, the IP in the request request will be obtained first from the X-Forwarded-For in the Header field. The default is false. | -| subdomainOffset | number | optional, the offset of the subdomain name, default 2. | -| proxyIpHeader | string | optional. obtains the field name of the proxy ip address. the default value is X-Forwarded-For | -| maxIpsCount | number | optional. the maximum number of ips obtained, which is 0 by default. | -| serverTimeout | number | Optional, server timeout configuration, the default is 2 * 60 * 1000 (2 minutes), in milliseconds | -| serverOptions | Record | Optional,http Server [Options](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener) | +| port | `number` | Optional, port to start | +| globalPrefix | `string` | Optional, the global http prefix | +| keys | `string[]` | Optional, Cookies signature, if the upper layer does not write keys, you can also set it here | +| hostname | `string` | Optional. The hostname to listen to. Default 127.1 | +| key | `string \| Buffer \| Array` | Optional, Https key, server private key | +| cert | `string \| Buffer \| Array` | Optional, Https cert, server certificate | +| ca | `string \| Buffer \| Array` | Optional, Https ca | +| http2 | `boolean` | Optional, supported by http2, default false | +| proxy | `boolean` | Optional, whether to enable the proxy. If it is true, the IP in the request request will be obtained first from the X-Forwarded-For in the Header field. The default is false. | +| subdomainOffset | `number` | Optional, the offset of the subdomain name, default 2. | +| proxyIpHeader | `string` | Optional. obtains the field name of the proxy ip address. The default value is X-Forwarded-For | +| maxIpsCount | `number` | Optional. the maximum number of ips obtained, which is 0 by default. | +| serverTimeout | `number` | Optional, server timeout configuration, the default is 2 * 60 * 1000 (2 minutes), in milliseconds | +| serverOptions | `Record` | Optional,http Server [Options](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener) | diff --git a/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/security.md b/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/security.md index a174a3bce1ec..936dd449b435 100644 --- a/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/security.md +++ b/site/i18n/en/docusaurus-plugin-content-docs/current/extensions/security.md @@ -313,7 +313,7 @@ There are three possible values for `X-Frame-Options`: | Configuration Item | Type | Description of action | Default | | --- | --- | --- | --- | | enable | boolean | Whether to open | false | -| policy | Object | Policy list | {} | +| policy | Object\ | Policy list | {} | | reportOnly | boolean | Whether to open | false | | supportIE | boolean | Does IE browser support | false | diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0.json b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0.json new file mode 100644 index 000000000000..32cc5f86ba28 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0.json @@ -0,0 +1,114 @@ +{ + "version.label": { + "message": "3.0.0", + "description": "The label for version current" + }, + "sidebar.common.category.新手指南": { + "message": "Quick Guide", + "description": "The label for category 新手指南 in sidebar common" + }, + "sidebar.common.category.基础": { + "message": "Fundamentals", + "description": "The label for category 基础 in sidebar common" + }, + "sidebar.common.category.进阶": { + "message": "Advanced", + "description": "The label for category 进阶 in sidebar common" + }, + "sidebar.common.category.设计模式": { + "message": "Design Patterns", + "description": "The label for category 设计模式 in sidebar common" + }, + "sidebar.common.category.自定义": { + "message": "Customize", + "description": "The label for category 自定义 in sidebar common" + }, + "sidebar.component.category.通用": { + "message": "Common", + "description": "The label for category 通用 in sidebar component" + }, + "sidebar.component.category.Http 服务": { + "message": "Http Service", + "description": "The label for category Http 服务 in sidebar component" + }, + "sidebar.component.category.数据存储": { + "message": "Data Storage", + "description": "The label for category 数据存储 in sidebar component" + }, + "sidebar.component.category.微服务": { + "message": "MicroService", + "description": "The label for category 微服务 in sidebar component" + }, + "sidebar.component.category.WebSocket": { + "message": "WebSocket", + "description": "The label for category WebSocket in sidebar component" + }, + "sidebar.component.category.消息": { + "message": "Message", + "description": "The label for category 消息 in sidebar component" + }, + "sidebar.component.category.监控和启动": { + "message": "Monitor And Bootstrap", + "description": "The label for category 监控和启动 in sidebar component" + }, + "sidebar.serverless.category.基础": { + "message": "Base", + "description": "The label for category 基础 in sidebar serverless" + }, + "sidebar.serverless.category.平台支持": { + "message": "Platform Support", + "description": "The label for category 平台触发器 in sidebar serverless" + }, + "sidebar.serverless.category.应用部署到弹性容器": { + "message": "Application Migrate", + "description": "The label for category 应用部署到弹性容器 in sidebar serverless" + }, + "sidebar.serverless.doc.介绍": { + "message": "Introduce", + "description": "The label for the doc item 介绍 in sidebar serverless, linking to the doc serverless/serverless_intro" + }, + "sidebar.serverless.doc.Serverless 触发器 POST 情况差异": { + "message": "Serverless trigger POST case differences", + "description": "The label for the doc item Serverless 触发器 POST 情况差异 in sidebar serverless, linking to the doc serverless/serverless_post_difference" + }, + "sidebar.category.基础功能": { + "message": "Fundamentals", + "description": "The label for category 基础功能 in sidebar hooks" + }, + "sidebar.category.一体化": { + "message": "Integration", + "description": "The label for category 一体化 in sidebar hooks" + }, + "sidebar.hooks.category.进阶": { + "message": "Advanced", + "description": "The label for category 进阶 in sidebar hooks" + }, + "sidebar.hooks.doc.介绍": { + "message": "Introduce", + "description": "The label for the doc item 介绍 in sidebar hooks, linking to the doc hooks/intro" + }, + "sidebar.other.category.工具": { + "message": "Tools", + "description": "The label for category 工具 in sidebar other" + }, + "sidebar.other.category.运维": { + "message": "OPS", + "description": "The label for category 运维 in sidebar other" + }, + "sidebar.other.category.常见问题": { + "message": "Common problem", + "description": "The label for category 常见问题 in sidebar other" + }, + "sidebar.other.category.其他": { + "message": "Other", + "description": "The label for category 其他 in sidebar other" + }, + "sidebar.category.开发工具": { + "message": "Development Tools", + "description": "The label for category 开发工具 in sidebar" + }, + "sidebar.category.历史文档": { + "message": "History Documents", + "description": "The label for category 历史文档 in sidebar" + } +} diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/aspect.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/aspect.md new file mode 100644 index 000000000000..ae4922470386 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/aspect.md @@ -0,0 +1,418 @@ +# Interceptors(AOP) + +We often have the need for global unified processing logic, such as unified processing errors, conversion formats, etc. Although Web middleware is available in Web scenarios, this capability cannot be used in other scenarios. + + +Midway has designed a set of general method interceptors (aspects) to write logic uniformly in different scenarios. + +Interceptor is different from traditional Web middleware and decorator. It is the ability provided by Midway framework. In the execution sequence, it is in the middle position. This ability can intercept any Class method. + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01DFfT1y1FC8xYeocrX_!!6000000000450-2-tps-823-133.png) + +## Using Interceptors (Aspects) + + +The interceptor is usually placed in the `src/aspect` directory. Let's write an example of intercepting the controller (Controller) method. Create a `src/aspect/report.ts` file. + + +``` +➜ my_midway_app tree +. +├── src +│ │── aspect ## interceptor directory +│ │ └── report.ts +│ └── controller ## Web Controller Directory +│ └── home.ts +├── test +├── package.json +└── tsconfig.json +``` +```typescript +// src/controller/home.ts + +import { Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + return "Hello Midwayjs!"; + } +} +``` + + +The content is as follows: +```typescript +import { Aspect, IMethodAspect, JoinPoint } from '@midwayjs/core'; +import { HomeController } from '../controller/home'; + +@Aspect(HomeController) +export class ReportInfo implements IMethodAspect { + async before(point: JoinPoint) { + console.log('before home router run'); + } +} + +``` +After the project is started, the `before home router run` is output in the console. + + +You will find that we don't need to hack into the controller's code, neither adding a decorator to the business file, nor adding code that is visible before and after the mainstream process. + + +The ability of the interceptor (section) is very powerful and terrible. We must use it carefully and correctly. + + +The interceptor is **fixed as a single instance**. + +:::caution +In the case of inheritance, the interceptor will not take effect on the methods of the parent class. +::: + + +## Aspectable Lifecycle + + +The method interceptor can intercept the whole method, and the way of interception includes several aspects. +```typescript +export interface IMethodAspect { + after?(joinPoint: JoinPoint, result: any, error: Error); + afterReturn?(joinPoint: JoinPoint, result: any): any; + afterThrow?(joinPoint: JoinPoint, error: Error): void; + before?(joinPoint: JoinPoint): void; + around?(joinPoint: JoinPoint): any; +} +``` + +| Methods | Description | +| --- | --- | +| before | Execute before method call | +| around | Before and after the execution of the package method | +| afterReturn | Execute when content is returned correctly | +| afterThrow | Execute when an exception is thrown | +| after | Final execution (whether correct or wrong) | + +A simple understanding is as follows; + +```javascript +try { + // before + // around or invokeMethod + // afterReturn +} catch(err) { + // afterThrow +} finally { + // after +} +``` +| | Revised input parameters | Call the original method | Gets the return value | Modify return value | Get error | Intercept and throw an error | +| --- | --- | --- | --- | --- | --- | --- | +| before | √ | √ | | | | | +| around | √ | √ | √ | √ | √ | √ | +| afterReturn | | | √ | √ | | | +| afterThrow | | | | | √ | √ | +| after | | | √ | | √ | | + + + +We often modify the input parameters and verify the parameters in the `before` process to conform to the logic of the program execution, for example: +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home(data1, data2) { + return data1 + data2; //Because the method is intercepted, the return value here is 3 + } +} + +// src/aspect/ +@Aspect(HomeController, 'home') // Only the home method is intercepted here. +export class ReportInfo implements IMethodAspect { + async before(point: JoinPoint) { + console.log(point.args); // Because the Controller method is cut, the original parameter is [ctx, next] + Point. args = [1, 2]; // Modify parameters + } +} + +``` +The `JoinPoint` here is the parameter that can be modified to the method, defined as follows. +```typescript +export interface JoinPoint { + methodName: string; + target: any; + args: any[]; + proceed(...args: any[]): any; +} +``` +| Parameters | Description | +| --- | --- | +| methodName | intercepted method name | +| target | The instance when the method is called. | +| args | The parameters of the original method call | +| proceed | The original method itself, only exists in before and around | + +`around` is a versatile method that can wrap the entire method call process. +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + return 'hello'; + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') // Only the home method is intercepted here. +export class ReportInfo implements IMethodAspect { + async around(point: JoinPoint) { + Const result = await point.proceed(...point.args); //Execute the original method + return result + 'world'; + } +} + +``` +Finally, Controller will return to `hello world`. + + +`afterReturn` method will have one more return result parameter. If you only need to modify the return result, you can directly use it. The above `around` example is easier to rewrite with `afterReturn`. +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + return 'hello'; + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') // Only the home method is intercepted here. +export class ReportInfo implements IMethodAspect { + async afterReturn(point: JoinPoint, result) { + return result + 'world'; + } +} + +``` +`afterThrow` is used to intercept errors. + + +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + throw new Error('custom error'); + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') +export class ReportInfo implements IMethodAspect { + async afterThrow(point: JoinPoint, error) { + if(/not found/.test(error.message)) { + throw new Error('another error'); + } else { + console.error('got custom error'); + } + } +} + +``` +`afterThrow` can intercept errors. Accordingly, it cannot return results in the process. It is generally used to record error logs. + + +`after` is used to perform the final processing. You can use it to perform some tasks, such as recording the number of successes or failures. + + +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + throw new Error('custom error'); + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') +export class ReportInfo implements IMethodAspect { + async after(point: JoinPoint, result, error) { + if(error) { + console.error(error); + } else { + console.log(result); + } + } +} + +``` + + +## Aspects of Asynchronous Issues + + +If the blocked method is asynchronous, in principle, all methods such as `before` should be asynchronous. Otherwise, all methods should be synchronous. +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home() { // here is asynchronous, then the following before is asynchronous + + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') +export class ReportInfo implements IMethodAspect { + async before(point: JoinPoint) { + + } +} + +``` +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + Home () { // here is synchronized, then the following before is also synchronized + + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') +export class ReportInfo implements IMethodAspect { + before(point: JoinPoint) { + + } +} + +``` +## Apply to multiple classes + + +The parameter of the `@Aspect` decorator can be an array. We can provide multiple classes. All methods **of these classes will be blocked. For example, we can apply the above interceptor to multiple Controller, so that every method of **every Class** will be intercepted. + + +```typescript +@Aspect([HomeController, APIController]) +export class ReportInfo implements IMethodAspect { + + async before(point: JoinPoint) { + + } +} +``` + + +## Specific method matching + + +In general, we only need to intercept a certain class-specific method. We provide some capabilities for matching methods. The second parameter decorated by `@Aspect` is a string of a wildwith method. The rule used is [picomatch](https://github.com/micromatch/picomatch). + + +Suppose our method is: + + +```typescript +// src/controller/home.ts + +import { Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/1') + async hello1() { + return "Hello Midwayjs!"; + } + + @Get('/2') + async hello2() { + return "Hello Midwayjs, too!"; + } +} +``` +Then, when you configure the following configuration, only the `hello2` method is matched. +```typescript +@Aspect([HomeController], '*2') +export class ReportInfo implements IMethodAspect { + + async before(point: JoinPoint) { + console.log('hello method with suffix 2'); + } +} +``` + + +## Aspect execution order + + +If multiple interceptors (sections) operate on one method at the same time, there may be a problem of disorder of order. If in two files, this order is random. + + +The third parameter of `@Aspect` is used to specify the priority of the interceptor. The default value is 0. The larger the number, the higher the priority. This means that the method is registered in the method first, and the **first registered method is called later,** that is, the onion model**.** + + +The following code is an example. The priority of `MyAspect2` is higher than that of `MyAspect1`, so registration will be given priority. The schematic diagram is as follows. The whole interception process is divided into two parts, first registration and then execution. + + +**Registration process** + + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01d31RXA1cpHyjyPHCs_!!6000000003649-2-tps-924-497.png) + + +**Execution process** + + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01RXmEtD26Thmkg8eX8_!!6000000007663-2-tps-769-311.png) + + + +The code is as follows. + +```typescript +@Aspect([HomeController]) +export class MyAspect1 implements IMethodAspect { + before(point: JoinPoint) { + console.log('111'); + } +} + +@Aspect([HomeController], '*', 1) // Priority can be set here +export class MyAspect2 implements IMethodAspect { + before(point: JoinPoint) { + console.log('222'); + } +} +``` + +The execution output is + +``` +111 +222 +``` + + +## Some restrictions + + +- 1. The interceptor will not take effect on the parent class diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/auto_run.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/auto_run.md new file mode 100644 index 000000000000..d00b3eb3fc2b --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/auto_run.md @@ -0,0 +1,69 @@ +# Self-executing code + +In the initialization process, when our code has nothing to do with the main process but wants to execute it, it will usually be executed in the startup onReady phase. As the amount of code increases, the onReady will become bloated. + +For example, we have some logic that needs to be executed in advance, one for listening for Redis errors and the other for initializing data synchronization: + +```typescript +@Provide() +@Scope(ScopeEnum.Singleton) +export class RedisErrorListener { + // ... +} + +@Provide() +@Scope(ScopeEnum.Singleton) +export class DataSyncListener { + // ... +} +``` + +In general, we will create an instance at startup by `getAsync` methods to make it execute. + +```typescript + + +// configuration.ts +//... + +@Configuration({ + // ... +}) +export class MainConfiguration { + async onReady(container) { + await container.getAsync(RedisErrorListerner); + await container.getAsync(DataSyncListerner); + } +} +``` + +In this way, once there is more code, there will be many unnecessary process codes in onReady. + + + +## Self-initialization + +If the code is not coupled to the main process and belongs to independent logic, such as listening to some events and initializing data synchronization, you can use the @Autoload decorator to enable a class to initialize itself. + +For example: + +```typescript +import { Autoload, Scope, ScopeEnum } from '@midwayjs/core'; + +@Autoload() +@Scope(ScopeEnum.Singleton) +export class RedisErrorListener { + @Init() + async init() { + const redis = new Redis(); + redis.on('xxx', () => { + // ... + }); + } +} +``` + +This automatically initializes without using the `getAsync` method in `onReady` and executes the init method. + + + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/awesome_midway.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/awesome_midway.md new file mode 100644 index 000000000000..39868d88a33a --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/awesome_midway.md @@ -0,0 +1,100 @@ +# Awesome Midway + +The following lists high-quality community projects related to Midwayjs + +## Microservices + +| Name | Author | Description | +| -------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@letscollab/midway-nacos][@lnulls] | Nawbc | midway nacos component | +| [midway-elasticsearch][midway-elasticsearch] | ddzyan | Mi1dway elasticsearch component | +| [midway-apollo][midway-apollo] | helloHT | Midway Ctrip Asynchronous Dynamic Configuration apollo Components | +| [@mwcp/cache][@mwcp/cache] | waitingsong | midway Cache Component supports [`Cacheable`][Cacheable], [`CacheEvict`][CacheEvict], [`CachePut`][CachePut] decorators and supports generics for [obtaining method parameter type][cache-generics] | +| [@mwcp/kmore][@mwcp/kmore] | waitingsong | midway Database QueryBuilder base on [Knex], declarative transaction via `Transactional` decorator, intergrated [OpenTelemetry] trace | +| [@mwcp/otel][@mwcp/otel] | waitingsong | midway [OpenTelemetry] component supports HTTP and [gRPC (Unary)], supports [`Trace`][Trace], [`TraceLog`][TraceLog], [`TraceInit`][TraceInit] decorators and supports generics for [obtaining method parameter type][otel-generics] | +| [@mwcp/jwt][@mwcp/jwt] | waitingsong | midway JWT component supports [`Public`][jwt-public] decorator | +| [@mwcp/paradedb][@mwcp/paradedb] | waitingsong | midway [ParadeDb] component. Postgres for Search & Analytics —— Modern Elasticsearch Alternative built on Postgres | +| [@mwcp/pgmq][@mwcp/pgmq] | waitingsong | midway [pqmg-js] component supports [`Consumer`][Consumer], [`PgmqListener`][PgmqListener] decorators. [PGMQ] is a lightweight message queue based on [PG] database, with native support for message persistence and delayed messages, similar to AWS SQS or RSMQ | + +| [midway-throttler][midway-throttler] | larryzhuo | midway throttler current limiting component | +| [邮件组件][mailer-zh] | MrDotYan | midway 邮箱组件,基于nodemailer和midwayjs,以服务的形式注入控制器使用[食用文档(国内)][mailer-zh-doc] | +## swagger + +| Name | Author | Description | +| -------------------------------------- | ------ | ----------------------- | +| [midwayjs-knife4j2][midwayjs-knife4j2] | Junyi | Midway swagger new skin | + +## Template rendering + +| Name | Author | Description | +| ---------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------- | +| [yuntian001/midway-vite-view][yuntian001/midway-vite-view] | yuntian001 | The midway vite server-side rendering (ssr)/client rendering (client) component supports vue3 react | + +## Community example + +| Name | Author | Description | +| ---------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [midwayjs-crud][midwayjs-crud] | DeveloperYvan | An example containing prisma + casbin + nacos + crud | +| [midway-practice][midway-practice] | ddzyan | An example of three mainstream ORM models (sequelize,typeORM,prisma) including request log link, unified response body, unified exception handling, and exception filter. | +| [midway-boot][midway-boot] | Code channel hero | A relatively complete best practice for back-end functions, including: addition, deletion, modification, check and base class encapsulation, database operation, cache operation, user security authentication and access security control, JWT access credentials, distributed access status management, password encryption and decryption, unified return result encapsulation, unified exception management, Snowflake primary key generation, Swagger integration and support for access authentication, use of environment variables, Docker image construction, Serverless release, etc. | +| [midway-vue3-ssr][midway-vue3-ssr] | LiQingSong | Based on the SSR framework assembled by Midway and Vue 3, it is simple, easy to learn and use, easy to expand and integrate Midway framework, which is the Vue SSR framework you have always wanted. | +| [midway-learn][midway-learn] | hbsjmsjwj | A demo for learning midway, including midway3 + egg + official Components & extensions (consul, JWT, typeorm, Prometheus, swagger, mysql2, gRPC, RabbitMQ) | + +## Learning materials + +| Name | Author | Description | +| --------------------------- | ----------------- | --------------------------------------- | +| Midway development practice | Code channel hero | https://edu.51cto.com/course/32086.html | + + +:::tip +Welcome everyone to contribute to the community, edit this page and add your favorite high-quality midway projects/components. +::: + + +[midway-elasticsearch]: https://github.com/ddzyan/midway-elasticsearch +[midway-apollo]: https://github.com/helloHT/midway-apollo +[@letscollab/midway-nacos]: https://github.com/deskbtm-letscollab/midway-nacos +[@mwcp/kmore]: https://github.com/waitingsong/kmore + +[@mwcp/cache]: https://github.com/waitingsong/midway-components/tree/main/packages/cache +[Cacheable]: https://github.com/waitingsong/midway-components/blob/main/packages/cache/README.md#cacheable-decorator +[CacheEvict]: https://github.com/waitingsong/midway-components/blob/main/packages/cache/README.md#cacheevict-decorator +[CachePut]: https://github.com/waitingsong/midway-components/blob/main/packages/cache/README.md#cacheput-decorator + +[@mwcp/otel]: https://github.com/waitingsong/midway-components/tree/main/packages/otel +[Trace]: https://github.com/waitingsong/midway-components/tree/main/packages/otel#trace-decorator +[TraceLog]: https://github.com/waitingsong/midway-components/tree/main/packages/otel#tracelog-decorator +[TraceInit]: https://github.com/waitingsong/midway-components/tree/main/packages/otel#traceinit-decorator +[otel-generics]: https://github.com/waitingsong/midway-components/tree/main/packages/otel#auto-parameter-type-of-keygenerator-from-generics +[otel-generics-cn]: https://github.com/waitingsong/midway-components/blob/main/packages/otel/README.zh-CN.md#%E4%BB%8E%E6%B3%9B%E5%9E%8B%E5%8F%82%E6%95%B0%E8%87%AA%E5%8A%A8%E8%8E%B7%E5%8F%96%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E5%8F%82%E6%95%B0%E7%B1%BB%E5%9E%8B +[cache-generics]: https://github.com/waitingsong/midway-components/tree/main/packages/cache#auto-parameter-type-of-keygenerator-from-generics +[cache-generics-cn]: https://github.com/waitingsong/midway-components/blob/main/packages/cache/README.zh-CN.md#%E4%BB%8E%E6%B3%9B%E5%9E%8B%E5%8F%82%E6%95%B0%E8%87%AA%E5%8A%A8%E8%8E%B7%E5%8F%96%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E5%8F%82%E6%95%B0%E7%B1%BB%E5%9E%8B + +[@mwcp/jwt]: https://github.com/waitingsong/midway-components/tree/main/packages/jwt +[jwt-public]: https://github.com/waitingsong/midway-components/blob/main/packages/jwt/README.md#public-decorator + +[@mwcp/paradedb]: https://github.com/waitingsong/paradedb/tree/main/packages/mwcp-paradedb +[ParadeDB]: https://www.paradedb.com/ + +[@mwcp/pgmq]: https://github.com/waitingsong/pgmq-js/tree/main/packages/mwcp-pgmq-js +[PGMQ]: https://tembo-io.github.io/pgmq/ +[PG]: https://pigsty.cc/blog/pg/pg-eat-db-world/ +[pqmg-js]: https://github.com/waitingsong/pgmq-js/tree/main/packages/pgmq-js +[Consumer]: https://github.com/waitingsong/pgmq-js/tree/main/packages/mwcp-pgmq-js#consumer-decorator +[PgmqListener]: https://github.com/waitingsong/pgmq-js/tree/main/packages/mwcp-pgmq-js#consumer-decorator + +[midwayjs-knife4j2]: https://github.com/fangbao-0418/midway/tree/master/packages/swagger +[yuntian001/midway-vite-view]: https://github.com/yuntian001/midway-vite-view + +[midwayjs-crud]: https://github.com/developeryvan/midwayjs-crud +[midway-practice]: https://github.com/ddzyan/midway-practice +[midway-boot]: https://github.com/bestaone/midway-boot +[midway-vue3-ssr]: https://github.com/lqsong/midway-vue3-ssr +[midway-learn]: https://github.com/hbsjmsjwj/midway-learn.git +[midway-throttler]: https://github.com/larryzhuo/midway-throttler + +[Knex]: https://knexjs.org/ +[OpenTelemetry]: https://github.com/open-telemetry + +[gRPC (Unary)]: https://github.com/midwayjs/midway/tree/main/packages/grpc diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/built_in_service.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/built_in_service.md new file mode 100644 index 000000000000..ae2008eabff7 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/built_in_service.md @@ -0,0 +1,636 @@ +# Built-in service + +In Midway, many built-in objects are provided for the convenience of users. + +In this section, we will introduce the Application associated with the framework, Context objects, and some service objects on Midway default containers, which are often encountered in the entire business development. + +The following are some services built into the Midway dependency injection container. These services are initialized by the dependency injection container and are globally available in the business. + + + +## MidwayApplicationManager + +Midway's built-in application manager can be used to get all the Application. + +It can be obtained by injection, such as adding the same middleware to different applications. + +```typescript +import { MidwayApplicationManager, Configuration, Inject } from '@midwayjs/core'; +import { CustomMiddleware } from './middleware/custom.middleware'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + applicationManager: MidwayApplicationManager; + + async onReady() { + this.applicationManager + .getApplications(['koa', 'faas', 'express', 'egg']) + .forEach(app => { + app.useMiddleware(CustomMiddleware); + }); + } +} +``` + +| API | return type | Description | +| ------------------------------------ | -------------------- | ------------------------------------------------------ | +| getFramework(namespace: string) | IMidwayFramework | Returns the framework specified by the parameter | +| getApplication(namespace: string) | IMidwayApplication | Returns the Application specified by the parameter | +| getApplications(namespace: string[]) | IMidwayApplication[] | Returns multiple Application specified by the parameter | +| getWebLikeApplication() | IMidwayApplication [] | Returns Application similar to Web scenarios (express/koa/egg/faas) | + + + +## MidwayInformationService + +Midway's built-in information service provides basic project data. + +It can be obtained by injection. + +```typescript +import { MidwayInformationService, Inject, Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + informationService: MidwayInformationService; + + @Get('/') + async home() { + // this.informationService.getAppDir(); + } +} +``` + +Generally used to return user-related directories. + +| API | Return type | Description | +| ------------ | -------- | ------------------------------------------------------- | +| getAppDir() | String | Return to the application root directory | +| getBaseDir() | String | Returns the application code directory. By default, the local development is src and the server running is dist. | +| getHome | String | Return to the machine user directory, which refers to the address. | +| getPkg | Object | Returns the contents of the package.json | +| getRoot | String | In the development environment, return to appDir, and in other environments, return to the Home directory. | + + + +## MidwayEnvironmentService + +Midway's built-in environmental services provide environmental settings and judgments. + +It can be obtained by injection. + +```typescript +import { MidwayEnvironmentService, Inject, Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + environmentService: MidwayEnvironmentService; + + @Get('/') + async home() { + // this.environmentService.getCurrentEnvironment(); + } +} +``` + +Generally used to obtain the current environment, the API is as follows: + +| API | Return type | Description | +| ------------------------ | -------- | ------------------ | +| getCurrentEnvironment() | String | Return to Apply Current Environment | +| setCurrentEnvironment() | | Set current environment | +| isDevelopmentEnvironment | Boolean | Judging whether it is a development environment | + + + +## MidwayConfigService + +Midway's built-in multi-environment configuration service provides the loading and obtaining of configurations. It is also the data source of the `@Config` decorator. + +It can be obtained by injection. + +```typescript +import { MidwayConfigService, Inject, Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + configService: MidwayConfigService; + + @Get('/') + async home() { + // this.configService.getConfiguration(); + } +} +``` + +Generally used to obtain the current configuration, the API is as follows: + +| API | Return type | Description | +| ------------------ | -------- | ------------------------ | +| addObject(obj) | | Dynamically add configuration objects | +| getConfiguration() | Object | Returns the currently merged configuration object | +| clearAllConfig() | | Clear all configurations | + + + +## MidwayLoggerService + +The built-in log service of Midway provides API operations such as log creation and retrieval. It is also the data source of the `@Logger` decorator. + +It can be obtained by injection. + +```typescript +import { MidwayLoggerService, Inject, Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + loggerService: MidwayLoggerService; + + @Get('/') + async home() { + // this.loggerService.getLogger('logger'); + } +} +``` + +Generally, it is used to obtain log objects. The API is as follows: + +| API | Return type | Description | +| ---------------------------- | -------- | ------------------------------ | +| createInstance(name, config) | ILogger | Dynamically create a Logger instance | +| getLogger(name) | ILogger | Returns a Logger instance based on the log name. | + + + +## MidwayFrameworkService + +Midway's built-in custom framework service, combined with the custom `@Framework` marked Class in the component, provides external services of different protocols. + +It can be obtained by injection. + +```typescript +import { MidwayFrameworkService, Inject, Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + frameworkService: MidwayFrameworkService; + + @Get('/') + async home() { + // this.frameworkService.getMainFramework(); + } +} +``` + +Generally used to obtain Framework objects, the API is as follows: + +| API | return type | Description | +| --------------------------------- | ------------------ | ---------------------------------- | +| getMainFramework() | IMidwayFramework | Returns the main frame instance | +| getMainApp() | IMidwayApplication | Returns the app object in the main frame | +| getFramework(nameOrFrameworkType) | IMidwayFramework | Returns a frame instance based on the frame name or frame type | + + + +## MidwayMiddlewareService + +Midway's built-in middleware processing service is used for the processing of self-built middleware. + +Midway's built-in custom decorator service is used to implement frame-level custom decorators, which are generally used when customizing frames. + +It can be obtained by injection. + +```typescript +import { MidwayMiddlewareService, Inject, Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + middlewareService: MidwayMiddlewareService; + + @Get('/') + async home() { + // this.middlewareService.com pose(/**omitted**/); + } +} +``` + +The API is as follows: + +| API | return type | Description | +| -------------------------------- | ----------- | -------------------------------------------- | +| compose(middlewares, app, name?) | IMiddleawre | Combine multiple middleware arrays together to return a new middleware | + + + +## MidwayDecoratorService + +Midway's built-in custom decorator service is used to implement custom decorators at the frame level. + +It can be obtained by injection. + +```typescript +import { MidwayDecoratorService, Inject, Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + decoratorService: MidwayDecoratorService; + + @Get('/') + async home() { + // this.decoratorService.registerPropertyHandler(/* omitted */); + } +} +``` + +The API is as follows: + +| API | return type | Description | +| ----------------------------------------------- | -------- | ---------------------- | +| registerPropertyHandler(decoratorKey, handler) | | Add a property decorator implementation | +| registerMethodHandler(decoratorKey, handler) | | Add a method decorator implementation | +| registerParameterHandler(decoratorKey, handler) | | Add a parameter decorator implementation | + +For more information, see **Custom decorator**. + + + +## MidwayAspectService + +Midway's built-in interceptor service is used to load `@Aspect` related capabilities. Custom decorators also use this service. + +It can be obtained by injection. + +```typescript +import { MidwayAspectService, Inject, Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + aspectService: MidwayAspectService; + + @Get('/') + async home() { + // this.aspectService.interceptPrototypeMethod(/* omitted */); + } +} +``` + +The API is as follows: + +| API | Return type | Description | +| ------------------------------------------------------------------------ | -------- | ---------------------------------------- | +| addAspect(aspectInstance, aspectData) | | Add an interceptor implementation | +| interceptPrototypeMethod(Clazz, methodName, aspectObject: IMethodAspect) | | The method on the interception prototype is added to the implementation of the interceptor. | + + + +## MidwayLifeCycleService + +Midway's built-in lifecycle run service is used to run the lifecycle in the `configuration`. + +This service is an internal method and cannot be used directly by users. + + + +## MidwayMockService + +Midway's built-in data simulation service is used to simulate data during development and testing. + +It can be obtained by injection. + +```typescript +import { MidwayMockService, Inject, Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + mockService: MidwayMockService; + + @Get('/') + async home() { + // this.mockService.mockProperty(/** omitted **/); + } +} +``` + +API is as follows + +| API | Return type | Description | +| -------------------------------------------- | ----------- | ----------------------------------------- | +| mockClassProperty(clzz, propertyName, value, group?) | | Mock a property (method) on a class, supports grouping, default group is `default` | +| mockProperty(obj, key, value, group?) | | Mock a property (method) on a normal object, supports grouping, default group is `default` | +| mockContext(app, key, value, group?) | | Mock properties on context objects, supports grouping, default group is `default` | +| restore(group?) | | Restore mock data for the specified group, restore all if not specified | +| restoreAll() | | Clear all mock data | + +### mockClassProperty + +Used to simulate a property or method of a class. Supports specifying a group through the `group` parameter. If the `group` parameter is not passed, the default group `default` is used. + +```typescript +@Provide() +export class UserService { + data; + + async getUser() { + return 'hello'; + } +} +``` + +We can also simulate in code. + +```typescript +import { MidwayMockService, Provide, Inject } from '@midwayjs/core'; + +@Provide() +class TestMockService { + @Inject() + mockService: MidwayMockService; + + mock() { + // Simulate property, use default group + this.mockService.mockClassProperty(UserService, 'getUser', async () => { + return 'midway'; + }); + + // Simulate property, specify group + this.mockService.mockClassProperty(UserService, 'data', { + bbb: '1' + }, 'group2'); + } +} +``` + +### mockProperty + +Use the `mockProperty` method to simulate the properties of objects. Supports specifying a group through the `group` parameter. + +```typescript +import { MidwayMockService, Provide, Inject } from '@midwayjs/core'; + +@Provide() +class TestMockService { + @Inject() + mockService: MidwayMockService; + + mock() { + const a = {}; + // Default group + this.mockService.mockProperty(a, 'name', 'hello'); + // Simulate property, custom group + this.mockService.mockProperty(a, 'name', 'hello', 'group1'); + // a['name'] => 'hello' + + // Simulate method + this.mockService.mockProperty(a, 'getUser', async () => { + return 'midway'; + }, 'group2'); + // await a.getUser() => 'midway' + } +} +``` + +### mockContext + +Since Midway's Context is associated with app, app instances need to be passed in during simulation. Supports specifying a group through the `group` parameter. + +Use the `mockContext` method to simulate the context. + +```typescript +import { MidwayMockService, Configuration, App } from '@midwayjs/core'; + +@Configuration(/**/) +export class MainConfiguration { + @Inject() + mockService: MidwayMockService; + + @App() + app; + + async onReady() { + // Simulate context, default group + this.mockService.mockContext(app, 'user', 'midway'); + // Custom group + this.mockService.mockContext(app, 'user', 'midway', 'group1'); + } +} + +// ctx.user => midway +``` + +If your data is complex or logical, you can also use the callback form. + +```typescript +import { MidwayMockService, Configuration, App } from '@midwayjs/core'; + +@Configuration(/**/) +export class MainConfiguration { + @Inject() + mockService: MidwayMockService; + + @App() + app; + + async onReady() { + // Simulate context + this.mockService.mockContext(app, (ctx) => { + ctx.user = 'midway'; + }, 'group2'); + } +} + +// ctx.user => midway +``` + +Note that this mock behavior is executed before all middleware. + + + +## MidwayWebRouterService + +Midway's built-in routing table service is used to apply routing and function creation. + +It can be obtained by injection. + +```typescript +import { MidwayWebRouterService, Configuration, Inject } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + webRouterService: MidwayWebRouterService; + + async onReady() { + this.webRouterService.addRouter(async (ctx) => { + return 'hello world'; + }, { + url: '/', + requestMethod: 'GET', + }); + } +} + +``` + +API is as follows + +| API | Return type | Description | +| ------------------------------------------------- | ---------------------------------- | -------------------------------------- | +| addController(controllerClz, controllerOption) | | Dynamically add a Controller | +| addRouter(routerFunction, routerInfoOption) | | Dynamically add a routing function | +| getRouterTable() | Promise\> | Get hierarchical routes | +| getFlattenRouterTable() | Promise\ | Get a list of flat routes | +| getRoutePriorityList() | Promise\ | Get the route prefix list | +| getMatchedRouterInfo(url: string, method: string) | Promise\ | Returns the current matching route information based on the access path | + +For more information, see [Web route table](# router_table). + + + +## MidwayServerlessFunctionService + +Midway's built-in function information service inherits and `MidwayWebRouterService` in almost the same way. + +It can be obtained by injection. + +```typescript +import { MidwayServerlessFunctionService, Configuration, Inject } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + serverlessFunctionService: MidwayServerlessFunctionService; + + async onReady() { + this.serverlessFunctionService.addServerlessFunction(async (ctx, event) => { + return 'hello world'; + }, { + type: ServerlessTriggerType.HTTP, + metadata: { + method: 'get', + path: '/api/hello' + }, + functionName: 'hello', + handlerName: 'index.hello', + }); + } +} + +``` + +API is as follows + +| API | Return type | Description | +| ---------------------------------------------------------- | --------------------- | ---------------- | +| addServerlessFunction(fn, triggerOptions, functionOptions) | | Dynamically add a function | +| getFunctionList() | Promise\ | Get a list of all functions | + +For more information, see [Web route table](# router_table). + + + +## MidwayHealthService + +Midway's built-in health check execution service is used for externally extended health check capabilities. + +A complete health check consists of two parts: + +* 1. The trigger end of the health check, such as an external scheduled request, is usually an Http interface +* 2. The execution end of the health check usually checks whether specific items are normal in each component or business. + +`MidwayHealthService` is generally used as the trigger end of health check. The content described below is generally implemented on the trigger end. + +It can be obtained through injection and then perform health check tasks. + +```typescript +import { MidwayHealthService, Configuration, Inject } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + healthService: MidwayHealthService; + + async onServerReady() { + setInterval(() => { + const results = await this.healthService.getStatus(); + + // console.log(results); + // => + // { + // "status": false + // "namespace": "redis", + // "reason": "health check timeout", + // "results": [ + // { + // "status": false + // "reason": "health check timeout", + // "namespace": "redis" + // } + // ] + // } + + }, 1000); + // ... + } +} +``` + +The API is as follows + +| API | Return Type | Description | +| -------------------------------- |-------------------------| -------------------------- | +| getStatus() | Promise\ | Dynamically add a function | +| setCheckTimeout(timeout: number) | void | Set timeout | + +The `getStatus` method is used to externally call the `onHealthCheck` method in polling `configuration` and return a data that conforms to the `HealthResults` structure. + + `HealthResults` contains several fields. `status` indicates whether the check is successful. If it fails, `reason` indicates the reason for the first failed component. `namespace` represents the name of the first failed component. `results` It means that all the returned items are checked this time, and the structure of the returned items is the same as the external one. + +When executing the process, if the following conditions occur in the `onHealthCheck` method, it will be marked as failed. + +* 1. No data conforming to the `HealthResult` structure was returned. +* 2. No value returned +* 3. Execution timeout +* 4. Throw an error +* 5. Return error data that conforms to the `HealthResult` structure, such as `{status: false}` + +The default waiting timeout for health checks is 1s. + +Can be overridden using global configuration. + +```typescript +//config.default +export default { + core: { + healthCheckTimeout: 2000, + } +}; +``` + +The execution end of the health check is implemented in the life cycle of the business or component. For details, please see [Life Cycle](/docs/lifecycle#onhealthcheck). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/change_start_dir.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/change_start_dir.md new file mode 100644 index 000000000000..51e6277bc732 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/change_start_dir.md @@ -0,0 +1,118 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Change Source Dir + +In some special scenarios, you can modify the `src` directory where the source code is located. + + +Some restrictions: + +- 1. @midwayjs/web(egg)egg cannot be modified due to fixed directory +- 2. Only pass the test under pure node project (non-integration) + +## Modification of Source Code Directory + +Below, we will change the `src` directory to `server` as an example. + +### dev development + +The Dev command in `package.json` needs to add a source directory to facilitate Dev search. + + + + + +The `outDir` field in `tsconfig.json` is recognized by default and no adjustment is required. + + + + + +```typescript +"dev": "cross-env NODE_ENV=local midway-bin dev --sourceDir=./server --ts", +``` + + + + + + +### build compilation + + + + + +The `outDir` field in `tsconfig.json` is recognized by default and no adjustment is required. + + + + + +In order for tsc compilation to find the source directory, it is necessary to modify the `tsconfig.json` and add `rootDir` fields. + +```typescript +{ + "compileOnSave": true + "compilerOptions": { + // ... + "rootDir": "server" + }, +} +``` + +In this way, development and compilation are normal. + + + + + + +## Modification of Compiled Directory + +Compiling the directory affects the deployment and can also be modified. In this example, change the `dist` directory to `build`. + +### build compilation + +Modify the `outDir` field in the `tsconfig.json`. + +```typescript +{ + "compileOnSave": true + "compilerOptions": { + // ... + "outDir": "build" + }, + "exclude": { + "build", + //... + } +} +``` + +So the compilation is normal. + + +### bootstrap startup + + +After the compilation directory is modified, the online deployment will not find the code, so if the `bootstrap.js` is started, the code needs to be modified. + +```typescript +// bootstrap.js + +const { join } = require('path'); +const { Bootstrap } = require('@midwayjs/bootstrap'); + +//... + +// configure method is required to configure baseDir +Bootstrap + .configure({ + baseDir: join(__dirname, 'build') + }) + .run(); +``` + +Configure the portal directory for the `Bootstrap`. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/component_development.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/component_development.md new file mode 100644 index 000000000000..f7c03c1225bb --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/component_development.md @@ -0,0 +1,719 @@ +# Custom component + +A component (Component) is a reusable and multi-frame module package, which is generally used in several scenarios: + +- 1. Package the code called downstream, and package the three-party module to simplify the use, such as orm (database call),swagger (simplified use), etc. +- 2. Reusable business logic, such as abstract public Controller,Service, etc. + +Components can be loaded locally or packaged together and published into an npm package. Components can be used in midway v3/Serverless. You can put reusable business codes or functional modules into components for maintenance. Almost all Midway general capabilities can be used in components, including but not limited to configuration, life cycle, controller, interceptor, etc. + +When designing components, we should face all upper-level frame scenarios as much as possible, so we only rely on `@midwayjs/core` as much as possible. + +Starting from v3, the framework (Framework) has also become part of the component, and the usage and component are unified. + + + +## Development component + +### Boilerplate + +Just execute the script below and select the `component-v3` template in the template list to quickly generate a sample component. + +```bash +$ npm init midway@latest -y +``` + +Note [Node.js environment requirements](/docs/intro#environmental-preparation). + + +### Component directory + +The structure of the component is the same as the recommended directory structure of midway. The directory structure of the component is not specifically specified and can be consistent with the application or function. Simply understood, a component is a "mini application". + +A recommended component directory structure is as follows. + +``` +. +├── package.json +├── src +│ ├── index.ts // Entry export file +│ ├── configuration.ts // Component behavior configuration +│ └── service // Logical Code +│ └── bookService.ts +├── test +├── index.d.ts // component extension definition +└── tsconfig.json +``` + +For components, the only specification is the `Configuration` attribute exported by the entry, which must be a Class with a `@Configuration` decorator. + +Generally speaking, our code is TypeScript standard directory structure, which is the same as Midway system. + +At the same time, it is an ordinary Node.js package, which needs to use the `src/index.ts` file as the entry to export the content + +Let's take a very simple example to demonstrate how to write a component. + + + +### Component Lifecycle + +Like the application, the component also uses `src/configuration.ts` as the entry startup file (or the application is a large component). + +The code and application are exactly the same. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; + +@Configuration({ + namespace: 'book' +}) +export class BookConfiguration { + async onReady() { + // ... + } +} +``` + +The only difference is that you need to add a `namespace` as the namespace for the component. + +The code for each component is a separate scope so that even if a class with the same name is exported, it will not conflict with other components. + +It is the same as the [lifecycle extension](lifecycle) capability that is common to the entire Midway. + + + +### Component logic code + +Same as the application, write the class export, and the dependent injection container is responsible for management and loading. + +```typescript +// src/service/book.service.ts +import { Provide } from '@midwayjs/core'; + +@Provide() +export class BookService { + async getBookById() { + return { + data: 'hello world', + } + } +} +``` + +:::info +A component does not rely on a clear upper-level framework. In order to reuse in different scenarios, it only depends on the common `@midwayjs/core`. +::: + + + +### Component configuration + +The configuration is the same as that of the application. For more information, see [Configure multiple environments](env_config). + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as DefaultConfig from './config/config.default'; +import * as LocalConfig from './config/config.local'; + +@Configuration({ + namespace: 'book', + importConfigs: [ + { + default: DefaultConfig, + local: LocalConfig + } + ] +}) +export class BookConfiguration { + async onReady() { + // ... + } +} +``` + +There is an important feature in v3. After the component is loaded, the `MidwayConfig` definition will include the definition of the component configuration. + +To do this, we need to write the definition of the configuration independently. + +Add the configuration definition to the `index.d.ts` file in the root directory. + +```typescript +// Because the default type export position is modified, additional types under dist need to be exported +export * from './dist/index'; + +// Standard extension statement +declare module '@midwayjs/core/dist/interface '{ + + // Merge the configuration into the MidwayConfig + interface MidwayConfig { + book ?: { + // ... + }; + } +} + +``` + +At the same time, the `package.json` of the component is also modified. + +```json +{ + "name": "****", + "main": "dist/index.js", + "typings": "index.d.ts", // The type export file here uses the project root directory's + // ... + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "index.d.ts" // This file needs to be brought with you when you publish it. + ], +} +``` + + + +### Component convention + +The components and the application itself are slightly different, mainly in the following aspects. + +- 1. The code of the component needs to export a `Configuration` attribute, which must be a Class with a `@Configuration` decorator to configure the component's own capabilities +- 2. All **explicitly exported codes** will be loaded by the dependent injection container. Simply put, all classes **decorated by decorators** need to be exported, including controllers, services, middleware, etc. + +For example: + +```typescript +// src/index.ts +export { BookConfiguration as Configuration } from './configuration'; +export * from './service/book.service'; +``` + +:::info +In this way, only the `service/book.service.ts` file in the project will be scanned and loaded by the dependent injection container. +::: + +And specify the main path in the `package.json`. + +```typescript +"main": "dist/index" +``` + +In this way, the component can be loaded by the upper scene dependency. + + + +### Test components + +Testing a single service can be executed by starting an empty business and specifying the current component. + +```typescript +import { createLightApp } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + const app = await createLightApp ('', { + imports: [ + custom + ] + }); + const bookService = await app.getApplicationContext().getAsync(custom.BookService); + expect(await bookService.getBookById()).toEqual('hello world'); + }); +}); + +``` + +If the component is part of the Http protocol process and strongly relies on context and must rely on an Http framework, then use a complete project example and use `createApp` to test. + +```typescript +import { createApp, createHttpRequest } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + // In the sample project, you need to rely on @midwayjs/koa or other peer-to-peer frameworks. + const app = await createApp(join(__dirname, 'fixtures/base-app'), { + imports: [ + custom + ] + }); + + const result = await createHttpRequest(app).get('/'); + // ... + + }); +}); + + +``` + + + +### Depends on other components + +If a component depends on a class in another component and is the same as the application, it needs to be declared at the entrance, and the framework will load and handle the duplication in the order of the module. + +If you explicitly rely on a class in a component, it must be a strong dependency of that component. + +For example: + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as axios from '@midwayjs/axios'; + +@Configuration({ + namespace: 'book', + imports: [axios] +}) +export class BookConfiguration { + async onReady() { + // ... + } +} +``` + +There is also a case of weak dependencies, which do not need to be explicitly declared, but require additional judgment. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import { IMidwayContainer } from '@midwayjs/core'; + +@Configuration({ + namespace: 'book', +}) +export class BookConfiguration { + async onReady(container: IMidwayContainer) { + // ... + if (container.hasNamespace('axios')) { + // Execute only when axios component is loaded + } + // ... + } +} +``` + +Increase dependency. + +```json +// package.json +{ + "dependencies": { + "@midwayjs/axios": "xxxx" + } +} +``` + +In the `index.d.ts` directory of the root directory, add the component definitions that are dependent on the explicit import. + +```typescript +// Explicitly import dependent components +import '@midwayjs/axios'; +export * from './dist/index'; + +// ... + +``` + +:::tip + +If the main application does not explicitly rely on axios, the code execution is normal, but the typescript axios definition will not be scanned, resulting in no axios definition when writing the configuration. the above code can solve this problem. + +::: + + +### Development components in applications + +It is recommended to use [lerna](https://github.com/lerna/lerna) and enable lerna's hoist mode to write components. If you want to develop a component in a non-lerna scenario, make sure that the component is in the `src` directory. Otherwise, the component may fail to be loaded. + +#### Use lerna + +Development using lerna is relatively simple, and the specific directory structure is similar to the following. + +``` +. +├── src +├── packages/ +│ ├── component-A +│ │ └── package.json +│ ├── component-B +│ │ └── package.json +│ ├── component-C +│ │ └── package.json +│ └── web +│ └── package.json +├── lerna.json +└── package.json +``` + +#### Non-lerna + +The following is a common component development method. The sample structure is to develop two components at the same time during application code development. Of course, you can also customize the directory structure you like. + +``` +. +├── package.json +├── src // source directory +│ ├── components +│ │ ├── book // book component code +│ │ │ ├── src +│ │ │ │ ├── service +│ │ │ │ │ └── bookService.ts +│ │ │ │ ├── configuration.ts +│ │ │ │ └── index.ts +│ │ │ └── package.json +│ │ │ +│ │ └── school +│ │ ├── src +│ │ │ ├── service // school component code +│ │ │ │ └── schoolService.ts +│ │ │ └── configuration.ts +│ │ └── package.json +│ │ +│ ├── configuration.ts // Application Behavior Profile +│ └── controller // Application Routing Directory +├── test +└── tsconfig.json +``` + +Component behavior configuration. + +```typescript +// src/components/book/src/bookConfiguration.ts +import { Configuration } from '@midwayjs/core'; + +@Configuration() +export class BookConfiguration {} +``` + +In order for components to export, we need to export `Configuration` attributes at the entry of the component `src/components/book/src/index.ts`. + +```typescript +// src/components/book/src/index.ts +export { BookConfiguration as Configuration } from './bookConfiguration/src'; + +``` + +:::info +Note that the place quoted here is "./xxxx/src", because generally the main field in our package.json points to dist/index. If you want the code not to be modified, then the main field should point to src/index, and it will be published in Remember to modify it back to dist. + +The directory introduced by the component is pointed to src so that the save takes effect automatically (restart). +::: + +In addition, scanning conflicts may occur in the new version. The dependency injection conflict checking function in `configuration.ts` can be turned off. + + + +### Use components + +In any midway series application, this component can be introduced in the same way. + +First, add dependencies to the application. + +```json +// package.json +{ + "dependencies": { + "midway-component-book": "*" + } +} +``` + +This component is then introduced in the application (function). + +```typescript +// src/configuration.ts of application or function +import { Configuration } from '@midwayjs/core'; +import * as book from 'midway-component-book'; + +@Configuration({ + imports: [book] +}) +export class MainConfiguration {} +``` + +At this point, our preparations have been completed and we will start to use them. + +Class injection that directly introduces components. + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { BookService } from 'midway-component-book'; + +@Provide() +export class Library { + + @Inject(); + bookService: BookService; + +} +``` + +For the rest, if the component has specific capabilities, please refer to the documentation of the component itself. + + + +### Component publishing + +A component is an ordinary Node.js package that can be compiled and published to npm for distribution. + +```bash +## Compile and publish the corresponding component +$ npm run build && npm publish +``` + + + +### Component example + +[Here](https://github.com/czy88840616/midway-test-component) is an example of a component. It has been published to npm and can be tried to directly introduce it into the project to start execution. + + + +## Development Framework (Framework) + +In v3, components can contain a Framework to provide different services. Using the life cycle, we can extend the provision of gRPC,Http and other protocols. + +The Framework here is just a special business logic file in the component. + +For example: + +``` +. +├── package.json +├── src +│ ├── index.ts // Entry export file +│ ├── configuration.ts // Component behavior configuration +│ └── framework.ts // Framework code +│ +├── test +├── index.d.ts // Component extension definition +└── tsconfig.json +``` + + + + + +### Expand existing Framework + +As mentioned above, the Framework is part of the component and also follows the component specification, which can be injected and extended. + +Let's take the extension `@midwayjs/koa` as an example. + +First, create a custom component, which is the same as a common application. Because `@midwayjs/koa` needs to be extended, we need to rely on `@midwayjs/koa` in the component. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + namespace: 'myKoa', + imports: [koa] +}) +export class MyKoaConfiguration { + async onReady() { + // ... + } +} +``` + +Then, we can inject the framework exported by `@midwayjs/koa` for extension. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + namespace: 'myKoa', + imports: [koa] +}) +export class MyKoaConfiguration { + @Inject() + framework: koa.Framework; + + async onReady() { + // Add middleware, app.useMiddleware in koa actually proxy the framework method + this.framework.useMiddleware(/* ... */); + + // Add filter, app.useFilter in koa actually proxy the framework method + this.framework.useFilter(/* ... */); + + // koa's own expansion capabilities, such as expansion context + const app = this.framework.getApplication(); + Object.defineProperty(app.context, 'user', { + get() { + // ... + return 'xxx'; + }, + enumerable: true + }); + // ... + } + + async onServerReady() { + const server = this.framework.getServer(); + // server.xxxx + } +} +``` + +This is a method of scaling based on existing Framework. + +- If the context is extended in the component, refer to the [extended context definition](./context_definition). +- For more information about how to configure a widget, see [Configure a widget](# Component Configuration). + +After the component is released, such as `@midwayjs/my-koa`, the business can directly use your component without introducing `@midwayjs/koa`. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +// Your own components +import * as myKoa from '@midwayjs/my-koa'; + +@Configuration({ + imports: [myKoa], +}) +export class MyConfiguration { + async onReady() { + // ... + } +} +``` + +If you want to fully define your own components, such as different protocols, you need to fully customize the Framework. + + + +### Write Framework + +The frameworks all follow the `IMidwayFramewok` interface definitions and the following conventions. + +- Each framework has a separate start-stop process to be customized. +- Each framework needs to define its own independent `Application`, `Context` +- Each framework can have its own independent middleware capabilities + +In order to simplify development, Midway provides a basic `BaseFramework` class for inheritance. + +```typescript +import { Framework } from '@midwayjs/core'; +import { BaseFramework, IConfigurationOptions, IMidwayApplication, IMidwayContext } from '@midwayjs/core'; + +// Define Context +export interface Context extends IMidwayContext { + // ... +} + +// Define Application +export interface Application extends IMidwayApplication { + // ... +} + +// Frame configuration +export interface IMidwayCustomConfigurationOptions extends IConfigurationOptions { + // ... +} + +// Implement a custom framework and inherit the basic framework +@Framework() +export class MidwayCustomFramework extends BaseFramework { + + // Process initialization configuration + configure() { + // ... + } + + // app initialization + async applicationInitialize() { + // ... + } + + // Framework startup, such as listen + async run() { + // ... + } +} +``` + + + +### Custom example + +Next, we will take the implementation of a basic HTTP service framework as an example. + +```typescript +import { BaseFramework, IConfigurationOptions, IMidwayApplication, IMidwayContext } from '@midwayjs/core'; +import * as http from 'http'; + +// Define the definitions to be used by some upper-level businesses. +export interface Context extends IMidwayContext {} + +export interface Application extends IMidwayApplication {} + +export interface IMidwayCustomConfigurationOptions extends IConfigurationOptions { + port: number; +} + +// Implement a custom framework that inherits the base framework +export class MidwayCustomHTTPFramework extends BaseFramework { + + configure(): IMidwayCustomConfigurationOptions { + return this.configService.getConfiguration('customKey'); + } + + async applicationInitialize(options: Partial) { + // Create an app instance + this.app = http.createServer((req, res) => { + // Create a request context with logger, request scope, etc. + const ctx = this.app.createAnonymousContext(); + // Get the injected service from the request context + ctx.requestContext + .getAsync('xxxx') + .then((ins) => { + // Call service + return ins.xxx(); + }) + .then(() => { + // End of request + res.end(); + }); + }); + + // Some methods needed to bind midway framework to app, such as getConfig, getLogger, etc. + this.defineApplicationProperties(); + } + + async run() { + // Startup parameters, only the HTTP port that is started is defined here. + if (this.configurationOptions.port) { + new Promise((resolve) => { + this.app.listen(this.configurationOptions.port, () => { + resolve(); + }); + }); + } + } +} +``` + +We define a `MidwayCustomHTTPFramework` class, inherit the `BaseFramework`, and implement both `applicationInitialize` and `run` methods. + +In this way, the most basic framework is completed. + +Finally, we just need to export the Framework as agreed. + +```typescript +export { + Application, + Context, + MidwayCustomHTTPFramework as Framework, + IMidwayCustomConfigurationOptions +} from './custom'; +``` + +The above is an example of the simplest framework. In fact, all Midway frameworks are written in this way. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/container.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/container.md new file mode 100644 index 000000000000..dc56f307975f --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/container.md @@ -0,0 +1,1257 @@ +# Dependency injection + +Midway uses a lot of dependency injection features. Through the lightweight features of decorators, dependency injection becomes elegant, thus making the development process convenient and interesting. + + +Dependency injection is a very important core in the Java Spring system, and we explain this capability in a simple way. + + +As an example, take the following function directory structure as an example. + + +``` +. +├── package.json +├── src +│ ├── controller # Controller directory +│ │ └── user.controller.ts +│ └── service # Service Directory +│ └── user.service.ts +└── tsconfig.json +``` + + +In the above example, two files, `user.controller.ts` and `user.service.ts`, are provided. + +:::tip +In the following example, in order to show the complete function, we will write a complete `@Provide` decorator, but in actual use, if there are other decorators (such as `@Controller`), `@Provide` can be used Omit. +::: + + +For the convenience of explanation, we merged it together, and the content is roughly as follows. + + +```typescript +import { Provide, Inject, Get } from '@midwayjs/core'; + +// user.controller.ts +@Provide() // Actually can be omitted +@Controller() +export class UserController { + + @Inject() + userService: UserService; + + @Get('/') + async get() { + const user = await this.userService.getUser(); + console.log(user); // world + } +} + +// user.service.ts +@Provide() +export class UserService { + async getUser() { + return 'world'; + } +} + +``` + +Leaving aside all the decorators, you can see that this is the standard Class writing and there is no other extra content. This is also the core capability of Midway system and the most fascinating place to rely on injection. + +What `@Provide` does is tell the **dependency injection container** that I need to be loaded by the container. The `@Inject` decorator tells the container that I need to inject an instance into the property. + +Through the matching of these two decorators, we can easily get the instance object in any class, just like `this.userService` above. + +**Note**: In actual use, if there are other decorators (such as `@Controller` ), the `@Provide` is often omitted. + + + +## Dependency injection principle + + +Let's take the following pseudo code as an example. During the startup phase of Midway system, a dependency injection container (MidwayContainer) will be created, files in all user codes (src) will be scanned, and Class with `@Provide` decorator will be saved to the container. + + +```typescript +/***** The following is Midway's internal code *****/ + +const container = new MidwayContainer(); +container.bind(UserController); +container.bind(UserService); + +``` + +The dependency injection container here is similar to a Map. The key of the map is the identifier of the class (for example, **the hump string of the class name** ), and the value is the **class itself**. + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01qRbFaS1dETlDbbrsl_!!6000000003704-2-tps-623-269.png) + + +When requested, these Classes are dynamically instantiated and the assignment of attributes is handled, such as the pseudo code below, which is easy to understand. + + +```typescript +/***** The following is the dependency injection container pseudo code *****/ +const userService = new UserService(); +const userController = new UserController(); + +userController.userService = userService; +``` + + +After this, we can get the complete `userController` object, and the actual code will be slightly different. + + +MidwayContainer have `getAsync` methods for asynchronously processing the initialization of objects (many dependencies are required for asynchronous initialization), automatic attribute assignment, caching, returning objects, and combining the above processes into one. + + +```typescript +/***** The following is the internal code of the dependency injection container *****/ + +// Automatic new UserService(); +// Automatic new UserController(); +// Automatic assignment userController.userService = await container.getAsync(UserService); + +const userController = await container.getAsync(UserController); +await userController.handler(); // output 'world' +``` + + +The above is the core process of dependency injection, creating an instance. + + +:::info +In addition, here is an article called ["This time, I will teach you to write an IoC container from scratch"](https://mp.weixin.qq.com/s/g07BByYS6yD3QkLsA7zLYQ). Please read it more. +::: + + + +## Dependency injection scope + + +If the default is unspecified or undeclared, the scope of all `@Provide` classes is the **request scope**. This means that these Classes will be instantiated (new) at the first call of each request, and the instance will be destroyed after the request ends. **Our controller (Controller) and service (Service) by default are both in this scope. + +In Midway's dependency injection system, there are three scopes. + +| Scope | Description | +| --------- | ------------------------------------------------------------ | +| Singleton | Single instance, globally unique (process level) | +| Request | **Default**: the scope of the request, the lifecycle of the request is bound to the **request link**. The instance is unique on the request link and destroyed immediately after the request ends. | +| Prototype | Prototype scope, creating a new object repeatedly for each call | + +Different scopes have different functions, * * Singleton * can be used to do process-level data cache, or database connection and other tasks that only need to be performed once. at the same time, Singleton is only initialized once due to global uniqueness, so the calling speed is relatively fast. However, **request scope** is the choice of most services that need to obtain request parameters and data. **prototype scope** is less used and has its unique function in some special scenarios. + + + +### Configure scope + + +If we need to define one object as the other two scopes, additional configuration is required. Midway provides a `@Scope` decorator to define the scope of a class. The following code turns our user service into a globally unique instance. + + +```typescript +// service +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class UserService { + //... +} +``` + +:::info + +Note that all entry classes, such as Controller, are request scopes and do not support modification. In most cases, only the Service needs to be adjusted. + +::: + + + +### Singleton scope + +After explicit configuration, the scope of a class can become a singleton scope. . + +```typescript +// service +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class UserService { + //... +} + +``` + +No matter how many times an instance of this class is obtained in the future, it will be the same instance under the same process. + +For example, based on the above singleton service, the following two injected `userService` attributes are the same instance: + +```typescript +@Provide() +export class A { + + @Inject() + userService: UserService + //... +} + +@Provide() +export class B { + + @Inject() + userService: UserService + //... +} +``` + +After the v3.10 version, the singleton decorator can be used to simplify the original writing. + +```typescript +import { Singleton } from '@midwayjs/core'; + +@Singleton() +class UserService { + //... +} +``` + +### Request scope + +By default, all classes written in the code are **request scope**. + +In each protocol entry framework, a dependency injection container under the request scope is automatically created, and all created instances are bound to the context of the current protocol. + +For example: + +- When an http request comes in, a request scope is created, and each Controller is dynamically created when the request is routed. +- The timer is triggered, which is equivalent to creating a request scope ctx. We can get this request scope through @Inject()ctx. + +:::info +The default is the request scope for the purpose of associating with the request context. Explicitly passing ctx is more secure and reliable, and easy to debug. +::: + +Therefore, in the request scope, we can use `@Inject()` to inject the current ctx object. + +```typescript +import { Controller, Provide, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Provide() // actually can be omitted +@Controller('/user') +export class UserController { + + @Inject() + ctx: Context; + //... +} +``` + + + + +Our `@Inject` decorator also looks for objects to inject under the **scope** of the current class. For example, in the `Singleton` scope, it is incorrect to inject CTX because it is not associated with the request and there is no `CTX` object by default. + +```typescript +@Provide() +@Scope(ScopeEnum.Singleton) +export class UserService { + + @Inject() + ctx; // undefined + //... +} +``` + + + +### Scope cache + + +When the scope is set to a singleton (Singleton), the entire Class injected object has been fixed after the first instantiation, which means that the injected content in the singleton cannot be another scope. + + +Let's give an example. +```typescript +// This class is the default request scope (Request) +@Provide() // Actually can be omitted +@Controller() +export class HomeController { + @Inject() + userService: UserService; +} + + +// Set a single instance, the process level is unique. +@Provide() +@Scope(ScopeEnum.Singleton) +export class UserService { + async getUser() { + // ... + } +} +``` +The situation of the call is as follows. +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01FN99rS1Xb1YydSFi0_!!6000000002941-2-tps-1110-388.png) + +In this case, no matter how many times the `HomeController` is called, the `HomeController` instance of each request is different, and the `UserService` will be fixed. + + +Let's take another example to show whether the service injected in the singleton will still retain the original scope. + +:::info +The `DBManager` here is specially set to the request scope to demonstrate the special scene. +::: +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01eAyxrC1xVEYzbNf9P_!!6000000006448-2-tps-1964-334.png) + +```typescript +// This class is the default request scope (Request) +@Provide() +export class HomeController { + @Inject() + userService: UserService; +} + + +// Set a single instance, the process level is unique. +@Provide() +@Scope(ScopeEnum.Singleton) +export class UserService { + + @Inject() + dbManager: DBManager; + + async getUser() { + // ... + } +} + +// The scope is not set, and the default is the request scope (here is used to verify the scenario where all subsequent instances are cached under the single instance link) +@Provide() +export class DBManager { +} + +``` +In this case, no matter how many times the `HomeController` is called, the `HomeController` instance of each request is different, and the `UserService` and `DBManager` will be fixed. + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01UoLu1526stZQFhp1U_!!6000000007718-2-tps-1870-762.png) +Simply understood, a singleton is like a cache in **which all dependent objects will be frozen and will not change.** + + + +### Scope downgrade + +As mentioned above, when a singleton scope is injected with a request scope object, the object instance of the request scope is solidified and a fixed instance is saved in the singleton cache. + +In this case, the scope of the request becomes a single instance, and the **scope is degraded** occurs. + +In daily development, this happens if you are not careful, such as calling services in middleware. + +```typescript +// The following paragraph is an example of error + +@Provide() +export class UserService { + @Inject() + ctx: Context; + + async getUser() { + const id = this.ctx.xxxx; + // ctx not found, will throw error + } +} + +// Middleware is a singleton +@Middleware() +export class ReportMiddleware implements IMiddleware { + @Inject() + userService: UserService; // The user service here is the request scope + + resolve() { + return async(ctx, next) => { + await this.userService.getUser(); + // ... + } + } +} +``` + +At this time, although `UserService` can be injected into the middleware normally, it is actually injected as a singleton object instead of an object in the request scope, which will cause `ctx` to be empty. + +The memory object diagram at this time is: + +![](https://img.alicdn.com/imgextra/i3/O1CN01SwATKb1zUtVUCaQGj_!!6000000006718-2-tps-1292-574.png) + +Instances of `UserService` become different objects, one is an instance of singleton invocation (singleton, excluding ctx), and the other is an instance of normal request-scoped invocation (request-scoped, including ctx). + +In order to avoid this situation, by default, when injecting such errors, the framework will automatically throw an error named `MidwaySingletonInjectRequestError` to prevent the program from executing. + +If the user understands the risks involved and explicitly needs to call the request scope object in a singleton, the parameter of the scope decorator can be set to allow degradation. + +In the `ctx` field, determine the empty object. + +```typescript +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Request, { allowDowngrade: true }) +export class UserService { + @Inject() + ctx: Context; + + async getUser() { + if (ctx && ctx.xxxx) { + // ... + } + // ... + } +} +``` + +Of course, if it is just a mistake, then you can use dynamic acquisition methods to make the scope uniform. + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + const userService = await ctx.requestContext.getAsync(UserService); + // TODO userService.xxxx + await next(); + }; + } +} +``` + +### Get object scope + +Starting from version v3.12.0, the dependency injection container adds a new API for obtaining object scope. + +```typescript +import { Controller, Inject, ApplicationContext, Get, IMidwayContainer } from '@midwayjs/core'; +import { UserService} from '../service/user.service'; + +@Singleton() +export class UserSerivce { + //... +} + +@Controller('/') +export class HomeController { + @Inject() + userService: UserService; + + @ApplicationContext() + applicationContext: IMidwayContainer; + + @Get('/') + async home(): Promise { + console.log(this.applicationContext.getInstanceScope(this)); + // => Request + + console.log(this.applicationContext.getInstanceScope(this.userService)); + // => Singleton + + //... + } +} +``` + +The `getInstanceScope` method returns a `ScopeEnum` value. + + + +## Injection rule + +Midway supports injection in many ways. + +### Class-based injection + +Export a Class, the type of injection uses Class, which is the simplest way to inject, and most businesses and components use this way. + +```typescript +import { Provide, Inject } from '@midwayjs/core'; + +@Provide() // <------ Expose a Class +export class B { + //... +} + +@Provide() +export class A { + + @Inject() + B: B; // <------ The attribute here uses Class + + //... +} +``` + +Midway will automatically use B as the type of the attribute B and instantiate it in the container. + +In this case, Midway automatically creates a unique uuid to associate with the class. This uuid is called a **dependency injection identifier**. + + +Default: + + +- 1. `@Provide` will automatically generate a uuid as the dependency injection identifier +- 2. `@Inject` searches for the uuid of the type. + +If you want to get this uuid, you can use the following API. + +```typescript +import { getProviderUUId } from '@midwayjs/core'; + +const uuid = getProviderUUId(B); +// ... +``` + + + +### Injection Based on fixed name + +```typescript +import { Provide, Inject } from '@midwayjs/core'; + +@Provide ('BBB') // <------ Expose a Class +export class B { + //... +} + +@Provide() +export class A { + + @Inject('bbbb') + B: B; // <------ The attribute here uses Class + + //... +} +``` + +Midway uses `bbbb` as the dependency injection identifier for Class B and instantiates it in the container. In this case, even if type B is written, the dependency injection container will still search for `bbbb`. + +The parameters of `@Provide` and `@Inject` decorators appear in pairs. + +The rules are as follows: + + +- 1. If the decorator contains parameters, the **parameter** is used as the dependency injection identifier. +- 2. If there are no parameters and the marked TS type is Class, the key of class `@Provide` is taken as the key. If there is no key, uuid is taken by default +- 3. If there is no parameter and the marked TS type is not Class, use the **attribute name** as the key. + + + +### Injection Based on Attribute Name + +Midway can also inject based on the interface, but since the Typescirpt will remove the interface type after compilation, it is better to use the class as a definition. + +For example, we define an interface and its implementation class. + +```typescript +export interface IPay { + payMoney() +} + +@Provide('APay') +export class A implements IPay { + async payMoney() { + // ... + } +} + +@Provide('BPay') +export class B implements IPay { + async payMoney() { + // ... + } +} +``` + +At this time, if there is a service that needs to be injected, you can use the following explicit declaration. + +```typescript +@Provide() +export class PaymentService { + + @Inject('APay') + payService: IPay; // Note that the type here is an interface, and the type information will be removed after compilation. + + async orderGood() { + await this.payService.payMoney(); + } + +} +``` + +Because the interface type is removed, Midway can only use the **parameter** or **attribute name** class of the `@Inject` decorator to match the injected object information, similar to the `Autowire by name` in Java Spring. + +### Inject existing objects + + +Sometimes, applications already have existing instances instead of classes. For example, a third library is introduced. At this time, if you want objects to be referenced by instances in other IoC containers, you can also add objects to handle them. + + +Let's take the common tool class library lodash as an example. + +If we want to inject it directly in different classes, instead of require. + +You need to add this object through the `registerObject` method before the business call (usually during the startup life cycle). + + +A **dependency injection identifier** is required to facilitate injection in other classes. + + +```typescript +// src/configuration.ts +import * as lodash from 'lodash'; +import { Configuration, IMidwayContainer } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration { + + async onReady(applicationContext: IMidwayContainer) { + // Add some global objects to the dependency injection container + applicationContext.registerObject('lodash', lodash); + } +} + +``` + + +At this time, you can use `@Inject` in any class. + + +```typescript +@Provide() +export class BaseService { + + @Inject('lodash') + lodashTool; + + async getUser() { + // this.lodashTool.defaults({ 'a': 1 }, { 'a': 3, 'B ': 2 }); + } +} +``` + + + +### Inject default identifier + + +Midway injects some values by default to facilitate direct business use. + +| **Identifier** | **Value Type** | **Scope** | **Description** | +| ---------- | ---------- | ---------- | ------------------------------------------------------------ | +| baseDir | string | Global | The src directory is developed locally, otherwise it is dist directory. | +| appDir | string | Global | The root path of the application is generally process.cwd() | +| ctx | object | Request | The context type of the corresponding framework, such as the Context of Koa and EggJS, and the req of the Express. | +| logger | object | Request | Equivalent to ctx.logger | +| req | object | Request | Unique to Express | +| res | object | Request | Unique to Express | +| socket | object | Request | WebSocket scenes are unique | + +```typescript +@Provide() +export class BaseService { + + @Inject() + baseDir; + + @Inject() + appDir; + + async getUser() { + console.log(this.baseDir); + console.log(this.appDir); + } +} +``` + + + +## Get the dependency injection container + + +In general, users do not need to care about relying on injection containers, but in some special scenarios, such + + +- The service needs to be called dynamically, such as the middleware scenario of the Web, and the service needs to be called during the startup phase. +- The encapsulation framework or other tripartite SDKs require dynamic access to services. + +Simply put, in any scenario where you need to **dynamically obtain services through API operations**, you must first obtain the dependency injection container. + +### Get from @ApplicationContext() decorator + +In the new version, Midway provides a @ApplicationContext() decorator to get the dependency injection container. + +```typescript +import { ApplicationContext, IMidwayContainer } from '@midwayjs/core'; + +@Provide() +export class BootApp { + + @ApplicationContext() + applicationContext: IMidwayContainer; // You can also replace it with the app definition of the actual framework here. + + async invoke() { + + // this.applicationContext + + } + +} +``` + + + +### Get from app + + +Midway mounts the dependent injection container in two places, the app of the framework and the context Context of each request. Due to the different situations of different upper-level frameworks, let's list common examples here. + + +For different upper-level frameworks, we provide a unified definition of `IMidwayApplication`. All upper-level framework apps will implement this interface, which is defined as follows. + +```typescript +export interface IMidwayApplication { + getApplicationContext(): IMidwayContainer; + //... +} +``` + +That is, through the `app.getApplicationContext()` method, we can all obtain the dependency injection container. + +```typescript +const container = app.getApplicationContext(); +``` + +With the `@App` decorator, you can easily access the currently running app instance anywhere. + +```typescript +import { App, IMidwayApplication } from '@midwayjs/core'; + +@Provide() +export class BootApp { + + @App() + app: IMidwayApplication; //You can also replace it with the app definition of the actual framework here. + + async invoke() { + + // Get the dependency injection container + const applicationContext = this.app.getApplicationContext(); + + } + +} +``` + + +In addition to the common dependency injection container, Midway also provides a **dependency injection container for the request link.** The dependency injection container for this request link is associated with a global dependency injection container and shares an object pool. But there is still a difference between the two. + + +The dependency injection container of the request link is used to obtain the objects of the specific request scope. The objects obtained in this container are **bound to the request** and are associated with the current context. This means that **if the Class code is associated with the request, it must be obtained from the dependency injection container of the request link**. + + +The dependency injection container of the request link must be obtained from the request context object. The most common scenario is web middleware. + + +```typescript +@Middleware() +export class ReportMiddleware { + + resolve() { + return async(ctx, next) => { + // ctx. Dependency Injection Container for requestContext Request Link + await next(); + } + } +} +``` +The Express request link depends on the injection container mounted on the req object. + +```typescript +@Middleware() +export class ReportMiddleware { + + resolve() { + return (req, res, next) => { + // req. Dependency Injection Container for requestContext Request Link + next(); + } + } +} +``` + + + +### Get in Configuration + +In the life cycle of the code entry `configuration` file, we will also pass additional parameters that depend on the injection container, which is convenient for users to use directly. + +```typescript +// src/configuration.ts +import { Configuration, IMidwayContainer } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration { + async onReady(applicationContext: IMidwayContainer) { + // ... + } +} +``` + + + +## Dynamic API + + + +### Dynamic acquisition of instances + +After you get the **dependency injection container** or **dependency of the request link** injection container, you can obtain the object through the container API. + +We can use the standard dependency injection container API to obtain instances. + +```typescript +// The global container is obtained as a singleton. +const userSerivce = await applicationContext.getAsync(UserService); + +// Request scope container, get the request scope instance. +const userSerivce = await ctx.requestContext.getAsync(UserService); +``` + +We can use it wherever we can get dependent injection containers, such as in middleware. + +```typescript +import { Middleware, ApplicationContext, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; +import { UserService } from './service/user.service'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + @ApplicationContext() + applicationContext: IMidwayContainer; + + resolve() { + return async(ctx, next) => { + // Specify a generic type, such as an interface + const userService1 = await this.applicationContext.getAsync(UserService); + // You can deduce the correct type without writing generics. + const userService1 = await this.applicationContext.getAsync(UserService); + + // The following method obtains the service and request association, which can be injected into the context. + const userService2 = await ctx.requestContext.getAsync(UserService); + await next(); + } + } +} +``` + + +In Express. +```typescript +import { UserService, Middleware } from './service/user'; +import { NextFunction, Context, Response } from '@midwayjs/express'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (req, res, next) => { + const userService = await req.requestContext.getAsync(UserService); + // ... + next(); + } + } +} +``` + + + +### Pass constructor parameters + +In individual scenarios, we can pass the constructor parameters when we get the instance through the `getAsync`. Normal decorator mode cannot be done, only available in API form. + +```typescript +@Provide() +class UserService { + constructor(private readonly type) {} + + getUser() { + // this.type => student + } +} + +// The global container is obtained as a singleton. +const userSerivce = await applicationContext.getAsync(UserService, [ + 'student', // constructor parameters, will apply to the constructor +]); + +// Request scope container, get the request scope instance. +const userSerivce = await ctx.requestContext.getAsync(UserService, [ + 'student' +]); +``` + +Note that the constructor cannot pass instances in injection form, but can only pass fixed values. + + + + +### Dynamic function injection + + +In some scenarios, we need functions to be executed dynamically as a logic, while the object properties in the dependent injection container are already created and cannot meet the dynamic logic requirements. + + +For example, you need a factory function to return different instances according to different scenarios, or you may have a three-party package, which is a function that you want to call directly in the business. In various scenarios, you need to inject a factory method directly, get the context in the function, and dynamically generate the instance. + + +The following is a sample of the standard factory method injection. + + +General factory methods are used to return the implementation of the same interface, for example, we have two `ICacheService` interface implementations: +```typescript +export interface ICacheService { + getData(): any; +} + +@Provide() +export class LocalCacheService implements ICacheService { + async getData {} +} + +@Provide() +export class RemoteCacheService implements ICacheService { + async getData {} +} +``` +Then you can define a dynamic service (factory) and return different implementations according to the current user configuration. +```typescript +// src/service/dynamicCacheService.ts + +import { providerWrapper, IMidwayContainer, MidwayConfigService } from '@midwayjs/core'; + +export async function dynamicCacheServiceHandler(container: IMidwayContainer) { + // Get global configuration from container API + const config = container.get(MidwayConfigService).getConfiguration(); + if (config['redis']['mode'] === 'local') { + return await container.getAsync('localCacheService'); + } else { + return await container.getAsync('remoteCacheService'); + } +} + +providerWrapper ([ + { + id: 'dynamicCacheService', + provider: dynamicCacheServiceHandler + Scope: ScopeEnum.Request, // is set to the request scope, then the container passed in above is the request scope container. + // scope: ScopeEnum.Singleton, // can also be set to global scope, then the logic of the call will be cached + } +]); +``` + + +In this way, it can be used directly in business. Note: When injecting, the method is **called and then injected**. + + +```typescript +@Provide() +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Inject('dynamicCacheServiceHandler') + cacheService: ICacheService; + + @Get('/') + async home() { + const data = await this.cacheService.getData(); + // ... + } + +} +``` + + +By `providerWrapper`, we have wrapped an original function writing method, which can be integrated with the existing dependency injection system, so that the container can be managed uniformly. + + +:::info +Note that dynamic methods must be exported before they are scanned by dependency injection. The default is the request scope (the Container obtained is the request scope container). +::: + + +Since we can bind the dynamic method to the dependency injection container, we can also bind a callback method in, so that the obtained method can be executed, and we can determine the returned result based on the parameters of the business. +```typescript +import { providerWrapper, IMidwayContainer } from '@midwayjs/core'; + +export function cacheServiceHandler(container: IMidwayContainer) { + return async (mode: string) => { + if (mode === 'local') { + return await container.getAsync('localCacheService'); + } else { + return await container.getAsync('remoteCacheService'); + } + }; +} + +providerWrapper ([ + { + id: 'cacheServiceHandler', + provider: cacheServiceHandler + scope: ScopeEnum.Singleton + } +]); + + +@Provide() +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Inject('cacheServiceHandler') + getCacheService; + + @Get('/') + async home() { + const data = await this.getCacheService('local'); + // ... + } + +} +``` + + + +## Static API + + +In some tool classes, you can obtain the global dependency injection container (**after startup**) without creating a class instance. +```typescript +import { getCurrentApplicationContext } from '@midwayjs/core'; + +export const getService = async (serviceName) => { + return getCurrentApplicationContext().getAsync(serviceName); +} +``` + + +Gets the main frame **after startup**. +```typescript +import { getCurrentMainFramework } from '@midwayjs/core'; + +export const framework = () => { + return getCurrentMainFramework(); +} +``` +Gets the app object of the main frame **after startup**. +```typescript +import { getCurrentMainApp } from '@midwayjs/core'; + +export const getGlobalConfig = () => { + return getCurrentMainApp().getConfig(); +} +``` + + + +## Start Behavior + +### Automatic scan binding + +As mentioned above, after the container is initialized, we will bind the existing class registration to the container. + +```typescript +const container = new MidwayContainer(); +container.bind(UserController); +container.bind(UserService); +``` + +Midway will automatically scan the entire project directory during the startup process and automatically process this behavior, so that the user does not need to manually perform binding operations. + +Simply put, the framework will recursively scan the ts/js files in the entire `src` directory by default, and then perform require operations. When the file is exported as a class and explicitly or implicitly contains the `@Provide()` decorator, it will execute the `container.bind` logic. + +### Ignore scanning + +In general, we should not put non-ts files under src (such as front-end code). In special scenarios, we can ignore some directories and configure them in the `@Configuration` decorator. + +An example is as follows: + +```typescript +// src/configuration.ts +import { App, Configuration, Logger } from '@midwayjs/core'; +// ... + +@Configuration({ + // ... + detectorOptions: { + ignore: [ + '**/web/**' + ] + } +}) +export class MainConfiguration { + // ... +} + +``` + + + + + +## Object lifecycle + +When creating and destroying instances depending on the injection container, we can use the decorator to do some custom operations. + + + +### Asynchronous initialization + + +In some cases, we need an instance to be initialized before being called by other dependencies. If this initialization only reads a certain file, it can be written as a synchronization method. If this initialization is to take data from a remote end or connect to a certain service, in this case, ordinary synchronization code is very difficult to write. + + +Midway provides asynchronous initialization. You can use the `@Init` tag to manage initialization methods. + +Only one `@Init` method can be used. + + +```typescript +@Provide() +export class BaseService { + + @Config('hello') + config; + + @Init() + async init() { + await new Promise(resolve => { + setTimeout(() => { + this.config.c = 10; + resolve(); + }, 100); + }); + } + +} +``` + + +Equivalent + +```typescript +const service = new BaseService(); +await service.init(); +``` + +:::info +The method marked by the @Init decorator will definitely be called asynchronously. In general, asynchronous initialization of services is slow, please label it as a singleton (@Scope(ScopeEnum.Singleton)) as much as possible. +::: + + + +### Asynchronous destruction + +Midway provides the ability to execute methods before objects are destroyed and manages methods through the `@Destroy` decorator. + +There is currently only one `@Destroy` method. + + +```typescript +@Provide() +export class BaseService { + + @Config('hello') + config; + + @Destroy() + async stop() { + // do something + } +} +``` + + + +## Context object in request scope + +For objects created in the request scope, the framework will mount a context object on the object, even if the object does not explicitly declare `@Inject() ctx`, the current context object can be obtained. + +```typescript +import { REQUEST_OBJ_CTX_KEY } from '@midwayjs/core'; + +@Provide() +export class UserManager { + //... +} + +@Provide() +export class UserService { + //... + + @Inject() + userManager: UserManager; + + async invoke() { + const ctx = this. userManager[REQUEST_OBJ_CTX_KEY]; + //... + } +} +``` + +This feature is useful in [Interceptor](./aspect) or [Custom Method Decorator](./custom_decorator). + + + +## Common usage errors + + +### Error: Get Injection Property in Constructor + +Please do not get the injected attribute in the constructor * *, which will make the result undefined. The reason is that the properties injected by the decorator are assigned only after the instance is created (new). In this case, use the `@Init` decorator. + +```typescript +@Provide() +export class UserService { + + @Config('userManager') + userManager; + + constructor() { + console.log(this.userManager); // undefined + } + + @Init() + async initMethod() { + console.log(this.userManager); // has value + } + +} +``` + + + +### On inheritance + + +To avoid property confusion, do not use the `@Provide` decorator on the base class. + + +At this stage, Midway supports the inheritance of attribute decorators, but does not support the inheritance of class and method decorators (there will be ambiguity). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/context_definition.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/context_definition.md new file mode 100644 index 000000000000..4db17a0973b1 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/context_definition.md @@ -0,0 +1,114 @@ +# Extended context definition + +Due to static type analysis of TS, we do not recommend dynamically mounting certain attributes. Dynamic mounting will make it very difficult to process TS types. In some special scenarios, if you need to extend the context ctx attribute, such as the middleware in the Web scenario, we can add some methods or attributes. + +```typescript +import { Middleware } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IWebMiddleware { + + resolve() { + return async (ctx: Context, next) => { + + ctx.abc = '123'; + await next(); + + } + } + +} +``` + +However, due to the relationship between TypeScript module definitions, we cannot attach definitions to existing modules, so we used a new method to expand. + + + + +## Extended definition in project + + +You can extend Midway's common Context in the project through the following code in `src/interface.ts`. + +```typescript +// src/interface.ts + +import '@midwayjs/core'; + +// ... + +declare module '@midwayjs/core'{ + interface Context { + abc: string; + } +} +``` + +:::info + +Note that `declare module` replaces the original definition, so please import the module using `import` syntax before operating. + +::: + + + +## Extension definition in component + +Components are slightly different. Generally speaking, components may only be used in specific scenarios. + +You can extend Midway's common Context through the following code in `index.d.ts` of the component root directory. + +If you want to expand the Context of all scenes. + +```typescript +// index.d.ts + +// The following paragraph can extend all Context +declare module '@midwayjs/core/dist/interface '{ + interface Context { + abc: string; + } +} +``` + +If you only want to expand the Context of a specific scene. + +```typescript +// index.d.ts + +// The following paragraph is only extended by the Context of @midwayjs/koa +declare module '@midwayjs/koa/dist/interface '{ + interface Context { + abc: string; + } +} + +// The following paragraph is only extended by @midwayjs/Web Context +declare module '@midwayjs/web/dist/interface '{ + interface Context { + abc: string; + } +} + +// The following paragraph is only extended by the Context of @midwayjs/faas +declare module '@midwayjs/faas/dist/interface '{ + interface Context { + abc: string; + } +} + +// The following paragraph is only extended by @midwayjs/express Context +declare module '@midwayjs/express/dist/interface '{ + interface Context { + abc: string; + } +} + +``` + +:::caution +- 1. The extension in the component is slightly different from that in the project (it is suspected to be a bug of TS). +- 2. If the extension method of the project is used in the component, there will be problems with the extension prompts of the remaining components. + +::: diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/contributing.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/contributing.md new file mode 100644 index 000000000000..63fc89cf4240 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/contributing.md @@ -0,0 +1,89 @@ +# Contribute to Midway + +Midway is an open source framework that welcomes everyone to contribute to the community, and this article describes how to submit issue, contribution code, documentation, and more to Midway. + + + +## Report problems + +If you encounter some problems in the development process and you cannot solve the problems you need to ask developers, we strongly recommend: + +- 1. Find relevant problems in the document first +- 2. If you cannot solve it after searching, you can submit an [Q&A](https://github.com/midwayjs/midway/discussions/new/choose). + + + +Please follow the following specifications when submitting the content. + +- 1. Explain your purpose clearly in the title or content, either in Chinese or English. +- 2. Describe the following in the content + - If it is a new requirement, please describe the requirement in detail, preferably with pseudo code implementation. + - If it is a BUG, please provide reproduction steps, error logs, screenshots, relevant configurations, framework versions, etc. to enable developers to quickly locate the content of the problem + - If possible, please provide a minimum replicated code warehouse as much as possible for easy debugging. +- 3. Please search for relevant problems before you report them. Make sure you don't open duplicate questions + + + +Developers will mark the problem, reply or solve the problem when they see it. + + + +## Fix code problems + +If you find that the framework has some problems to be modified, you can submit them through PR. + + + +### Pull Request + +1. First, fork a warehouse in the upper-right corner of [midway github](null) to your own space. + +2. git clone The warehouse is developed or repaired in local or other IDE environments. + +```bash +# Create a new branch +$ git checkout -b branch-name +# install dependencies +$ npm i +# build code +$ npm run build + +# Develop and execute tests +$ npm test + +$ git add . # git add -u to delete files +$ git commit -m "fix(role): role.use must xxx" +$ git push origin branch-name +``` + +3. Create a Pull Request and choose to merge your own project branch into the main branch of the target midwayjs/midway. + +4. The system will automatically create PR under midway warehouse. After the test passes, the developer will merge the PR. + + + +### Submit specifications + +- 1. General PR uses English titles +- 2. The `fix`, `chore`, `feat`, and `docs` fields are used to specify the type of the fix. + + + +## Fix document problems + +Similar to ordinary PR, if it is a single document, it can be submitted by quick editing. + + + +### Quick Repair of Single Document + +- 1. Open the document that needs to be repaired on the official website and click the [Edit this page](#) link in the lower-left corner. +- 2. Click the "Pen Type" button to enter the editing page +- 3. After editing the content, change the submitted title to `docs: xxxx` and click Submit to create a PR. +- 4. Waiting for developers to merge + + + +### Multiple document repair + +Same as normal PR, clone warehouse, submit. Note that the title of the submitted PR is `docs: xxx`. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/controller.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/controller.md new file mode 100644 index 000000000000..fe5633e77b9d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/controller.md @@ -0,0 +1,1139 @@ +​ import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Routing and controller + +In the common MVC architecture, C represents the controller, which is responsible for **parsing the user's input and returning the corresponding results after processing.** + +As shown in the figure, the client requests the controller of the server through the Http protocol, and the controller responds to the client after processing. This is the most basic "request-response" process. + +![controller](https://img.alicdn.com/imgextra/i1/O1CN01dYitV22ADuagILnp3_!!6000000008170-2-tps-1600-634.png) + + +Common ones are: + + +- In the [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) interface, the controller accepts the user's parameters, returns the contents from the database to the user, or updates the user's request to the database. +- In the HTML page request, the controller renders different templates to obtain HTML and returns it to the user according to the user's access to different URLs. +- In a proxy server, the controller forwards the user's request to other servers and returns the processing results of other servers to the user. + + + +Generally speaking, the controller is often used to verify, convert, and call complex business logic on the user's request parameters, assemble the data after getting the corresponding business results, and then return. + + +In Midway, controllers **also carry routing capabilities**. Each controller can provide multiple routes, and different routes can perform different operations. + + +In the following example, we will demonstrate how to create a route in the controller. + + +## Routing + + +Controller files are generally in the `src/controller` directory, where we can create controller files. Midway annotates controllers with the `@Controller()` decorator, where the decorator has an optional parameter for route prefix (grouping), so that all routes under this controller will carry this prefix. + + +At the same time, Midway provides a method decorator for marking the type of request. + + +For example, we create a homepage controller to return a default`/`route. + +```text +➜ my_midway_app tree +. +├── src +│ └── controller +│ └── home.ts +├── test +├── package.json +└── tsconfig.json +``` + +```typescript +// src/controller/home.ts + +import { Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + return "Hello Midwayjs!"; + } +} + +``` +The `@Controller` decorator tells the framework that this is a class of type Web Controller, and the `@Get` decorator tells the framework that the decorated `home` method will be exposed as a `/` route, which can be accessed by ` GET` request to access. + +The whole method returns a string, and in the browser you will receive the response type of `text/plain` and a `200` status code. + +:::tip + +Routing methods are all async methods. + +::: + + + + +## Routing method + + +In the preceding example, you have created a **GET** route. In general, we will have other HTTP Methods, Midway provides more routing method decorators. + + +```typescript +// src/controller/home.ts + +import { Controller, Get, Post } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + return 'Hello Midwayjs!'; + } + + @Post('/update') + async updateData() { + return 'This is a post method' + } +} + +``` +Midway also provides other decorators, such as `@Get`, `@Post`, `@Put()`, `@Del()`, `@Patch()`, `@Options()`, `@Head()`, and `@All()`. + + +The `@All` decorator is special, indicating that it can accept all types of HTTP methods. + + +You can bind multiple routes to the same method. +```typescript +@Get('/') +@Get('/main') +async home() { + return 'Hello Midwayjs!'; +} +``` + + +## Get request parameters + + +Next, we will create an HTTP API about users. Similarly, we will create a `src/controller/user.ts` file. This time we will add a routing prefix and more request types. + + +Let's take the user type as an example, first add a user type, and we usually put the defined content in the `src/interface.ts` file. +``` +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ ├── user.ts +│ │ └── home.ts +│ └── interface.ts +├── test +├── package.json +└── tsconfig.json +``` +```typescript +// src/interface.ts +export interface User { + id: number; + name: string; + age: number; +} +``` +Add a route prefix and the corresponding controller. +```typescript +// src/controller/user.ts + +import { Controller } from '@midwayjs/core'; + +@Controller('/api/user') +export class UserController { + // xxxx +} + +``` + +Next, we will call different processing logic for different request types. Except for the request type, the requested data is generally dynamic and will be passed at different locations in HTTP, such as common Query,Body, etc. + + + +### Decorator parameter conventions + + +Midway adds a common decorator for dynamic values. Take the `@Query` decorator as an example. The `@Query` decorator obtains the query parameters in the URL and assigns them to the input parameters of the function. In the following example, the id is obtained from the query parameter of the route. If the URL is `/?id = 1`, the value of the id is 1. At the same time, the route returns an object of the `User` type. +```typescript +// src/controller/user.ts + +import { Controller, Get, Query } from '@midwayjs/core'; + +@Controller('/api/user') +export class UserController { + @Get('/') + async getUser(@Query('id') id: string): Promise { + // xxxx + } +} +``` + +The `@Query` decorator has a parameter. You can pass in a specified string key to obtain the corresponding value and assign the value to the input parameter. If the parameter is not passed in, the entire query object is returned by default. + +```typescript +// URL = /?id=1 +async getUser(@Query('id') id: string) // id = 1 +async getUser(@Query() queryData) // {"id": "1"} +``` + +Midway provides more decorators that get values from Query, Body, Header, etc., which are all out of the box and adapted to different upper-level Web frameworks. + + +The following are these decorators and the corresponding equivalent frame values. + +| Decorator | Express the corresponding method | Corresponding method of Koa/EggJS | +| --- | --- | --- | +| @Session(key?: string) | req.session / req.session [key] | ctx.session / ctx.session [key] | +| @Param(key?: string) | req.params / req.params [key] | ctx.params / ctx.params [key] | +| @Body(key?: string) | req.body / req.body [key] | ctx.request.body / ctx.request.body [key] | +| @Query(key?: string) | req.query / req.query [key] | ctx.query / ctx.query[key] | +| @Queries(key?: string) | - | -/ctx.queries [key] | +| @Headers(name?: string) | req.headers / req.headers [name] | ctx.headers / ctx.headers [name] | + +:::caution +**Note** @Queries decorator is **different from** @Query. + +Queries will aggregate the same keys together and become an array. When the interface parameter accessed by the user is `/? When name = a & name = B`, `@Queries` will return `{name: [a, B] }`, while `@Query` will only return `{name: B}`. +::: + + + +### Query + +The part after `?` in the URL is a Query String, which is often used to pass parameters in GET type requests. + +For example + +``` +GET /user?uid=1&sex=male +``` + +It is the parameter passed by the user. + +**Example: Get from Decorator** + +```typescript +// src/controller/user.ts +import { Controller, Get, Query } from '@midwayjs/core'; + +@Controller('/user') +export class UserController { + @Get('/') + async getUser(@Query('uid') uid: string): Promise { + // xxxx + } +} +``` + +**Example: Get from an API operation** + +```typescript +// src/controller/user.ts +import { Controller, Get, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/user') +export class UserController { + + @Inject() + ctx: Context; + + @Get('/') + async getUser(): Promise { + const query = this.ctx.query; + // { + // uid: '1', + // sex: 'male', + //} + } +} +``` + +:::caution +**Note** EggJS is different from other frameworks. When the key in the Query String is repeated, `ctx.query` only takes the value of the first occurrence of the key, and subsequent occurrences will be ignored. + +For example, the value obtained by `GET /user?uid=1&uid=2` through `ctx.query` is `{ uid: '1' }`. + +::: + + + +### Body + +Although we can pass parameters through the URL, there are still many limitations: + +- [The length of the URL is limited in the browser](http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers). If there are too many parameters to pass, the URL cannot be passed. +- The server often records the complete URL of the access to the log file, and it is unsafe to pass some sensitive data through the URL. + +In the previous HTTP request message example, we saw that there is a body part after the header, and we usually pass the parameters of POST, PUT, DELETE and other methods in this part. When there is a body in a general request, the client (browser) will send a `Content-Type` at the same time to tell the server what format the body of this request is. The two most commonly used data delivery formats in Web development are `JSON` and `Form`. + +The framework has built-in [bodyParser](https://github.com/koajs/bodyparser) middleware to parse requests in these two formats into objects and mount them to `ctx.request.body`. HTTP protocol does not recommend passing body when accessing through GET and HEAD methods, so we cannot obtain content according to this method in GET and HEAD methods. + +**Example: Get a single body** + +```typescript +// src/controller/user.ts +// POST /user/ HTTP/1.1 +// Host: localhost:3000 +// Content-Type: application/json; charset=UTF-8 +// +// {"uid": "1", "name": "harry"} +import { Controller, Post, Body } from '@midwayjs/core'; + +@Controller('/user') +export class UserController { + @Post('/') + async updateUser(@Body('uid') uid: string): Promise { + // id is equivalent to ctx.request.body.uid + } +} +``` + +**Example: Get the entire body** + +```typescript +// src/controller/user.ts +// POST /user/ HTTP/1.1 +// Host: localhost:3000 +// Content-Type: application/json; charset=UTF-8 +// +// {"uid": "1", "name": "harry"} +import { Controller, Post, Body } from '@midwayjs/core'; + +@Controller('/user') +export class UserController { + @Post('/') + async updateUser(@Body() user: User): Promise { + // user is equivalent to the entire body object of ctx.request.body + // => output user + // { + // uid: '1', + // name: 'harry', + // } + } +} +``` + +**Example: Get from an API operation** + +```typescript +// src/controller/user.ts +// POST /user/ HTTP/1.1 +// Host: localhost:3000 +// Content-Type: application/json; charset=UTF-8 +// +// {"uid": "1", "name": "harry"} +import { Controller, Post, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/user') +export class UserController { + + @Inject() + ctx: Context; + + @Post('/') + async getUser(): Promise { + const body = this.ctx.request.body; + // { + // uid: '1', + // name: 'harry', + //} + } +} +``` + +**Example: Obtain query and body parameters** + + +Decorators can be used in combination. +```typescript +@Post('/') +async updateUser(@Body() user: User, @Query('pageIdx') pageIdx: number): Promise { + // user gets it from body + // pageIdx obtained from query +} +``` +The framework sets some default parameters for the bodyParser. After configuration, it has the following features: + +- If the request Content-Type is `application/json`, `application/json-patch + json`, `application/vnd.api + json`, and `csp-report`, the request body is parsed in json format and the maximum length of the body is limited to `1mb`. +- When the request Content-Type is `application/x-www-form-urlencoded`, the request body is parsed in the form format and the maximum length of the body is limited to `1mb`. +- If the parsing is successful, the body must be an Object (possibly an array). + +:::caution + +Common errors: `ctx.request.body` is confused with `ctx.body`, which is the abbreviation of `ctx.response.body`. + +::: + + + +### Router Params + +If the route is declared in the `:xxx` format, you can use `ctx.params` to obtain the parameters. + +**Example: Get from Decorator** + +```typescript +// src/controller/user.ts +// GET /user/1 +import { Controller, Get, Param } from '@midwayjs/core'; + +@Controller('/user') +export class UserController { + @Get('/:uid') + async getUser(@Param('uid') uid: string): Promise { + // xxxx + } +} +``` + +**Example: Get from an API operation** + +```typescript +// src/controller/user.ts +// GET /user/1 +import { Controller, Get, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/user') +export class UserController { + + @Inject() + ctx: Context; + + @Get('/:uid') + async getUser(): Promise { + const params = this.ctx.params; + // { + // uid: '1', + //} + } +} +``` + + + +### Header + +In addition to getting parameters from the URL and request body, many parameters are passed through the request header. The framework provides some auxiliary properties and methods to obtain. + +- `ctx.headers`, `ctx.header`, `ctx.request.headers`, `ctx.request.header`: These methods are equivalent to obtaining the entire header object. +- `ctx.get(name)`, `ctx.request.get(name)`: Gets the value of a field in the request header. If this field does not exist, an empty string is returned. +- We recommend using `ctx.get(name)` instead of `ctx.headers['name']`, because the former will automatically handle case. + +**Example: Get from Decorator** + +```typescript +// src/controller/user.ts +// GET /user/1 +import { Controller, Get, Headers } from '@midwayjs/core'; + +@Controller('/user') +export class UserController { + @Get('/:uid') + async getUser(@Headers('cache-control') cacheSetting: string): Promise { + // no-cache + // ... + } +} +``` + +**Example: Get from an API operation** + +```typescript +// src/controller/user.ts +// GET /user/1 +import { Controller, Get, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/user') +export class UserController { + + @Inject() + ctx: Context; + + @Get('/:uid') + async getUser(): Promise { + const cacheSetting = this.ctx.get('cache-control'); + // no-cache + } +} +``` + + + +### Cookie + +HTTP requests are stateless, but our Web applications usually need to know who initiated the request. To solve this problem, HTTP protocol designs a special request header: [Cookie](https://en.wikipedia.org/wiki/HTTP_cookie). The server can respond to a small amount of data to the client through the response header (set-cookie). The browser will follow the protocol to save the data and bring it with it when requesting the same service next time (the browser will also follow the protocol and only bring the corresponding cookie when visiting websites that meet the cookie specified rules to ensure security). + +`ctx.cookies` allows us to set and read Cookie conveniently and safely in our Controller. + +```typescript +import { Inject, Controller, Get, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + // set cookie + this.ctx.cookies.set('foo', 'bar', { encrypt: true }); + // get cookie + this.ctx.cookies.get('foo', { encrypt: true }); + } +} +``` + +Although a cookie is only a header in HTTP, you can set multiple key-value pairs in the format of `foo = bar;foo1 = bar1;`. + +Cookie often plays a role in transmitting client identity information in Web applications. Therefore, there are many security-related configurations that cannot be ignored. The [Cookie](cookie_session#Default-Cookies) document describes the usage of cookies and security-related configuration items in detail. + + + +### Session + +Through Cookie, we can set a Session for each user to store information related to the user's identity. This information will be encrypted and stored in Cookie to maintain the user's identity across requests. + +The framework has built-in [Session](https://github.com/midwayjs/midway/tree/main/packages/session) plug-ins, which provide us with `ctx.session` to access or modify the current user Session. + +```typescript +import { Inject, Controller, Get, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + // Get the content on the Session + const userId = this.ctx.session.userId; + const posts = await this.ctx.service.post.fetch(userId); + // Modify the value of the Session + this.ctx.session.visited = ctx.session.visited? (ctx.session.visited + 1) : 1; + // ... + } +} +``` + +The use of the Session is very intuitive, just read it directly or modify it. If you want to delete it, assign it `null` directly: + +```typescript +ctx.session = null; +``` + +Like Cookie, Session also has many security options and functions. It is better to read [Session](cookie_session# Default-session) documents for further understanding before using them. + + + +### Uploaded file + +Generally, the `multipart/form-data` protocol header is used to obtain the uploaded files by the `@Files` decorator. The upload function is provided by the upload component. For more information, see [upload component](./extensions/upload). + + + +### Other parameters + +There are also some more common parameter decorators and their corresponding methods. + +| Decorator | Express the corresponding method | Corresponding method of Koa/EggJS | +| --- | --- | --- | +| @RequestPath | req.baseurl | ctx.path | +| @RequestIP | req.ip | ctx.ip | + + + +**Example: Obtain the body, path, and ip address** + +```typescript +@Post('/') +async updateUser ( + @Body('id') id: string, + @RequestPath() p: string + @RequestIP() ip: string): Promise { + +} +``` + + + +### Custom request parameter decorator + +You can quickly create custom request parameter decorators with `createRequestParamDecorator`. + +```typescript +import { createRequestParamDecorator } from '@midwayjs/core'; + +// Implement decorator +export const Token = () => { + return createRequestParamDecorator(ctx => { + return ctx.headers.token; + }); +}; + +// Use decorator +export class UserController { + async invoke(@Token() token: string) { + console.log(token); + } +} +``` + + + +## Request parameter type conversion + +If it is a simple type, Midway will automatically convert the parameter to the user-declared type. + +For example: + +Number type + +```ts +@Get('/') +async getUser(@Query('id') id: number): Promise { + console.log(typeof id) // number +} +``` + +Boolean type + +- When the value is 0,"0", "false" is converted to false, and the rest return Boolean(value) values + +```ts +@Get('/') +async getUser(@Query('id') id: boolean): Promise { + console.log(typeof id) // boolean +} +``` + +If it is a complex type, if the specified type is Class, it will be automatically converted to an instance of the class. + +```typescript +// class +class UserDTO { + name: string; + + getName() { + return this.name; + } +} + +@Get('/') +async getUser(@Query() query: UserDTO): Promise { + // query.getName() +} +``` + +If you do not want to be converted, you can use Interface. + +```typescript +interface User { + name: string; +} + +@Get('/') +async getUser(@Query() query: User): Promise { + // ... +} +``` + + + +## Parameter verification + +The parameter verification function is provided by the validate component. For details, please refer to the [validate component](./extensions/validate). + + + +## Set HTTP response + +### Set the return value + +Most of the data is sent to the requester through the body. Like the body in the request, the body sent in the response also needs a matching Content-Type to tell the client how to parse the data. + +- As a RESTful API interface controller, we usually return a body with Content-Type in `application/json` format and the content is a JSON string. +- As a controller of an html page, we usually return a body with a Content-Type of `text/html` format, and the content is html code segments. + +In Midway, you can simply use `return` to return data. + +```typescript +import { Controller, Get, HttpCode } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + // Return string + return "Hello Midwayjs!"; + + // Return json + return { + a: 1 + b: 2 + }; + + // return html + return '

Hello

'; + + // Return to stream + return fs.createReadStream('./good.png'); + } +} +``` + +You can also use koa's native API. + +```typescript +import { Controller, Get, HttpCode } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + // Return string + this.ctx.body = "Hello Midwayjs!"; + + // Return JSON + this.ctx.body = { + a: 1, + b: 2, + }; + + // return html + this.ctx.body = '

Hello

'; + + // Return to stream + this.ctx.body = fs.createReadStream('./good.png'); + } +} +``` + +:::caution + +Note: `ctx.body` is the abbreviation of `ctx.response.body`. Do not confuse it with `ctx.request.body`. + +::: + + + +### Set status code + +By default, the **status code** of the response is always **200**, and we can easily change this behavior by adding a `@HttpCode` decorator at the handler layer or through the API. + +When sending an error, such as `4xx/5xx`, you can use [exception handling](error_filter) to throw an error. + +**Example: Using a Decorator** + + +```typescript +import { Controller, Get, HttpCode } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + @HttpCode(201) + async home() { + return "Hello Midwayjs!"; + } +} +``` + +**Example: API operation** + +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.status = 201; + // ... + } +} +``` + +:::info +The status code cannot be modified after the response stream is closed (after response.end). +::: + + + +### Set response header + +Midway provides a `@SetHeader` decorator or an API to simply set up a custom response header. + +**Example: Using a Decorator** + +```typescript +import { Controller, Get, SetHeader } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + @SetHeader('x-bbb', '123') + async home() { + return "Hello Midwayjs!"; + } +} + +``` +When there are multiple response headers that need to be modified, you can directly pass in the object. + + +```typescript +import { Controller, Get, SetHeader } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + @SetHeader({ + 'x-bbb ': '123', + 'x-ccc ': '234' + }) + async home() { + return "Hello Midwayjs!"; + } +} + +``` +**Example: API operation** + +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.set('x-bbb', '123'); + // ... + } +} +``` + +:::info +The response header cannot be modified after the response flow is closed (after response.end). +::: + +### Redirection + +If you need to simply redirect a route to another route, you can use the `@Redirect` decorator. The parameters of the `@Redirect` decorator are a redirect URL and an optional status code. The default redirect status code is `302` . + +In addition, you can jump through the API. + +**Example: Using a Decorator** + + +```typescript +import { Controller, Get, Redirect } from '@midwayjs/core'; + +@Controller('/') +export class LoginController { + + @Get('/login_check') + async check() { + // TODO + } + + @Get('/login') + @Redirect('/login_check') + async login() { + // TODO + } + + @Get('/login_another') + @Redirect('/login_check', 302) + async loginAnother() { + // TODO + } +} +``` +**Example: API operation** + +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.redirect('/login_check'); + // ... + } +} +``` + +:::info +Redirection cannot be modified after the response flow is closed (after response.end). +::: + + + + +### Response type + +Although the browser will automatically judge the best response content based on the content, we often encounter situations that need to be set manually. We also provide a `@ContentType` decorator for setting the response type. + +In addition, it can also be set through API. + +**Example: Using a Decorator** + + +```typescript +import { Controller, Get, ContentType } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + @ContentType('html') + async login() { + return 'hello world'; + } +} +``` +**Example: API operation** + +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.type = 'html'; + // ... + } +} +``` + +:::info +The response type cannot be modified after the response flow is closed (after response.end). +::: + + + +### Streaming response + +If you want to stream data back, you can use the `write` and `end` methods on the Node.js raw response object. + +```typescript +import { Controller, Get, Inject, sleep } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.status = 200; + this.ctx.set('Transfer-Encoding', 'chunked'); + for (let i = 0; i < 100; i++) { + await sleep(100); + this.ctx.res.write('abc'.repeat(100)); + } + + this.ctx.res.end(); + } +} +``` + + + +## Internal redirection + +Starting from v3.12.0, the framework provides an internal redirect API `ctx.forward(url)`, which only supports koa/egg type. + +The difference from external redirection is that internal redirection does not modify the URL of the browser, but only flows inside the program. + +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + return this.ctx.forward('/api'); + } + + @Get('/api') + async api() { + return 'abc'; + } +} +``` + +Note that there are some rules for internal redirects: + +* 1. Redirection will retain all parameters of the original route, that is, transparently transmit the entire ctx + +* 2. Redirection can only be done in the same http method + +* 3. Redirection will not execute Web Middleware again, and guards will not be executed, but interceptors and parameter decorators will be executed + + + +## Global route prefix + +It needs to be set in the `src/config/config.default` configuration. + +Note that different components are configured under different keywords: + + + + +```typescript +// src/config/config.default.ts +export default { + koa: { + globalPrefix: '/v1' + } +}; +``` + + + +```typescript +// src/config/config.default.ts +export default { + egg: { + globalPrefix: '/v1' + } +}; +``` + + + + +```typescript +// src/config/config.default.ts +export default { + express: { + globalPrefix: '/v1' + } +}; +``` + + + + +After configuration, all routes automatically add this prefix. + +If there are special routes that are not required, you can use the decorator parameters to ignore them. + +**Example: Controller Level Ignoring** + +```typescript +// All routes under this Controller will ignore the global prefix +@Controller('/api', {ignoreGlobalPrefix: true}) +export class HomeController { + // ... +} +``` + +**Example: route level ignored** + +```typescript +@Controller('/') +export class HomeController { + // This route will not be ignored + @Get('/', {}) + async homeSet() { + } + + // The route ignores the global prefix + @Get('/bbc', {ignoreGlobalPrefix: true}) + async homeSet2() { + } +} +``` + + + +## Routing priority + + +Midway has already sorted the routes uniformly, and the wildcard route will automatically reduce the priority and be loaded at the end. + + +The rules are as follows: + + +- 1. The absolute path rule has the highest priority, such as `/AB/cb/e`. +- 2. The asterisk can only appear last and must be followed by/. For example, `/AB/cb/**` +- 3. If the absolute path and the general configuration can match one path, the absolute rule has a high priority, such as `/abc/*` and `/abc/d`, then the next absolute route is matched when the `/abc/d` request is requested. +- 4. If multiple wildspaces match a path, the longest rule matches. For example, `/AB/**` and `/AB/cd/**` hit `/AB/cd/**` when matching `/AB/cd/f`. +- 5. If both`/`and `/*` match`/`, the priority of`/`is higher than that of `/*` +- 6. If the weights are the same, such as `/:page/page` and `/page/:page`. + + + +This rule is also consistent with the routing rules of the functions under the Serverless. + + +It is simply understood as "clear routes have the highest priority, long routes have the highest priority, and general distribution has the lowest priority". + + +For example: +```typescript +@Controller('/api') +export class APIController { + @Get('/invoke/*') + async invokeAll() { + } + + @Get('/invoke/abc') + async invokeABC() { + } +} +``` +In this case, `/invoke/abc` is registered first to ensure higher priority. + + +The priority of different Controller will be sorted by length, and the`/`root Controller will be loaded finally. + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/cookie_session.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/cookie_session.md new file mode 100644 index 000000000000..2f894b6008c6 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/cookie_session.md @@ -0,0 +1,356 @@ +# Cookies and Session + +HTTP Cookie (also called Web Cookie or Browser Cookie) is a small piece of data sent by the server to the user's browser and stored locally. It will be carried and sent to the server the next time the browser rerequests the same server. Usually, it is used to tell the server whether the two requests come from the same browser, such as keeping the user logged in. Cookies make it possible for stateless HTTP protocols to record stable state information. Cookie are mainly used in the following three aspects: + +- Session state management (such as user login status, shopping cart, game score, or other information that needs to be recorded) +- Personalization settings (such as user-defined settings, themes, etc.) +- Browser behavior tracking (e. g. tracking and analyzing user behavior, etc.) + +Cookie often assume the function of identifying the requestor's identity in Web applications, so Web applications encapsulate the concept of Session on the basis of cookies and are specially used for user identification. + + + +## Scope of application + +* The built-in cookie under `@midwayjs/web` (i.e. egg) is the cookie that comes with egg. It does not provide replacement capabilities and is not applicable to this document. +* The built-in cookie library under `@midwayjs/express` (i.e. express) is the cookie library that comes with express. It does not provide replacement capabilities and is not applicable to this document. + + + +## Default Cookies + +Midway provides a `@midwayjs/cookies` module to manipulate Cookie. + +At the same time, in `@midwayjs/koa`, the method of directly reading and writing cookies from the context is provided by default. + +- `ctx.cookies.get(name, [options])` Cookie in Read Context Request +- `ctx.cookies.set(name, value, [options])` writes cookie in context + +Examples are as follows: + +```typescript +import { Inject, Controller, Get, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + // set cookie + this.ctx.cookies.set('foo', 'bar', { encrypt: true }); + // get cookie + this.ctx.cookies.get('foo', { encrypt: true }); + } +} +``` + + + +## Set Cookie + +Use the `ctx.cookies.set(key, value, options)` API to set Cookie. + +Setting Cookie is actually done by setting a set-cookie header in the HTTP response. Each set-cookie will allow the browser to store a key-value pair in the cookie. While setting the Cookie value, the protocol also supports many parameters to configure the transmission, storage and permissions of this Cookie. + +These options include: + +| Options | Type | Description | Support Version | +| -------- | ------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| path | String | The path where the key-value pair takes effect. By default, the path is set to the root path (`/`). That is, all URLs under the current domain name can access this cookie. | | +| domain | String | The domain name for which the key-value pair takes effect is not configured by default. It can be configured to be accessed only in the specified domain name. | | +| expires | Date | Set the expiration time of this key-value pair. If maxAge is set, the expires will be overwritten. If maxAge and expires are not set, Cookie will expire when the browser's session fails (usually when the browser is closed). | | +| maxAge | Number | Set the maximum save time for this key-value pair in the browser. is the number of milliseconds from the current time on the server. If maxAge is set, the expires will be overwritten. | | +| secure | Boolean | Set the key-value pair to [transmit only on HTTPS connections](http://stackoverflow.com/questions/13729749/how-does-cookie-secure-flag-work). The framework helps us to determine whether the secure value is automatically set on the HTTPS connection. | | +| httpOnly | Boolean | Set whether the key-value pair can be accessed by JS. The default value is true and JS access is not allowed. | | +| partitioned | Boolean | Set cookies for independent partition status ([CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips)). Note that this configuration will only take effect if `secure` is true and Chrome >=114 version | @midwayjs/cookies >= 1.1.0 | +| removeUnpartitioned | Boolean | Whether to delete the cookie with the same name in the non-independent partition state. Note that this configuration will only take effect when `partitioned` is true. | @midwayjs/cookies >= 1.2.0 | +| priority | String | Set the [Priority](https://developer.chrome.com/blog/new-in-devtools-81?hl=en#cookiepriority) of Cookie, the optional values are `Low`, `Medium`, `High` , only valid for Chrome >= 81 version | @midwayjs/cookies >= 1.1.0 | + +In addition to these attributes, the framework extends 3 additional parameters: + +| Options | Type | Description | +| --------- | ------- | ------------------------------------------------------------ | +| overwrite | Boolean | Set how to handle key-value pairs with the same key. If set to true, the value set later will overwrite the previously set. Otherwise, two set-cookie response headers will be sent. | +| signed | Boolean | Set whether to sign the Cookie. If set to true, the value of the key-value pair will be signed at the same time when the key-value pair is set, and the value will be checked when the key-value pair is taken later, which can prevent the front end from tampering with the value. The default value is true. | +| encrypt | Boolean | Set whether to encrypt the cookie. If set to true, the value of this key-value pair will be encrypted before sending the cookie. The client cannot read the plaintext value of the cookie. The default value is false. | + +When setting a cookie, we need to consider whether the cookie needs to be acquired by the front end, how long it will expire, etc. + +Example: + +```typescript +import { Inject, Controller, Get, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.cookies.set('cid', 'hello world', { + Domain: 'localhost', // write the domain name where the cookie is located + Path: '/index', // the path where the cookie is written + MaxAge: 10*60*1000, // cookie valid duration + expires: new Date('2017-02-15'), // cookie expiration time + httpOnly: false, // is it only used for http requests + overwrite: false, // whether rewrite is allowed + }); + ctx.body = 'cookie is OK'; + } +} +``` + +**By default, cookies are signed and not encrypted. The browser can view plaintext, js cannot access it, and cannot be tampered with by the client.** + +If you want Cookie to be accessed and modified by js on the browser side: + +```typescript +ctx.cookies.set(key, value, { + httpOnly: false, + signed: false, +}); +``` + +If you want the Cookie to not be modified on the browser side, you cannot see the clear text: + +```typescript +ctx.cookies.set(key, value, { + httpOnly: true, // the default is true + encrypt: true, // encrypted transmission +}); +``` + + + +## Get Cookie + +Use the `ctx.cookies.get(key, options)` API to get Cookie. + +Since the cookie in the HTTP request is transmitted in a header, the value of the corresponding key-value pair can be quickly obtained from the entire cookie through this method provided by the framework. When setting cookies above, we can set the `options.signed` and `options.encrypt` to sign or encrypt cookies, so the corresponding matching options should also be passed when obtaining cookies. + +- If it is specified as signed at the time of setting and not specified at the time of acquisition, the obtained value will not be checked during acquisition, which may result in tampering with the client. +- If it is specified as encrypt when setting and not specified when obtaining, the real value cannot be obtained, but the encrypted ciphertext. + +If you want to obtain a Cookie set by the frontend or other systems, you must specify the `signed` parameter to `false` to avoid that the value of the Cookie cannot be obtained. + +```typescript +ctx.cookies.get('frontend-cookie', { + signed: false, +}); +``` + + + +## Cookie key + +Since we need to use encryption, decryption and verification in Cookie, we need to configure a secret key for encryption. + +The default scaffold will automatically generate a secret key in the configuration file `src/config/config.default.ts`, or it can be modified by itself. + +```typescript +// src/config/config.default +export default { + keys: ['key1','key2'], +} +``` + +keys are a string by default, which can be used to separate and configure multiple keys. Cookie when encrypting and decrypting using this configuration: + +- Only the first key is used when encrypting and signing. +- When decrypting and checking, the keys will be traversed for decryption. + +If we want to update the key of the Cookie, but we don't want the Cookie previously set to the user's browser to become invalid, we can configure the new key to the front of the keys and delete the unnecessary key after a period of time. + + + +## Default Session + +The default `@midwayjs/koa` has built-in Session components and provides us with `ctx.session` to access or modify the current user Session. + +```typescript +import { Inject, Controller, Get, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + // Get the content on the Session + const userId = this.ctx.session.userId; + const posts = await this.ctx.service.post.fetch(userId); + // Modify the value of the Session + this.ctx.session.visited = ctx.session.visited? (ctx.session.visited + 1) : 1; + // ... + } +} +``` + +The use of the Session is very intuitive. Just read it or modify it. If you want to delete it, assign it null directly: + +```typescript +ctx.session = null; +``` + +What needs **special attention** is: when setting the session attribute, you need to avoid the following situations (which will cause field loss, see [koa-session](https://github.com/koajs/session/blob/master/lib/session.js#L37-L47) source code) + +- Do not start with `_` +- The value cannot be `isNew`. + +``` +// ❌ Wrong usage +ctx.session._visited = 1; // --> this field will be lost on the next request +ctx.session.isNew = 'HeHe'; // --> is an internal keyword and should not be changed + +// ✔️ The correct usage +ctx.session.visited = 1; // --> no problem here +``` + +The implementation of the Session is based on Cookie. By default, the content Session by the user is encrypted and stored directly in a field in the Cookie. Every time the user requests our website, he will bring this Cookie with him and we will use it after decryption by the server. The default configuration of the Session is as follows: + +```typescript +export default { + session: { + MaxAge: 24*3600*1000, // 1 day + key: 'MW_SESS', + httpOnly: true + }, + // ... +} +``` + +It can be seen that these parameters are cookie parameters except `key`. `key` represents the key of the cookie key value pair that stores the Session. Under the default configuration, cookies stored in Session will be encrypted and cannot be accessed by the front-end js, thus ensuring that the user's Session is secure. + + + +## Session in Serverless + +In the scenario of a function elastic container, the Session module is not built-in by default. You can add it manually if necessary. + +```json +{ + "dependencies": { + "@midwayjs/session": "^3.0.0", + // ... + }, +} +``` + +Introduce components in configuration. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as faas from '@midwayjs/faas'; +import * as session from '@midwayjs/session'; + +@Configuration({ + imports: [ + faas, + session, + // ... + ] +}) +export class MainConfiguration { + // ... +} +``` + + + +## Session example + +### Modify user Session expiration time + +Although one of the Session configurations is maxAge, it can only set the validity period of the Session globally. We can often see the option box to **remember me** on the login page of some websites. After checking, the validity period of the login user's Session can be longer. This Session effective time setting for a specific user can be implemented by `ctx.session.maxAge =`. + +```typescript +import { Inject, Controller, Post, Body, Provide, FORMAT } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { UserService } from './service/user.service'; + +@Controller('/') +export class UserController { + @Inject() + ctx: Context; + + @Inject() + userService: UserService; + + @Post('/') + async login(@Body() data) { + const { username, password, rememberMe } = data; + const user = await this.userService.loginAndGetUser(username, password); + + // Set Session + this.ctx.session.user = user; + // If the user checked "Remember Me", set a 30-day expiration time. + if (rememberMe) { + this.ctx.session.maxAge = FORMAT.MS.ONE_DAY * 30; + } + } +} +``` + + + +### Extend the validity period of user Session + +By default, when the user request does not cause the Session to be modified, the framework will not extend the validity period of the Session. However, in some scenarios, we hope that if users visit our site for a long time, they will extend their Session validity period and prevent users from exiting the login state. The framework provides a `renew` configuration item to implement this function. It will reset the validity period of the Session when it is found that the validity period of the user's Session is only half of the maximum validity period. + +```typescript +// src/config/config.default.ts +export default { + session: { + renew: true + // ... + }, + // ... +} +``` + + + +### Customize SameSite option of Session Cookie + +By default, the framework will leave the SameSite option of Session Cookie to unset. Since Chrome v84, cookies with empty SameSite will be treated as SameSite=Lax, which means when the document is requested cross origins, the cookie won't take effect. If your application is always accessed directly by your users, there won't be any problem. But if your application needs to support cross origin requests, such as being embedded with iframe, or requested from another origin with XHR, then the SameSite option needs to be changed to SiteSite=None: + +```typescript +// src/config/config.default.ts +export default { + session: { + sameSite: 'none', + // SameSite=None cookies must be Secure + secure: true, + // ... + }, + // ... +} +``` + +Please refer to [SameSite Cookie explained](https://web.dev/articles/samesite-cookies-explained?hl=zh-cn) for more introduction about SameSite option. + + + + +## Custom Session Store + +It is not reasonable to put too much data in the Session. In most cases, we only need to store some Id in the Session to ensure security. Although we think Cookie is sufficient as a storage Session, in some extreme cases, Redis, for example, is still needed to store Session. + +Different upper-level frameworks use different Session schemes, and some Session replacement schemes are listed below. + +- [@midwayjs/koa scheme](https://github.com/midwayjs/midway/tree/main/packages/session#custom-session-store) +- [@midwayjs/express Scheme](https://github.com/midwayjs/midway/tree/main/packages/express-session) +- [@midwayjs/Web (egg) scheme](https://github.com/eggjs/egg-session) + + + + + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/custom_decorator.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/custom_decorator.md new file mode 100644 index 000000000000..b0520b53d46c --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/custom_decorator.md @@ -0,0 +1,541 @@ +# Custom decorator + +In the new version, Midway provides custom decorator capabilities supported by the framework, which includes several common functions: + +- Define inheritable attribute decorators +- Define a wrappable method as a method decorator for interception +- Define parameter decorators that modify parameters + +We take into account the current stage of decorators in the standard and subsequent risks. Midway provides a custom decorator method and its supporting capabilities to be implemented by the framework to avoid the problems caused by subsequent specification changes as much as possible. + +In general, we recommend placing custom decorators in the `src/decorator` directory. + +For example: + +``` +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ ├── user.controller.ts +│ │ └── home.controller.ts +│ ├── interface.ts +│ ├── decorator ## custom decorator +│ │ └── user.decorator.ts +│ └── service +│ └── user.service.ts +├── test +├── package.json +└── tsconfig.json +``` + +## Decorator API + +Midway has a set of standard decorator management API, which is used to inject decorator docking dependencies into containers for scanning and expansion. We export these API methods from the `@midwayjs/core` package. + +Through the decorator advanced API, we can customize the decorator and attach metadata to it. The various decorators inside are realized through this capability. + +Common extension APIs are: + +**Decorator** + +- `saveModule` is used to save a class to a decorator +- `listModule` get all classes bound to a type of decorator + +**Meta-information access (corresponding to** [**reflect-metadata**](https://www.npmjs.com/package/reflect-metadata)**)** + +- Save meta information to class `saveClassMetadata` +- `attachClassMetadata` additional meta information to class +- `getClassMetadata` get meta information from class +- `savePropertyDataToClass` saves the property's meta information to the class` +- `attachPropertyDataToClass` meta-information for additional attributes to class +- `getPropertyDataFromClass` get attribute meta information from class +- `listPropertyDataFromClass` listing meta-information for all attributes saved on class +- `savePropertyMetadata` save attribute meta-information to the attribute itself +- `attachPropertyMetadata` additional attribute meta-information to the attribute itself +- `getPropertyMetadata` get the saved meta information from the attribute + +**shortcut** + +- `getProviderUUId` get the uuid provide by class, which corresponds to a class and will not change. +- The name saved when the `getProviderName` gets the provide, usually the class name is lowercase. + +- `getProviderId` get the id provide on the class, which is usually a lowercase class name or a custom id. +- `isProvide` judge whether a class has been modified by @Provide +- `getObjectDefinition` Get Object Definition (ObjectDefiniton) +- `getParamNames` get all parameter names of a function +- `getMethodParamTypes` gets the parameter type of a method, which is equivalent to `Reflect.getMetadata(design:paramtypes)` +- `getPropertyType` get the type of an attribute, which is equivalent to `Reflect.getMetadata(design:type)` +- `getMethodReturnTypes` get method return value type, equivalent to `Reflect.getMetadata(design:returntype)` + +## Class decorator + +Generally, class decorators are used in conjunction with other decorators to mark that a class belongs to a specific scene. For example, `@Controller` indicates that the class belongs to the entrance of the Http scene. + +Let's take an example, define a class decorator @Model, identify class as a model class, and then further operate. + +First create a decorator file, such as `src/decorator/model.decorator.ts`. + +```typescript +import { Scope, ScopeEnum, saveClassMetadata, saveModule, Provide } from '@midwayjs/core'; + +// Provide a unique key +const MODEL_KEY = 'decorator:model'; + +export function Model(): ClassDecorator { + return (target: any) => { + // Bind the decorated class to the decorator to obtain the class later. + saveModule(MODEL_KEY, target); + // Save some metadata information, whatever you want to save. + saveClassMetadata ( + MODEL_KEY, + { + test: 'abc', + }, + target + ); + // Specify the scope of the IoC container to create the instance, which is registered here as the request scope, so that ctx can be retrieved. + Scope(ScopeEnum.Request)(target); + + // Call the Provide decorator so that the user's class can omit the @Provide() decorator. + Provide()(target); + }; +} +``` + +The above only decided on this decorator, and we also need to implement the corresponding functions. midway v2 began to have the concept of life cycle, which can be executed in the life cycle of the `configuration`. + +```typescript +// src/configuration.ts + +import { listModule, Configuration, App, Inject } from '@midwayjs/core'; +import { join } from 'path'; +import * as koa from '@midwayjs/koa'; +import { MODEL_KEY } from './decorator/model.decorator'; + +@Configuration({ + imports: [koa] +}) +export class MainConfiguration { + @App() + app: koa.Application; + + async onReady() { + // ... + + // All classes decorated with @Model() decorators can be obtained + const modules = listModule(MODEL_KEY); + for (let mod of modules) { + // Realize custom capability + // For example, take metadata getClassMetadata(mod) + // For example, initialize app.applicationContext.getAsync(mod) in advance; + } + } +} +``` + +Finally, we're going to use this decorator. + +```typescript +import { Model } from '../decorator/model.decorator'; + +// The role of Model is that our own logic can be executed (saved metadata) +@Model() +export class UserModel { + // ... +} +``` + +## Property decorator + +Midway provides `createCustomPropertyDecorator` methods for creating custom attribute decorators. decorators such as `@Logger` and `@Config` of the framework are all created in this way. + +Unlike the decorator defined in the TypeScript, the attribute decorator provided by Midway can be used in inheritance. + +Let's take an example. If there is a memory cache now, our property decorator is used to obtain cache data. Here are some preparations. + +```typescript +// Simple cache class +import { Configuration, Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MemoryStore extends Map { + save(key, value) { + this.set(key, value); + } + + get(key) { + return this.get(key); + } +} + +// src/configuration.ts +// The entry is instantiated and some data is saved. +import { Configuration, App, Inject } from '@midwayjs/core'; +import { join } from 'path'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + imports: [koa] +}) +export class MainConfiguration { + @App() + app: koa.Application; + + @Inject() + store: MemoryStore; + + async onReady() { + // ... + + // Initialize some data + store.save('aaa', 1); + store.save('bbb', 1); + } +} +``` + +Let's implement a simple `@MemoryCache()` decorator. The implementation of the property decorator is divided into two parts: + +- 1. define a decorator method, generally only save metadata +- 2. Define an implementation before the decorator logic is executed + +The following is the section that defines the decorator method. + +```typescript +// src/decorator/memoryCache.decorator.ts +import { createCustomPropertyDecorator } from '@midwayjs/core'; + +// Unique id inside the decorator +export const MEMORY_CACHE_KEY = 'decorator:memory_cache_key'; + +export function MemoryCache(key?: string): PropertyDecorator { + return createCustomPropertyDecorator(MEMORY_CACHE_KEY, { + key + }); +} +``` + +It is implemented before the decorator's method is executed (usually at the initialization place). To realize the decorator, we need to use the built-in `MidwayDecoratorService` service. + +```typescript +import { Configuration, Inject, Init } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { MEMORY_CACHE_KEY, MemoryStore } from 'decorator/memoryCache.decorator'; +import { MidwayDecoratorService } from '@midwayjs/core'; + +@Configuration({ + imports: [koa] +}) +export class MainConfiguration { + @App() + app: koa.Application; + + @Inject() + store: MemoryStore; + + @Inject() + decoratorService: MidwayDecoratorService; + + @Init() + async init() { + // ... + + // Realize decorator + this.decoratorService.registerPropertyHandler(MEMORY_CACHE_KEY, (propertyName, meta) => { + return this.store.get(meta.key); + }); + } +} +``` + +The `registerPropertyHandler` method contains two parameters, the first is the unique id defined by the previous decorator, and the second is the callback method implemented by the decorator. + +`propertyName` is the method name of the decorator decoration, and meta is the parameter when the decorator is used. + +Then we can use this decorator. + +```typescript +import { MemoryCache } from 'decorator/memoryCache.decorator'; + +// ... +export class UserService { + @MemoryCache('aaa') + cacheValue; + + async invoke() { + console.log(this.cacheValue); + // => 1 + } +} +``` + +## Method decorator + +Midway provides `createCustomMethodDecorator` methods for creating custom method decorators. + +Different from the decorator defined in the TypeScript, the method decorator provided by Midway is uniformly implemented by interceptors, does not conflict with other interception methods, and is simpler. + +Let's take the printing method execution time as an example. + +Like property decorators, our definition and implementation are separate. + +The following is the section that defines the decorator method. + +```typescript +// src/decorator/logging.decorator.ts +import { createCustomMethodDecorator } from '@midwayjs/core'; + +// Unique id inside the decorator +export const LOGGING_KEY = 'decorator:logging_key'; + +export function LoggingTime(formatUnit = 'ms'): MethodDecorator { + // We pass a parameter that modifies the display format + return createCustomMethodDecorator(LOGGING_KEY, { formatUnit }); +} +``` + +The implementation part also needs to use the framework's built-in `DecoratorService` service. + +```typescript +//... + +function formatDuring(value, formatUnit: string) { + // Return time formatting here + if (formatUnit === 'ms') { + return `${value} ms`; + } else if (formatUnit === 'min') { + // return xxx + } +} + +@Configuration({ + imports: [koa] +}) +export class MainConfiguration { + @App() + app: koa.Application; + + @Inject() + decoratorService: MidwayDecoratorService; + + @Logger() + logger; + + async onReady() { + // ... + + // Implementation method decorator + this.decoratorService.registerMethodHandler(LOGGING_KEY, (options) => { + return { + around: async (joinPoint: JoinPoint) => { + // Get the formatting parameters + const format = options.metadata.formatUnit || 'ms'; + + // Record start time + const startTime = Date.now(); + + // Execute the original method + const result = await joinPoint.proceed(...joinPoint.args); + + const during = formatDuring(Date.now() - startTime, format); + + // Print execution time + this.logger.info('Method ${joinPoint.methodName} invoke during ${during}'); + + // Return execution result + return result; + }, + }; + }); + } +} +``` + +The first parameter of the `registerMethodHandler` method is the id defined by the decorator, and the second parameter is the implementation of the callback. The parameter is the options object, including: + +| Parameters | Type | Description | +| -------------------- | ------------- | ---------------------- | +| options.target | new (...args) | The class in which the decorator is decorated. | +| options.propertyName | string | The name of the method where the decorator is decorated. | +| options.metadata | {} | Parameters of the decorator itself | + +To implement a callback, you must return a method that is processed by the interceptor. The key is the `before`, `around`, `afterReturn`, `afterThrow`, and `after` of the interceptor. + +Since the method decorator itself is implemented by the interceptor, you can view the [interceptor](aspect) section for specific interception methods. + +Use the decorator as follows: + +```typescript +// ... +export class UserService { + @LoggingTime() + async getUser() { + // ... + } +} + +// When executing +// output => Method "getUser" invoke during 4ms +``` + +:::caution + +Note that the decorated method must be an async method. + +::: + + + +## Method decorator without implementation + +By default, the custom method decorator must have an implementation, otherwise the runtime will report an error. + +In some special cases, it is desirable to have a decorator that does not need to be implemented, such as only storing metadata without blocking. + +You can add an impl parameter when defining the decorator. + +```typescript +// src/decorator/logging.decorator.ts +import { createCustomMethodDecorator } from '@midwayjs/core'; + +// Unique id inside the decorator +export const LOGGING_KEY = 'decorator:logging_key'; + +export function LoggingTime(): MethodDecorator { + // The last parameter tells the framework, no need to specify the implementation + return createCustomMethodDecorator(LOGGING_KEY, {}, false); +} +``` + +## Parameter decorator + +Midway provides `createCustomParamDecorator` methods for creating custom parameter decorators. + +Parameter decorators are generally used to modify parameter values and preprocess data in advance. The decorators of request series such as `@Query` of Midway are implemented based on them. + +Like other decorators, our definition and implementation are separate. Let's take the user (ctx.user) in the parameter as an example. + +The following is the section that defines the decorator method. + +```typescript +// src/decorator/logging.decorator.ts +import { createCustomParamDecorator } from '@midwayjs/core'; + +// Unique id inside the decorator +export const USER_KEY = 'decorator:user_key'; + +export function User(): ParameterDecorator { + return createCustomParamDecorator(USER_KEY, {}); +} +``` + +The implementation part also needs to use the `DecoratorService` service built into the framework. + +```typescript +//... + +@Configuration({ + imports: [koa] +}) +export class MainConfiguration { + @App() + app: koa.Application; + + @Inject() + decoratorService: MidwayDecoratorService; + + @Logger() + logger; + + async onReady() { + // ... + + // Implement parameter decorator + this.decoratorService.registerParameterHandler(USER_KEY, (options) => { + // originArgs is the original method + // The first parameter here is ctx, so ctx.user is taken. + return options.originArgs[0]?.user ?? {}; + }); + } +} +``` + +The first parameter of the `registerParameterHandler` method is the id defined by the decorator, and the second parameter is the implementation of the callback. The parameter is the options object, including: + +| Options | Type | Description | +| ----------------------- | --------------- | ---------------------- | +| options.target | new (...args) | The class in which the decorator is decorated. | +| options.propertyName | string | The name of the method where the decorator is decorated. | +| options.metadata | {} \| undefined | Parameters of the decorator itself | +| options.originArgs | Array | The original parameters of the method | +| options.originParamType | | The original parameter type of the method | +| options.parameterIndex | number | Parameter Index of Decorator Modification | + +Use the decorator as follows: + +```typescript +// ... +export class UserController { + async invoke(@User() user: string) { + console.log(user); + // => xxx + } +} +``` + +:::tip + +Note that for the correctness of the method call, if an error is reported in the parameter decorator, the framework will use the original parameters to call the method and will not throw an exception directly. + +You can find this error when turning on the `NODE_DEBUG = midway:debug` environment variable. + +::: + +:::caution + +Note that the decorated method must be an async method. + +::: + +## Method decorator gets context + +On the request link, it is often difficult to get the context if the decorator is customized. If the code does not explicitly inject the context, it is very difficult to get it in the decorator. + +In Midway's dependency injection request scope, we bind the context to each instance, and obtain the current context from the specific attribute of the instance `REQUEST_ OBJ_CTX_KEY`, thus further operating on the request. + +For example, in our custom implementation of the method decorator: + +```typescript +import { REQUEST_OBJ_CTX_KEY } from '@midwayjs/core'; +//... + +export class MainConfiguration { + @App() + app: koa.Application; + + @Inject() + decoratorService: MidwayDecoratorService; + + @Logger() + logger; + + async onReady() { + // ... + + // Implementation method decorator + this.decoratorService.registerMethodHandler(LOGGING_KEY, (options) => { + return { + around: async (joinPoint: JoinPoint) => { + // Instance where the decorator is located + const instance = joinPoint.target; + const ctx = instance[REQUEST_OBJ_CTX_KEY]; + // ctx.xxxx + // ... + }, + }; + }); + } +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/custom_error.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/custom_error.md new file mode 100644 index 000000000000..b7160c8230de --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/custom_error.md @@ -0,0 +1,165 @@ +# Custom error + +In Node.js, each exception is an instance of the built-in Error type. + +By extending the standard Error,Midway provides built-in error types with additional attributes. + +```typescript +export class MidwayError extends Error { + // ... +} +``` + +At this stage, all errors provided by Midway framework are instances thrown by this error class. + +MidwayError includes several properties: + +- The name of the name error, such as Error,TypeError, etc., is the class name of the custom error in the custom error. +- message error message +- Stack error stack +- Code custom error code +- Cause the source of the error + + + +We can use it by simply instantiating and throwing it out, such: + +```typescript +import { MidwayError } from '@midwayjs/core'; + +// ... + +async findAll() { + throw new MidwayError('my custom error'); +} +``` + +Some errors can also be customized in the business. + +In common, we will uniformly define exceptions into the error directory. + +``` +➜ my_midway_app tree +. +├── src +│ └── error +│ ├── customA.error.ts +│ └── customB.error.ts +├── test +├── package.json +└── tsconfig.json +``` + +If the business has some reuse exceptions, such as fixed errors + +```typescript +// src/error/custom.error.ts +import { HttpStatus } from '@midwayjs/core'; + +export class CustomError extends MidwayError { + constructor() { + super('my custom error', 'CUSTOM_ERROR_CODE_10000'); + } +} +``` + +Then throw the use in the business. + +```typescript +import { CustomError } from './error/custom.error'; + +// ... + +async findAll() { + throw new CustomError(); +} + +``` + +The above `CUSTOM_ ERROR_CODE_10000` is the wrong error code. Generally, we will assign different error codes and error messages to different errors to facilitate troubleshooting. + + + +## Custom error code + +The framework provides a universal mechanism for registering error codes. Error codes can be easily debuggered and counted later. + +It is very useful when business errors are defined and component errors are defined. + +The error code is generally an enumeration value, such: + +```typescript +const CustomErrorEnum = { + UNKNOWN: 10000 + COMMON: 10001 + PARAM_TYPE: 10002, + // ... +}; +``` + +In encoding, we will provide a fixed error code and hope that there will be no conflicts in the SDK or components, which requires framework support. + +Midway provides `registerErrorCode` method for registering non-duplicate error codes with the framework and for certain formatting. + +For example, within the framework, we have the following definition: + +```typescript +import { registerErrorCode } from '@midwayjs/core'; + +export const FrameworkErrorEnum = registerErrorCode('midway', { + UNKNOWN: 10000 + COMMON: 10001 + PARAM_TYPE: 10002, + // ... +} as const); +``` + +The `registerErrorCode` contains two parameters: + +- Error grouping, such as `midway`, is the name of the built-in error group in the framework. In an application, this group name should not be repeated. +- The error enumeration object is named key and the error code is value. + + + +The method returns an error enumeration value with the error name as the key and the error group plus the error code as the value. + +For example: + +```typescript +FrameworkErrorEnum.UNKNOWN +// => output: MIDWAY_10000 + +FrameworkErrorEnum.COMMON +// => output: MIDWAY_10001 +``` + +In this way, when the `MIDWAY_10000` error code appears in the error, we will know what the error is, and all the errors can be precipitated by cooperating with the document. + +When defining errors, use this error code enumeration directly. + +```typescript +export class MidwayParameterError extends MidwayError { + constructor(message?: string) { + super(message ?? 'Parameter type not match', FrameworkErrorEnum.PARAM_TYPE); + } +} + +// user code +async findAll(data) { + if (! data.user) { + throw new MidwayParameterError(); + } + // ... +} + +// output +// 2022-01-02 14:02:29,124 ERROR 14259 MidwayParameterError: Parameter type not match +// at APIController.findAll (.... +// at /Users/harry/project/midway-v3/packages/core/src/common/webGenerator.ts:38:57 +// at processTicksAndRejections (node:internal/process/task_queues:96:5) { +// code: 'MIDWAY_10002', +// cause: undefined +//} + +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/data_listener.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/data_listener.md new file mode 100644 index 000000000000..4e7e6dde049b --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/data_listener.md @@ -0,0 +1,121 @@ +# Data subscription + +In some scenarios, we want to subscribe to a certain data and update it after a period of time. This kind of subscription-like method, which we call "data subscription", common remote data acquisition, etc., can apply this mode. + +Midway provides `DataListener` abstractions to easily create code for this pattern. + +## Realize data subscription + +Let's take a simple **memory data update** requirement as an example. + +Data subscription is also a common class in midway. For example, we can also put it in `src/listener/memory.listner.ts`. + +We only need to inherit the built-in `DataListener` class, and at the same time, the general data subscription class is singleton. + +`DataListener` contains a generic type, you need to declare the data type returned by the data subscription. + +For example: + +```typescript +// src/listener/memory.listner.ts +import { DataListener, Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MemoryDataListener extends DataListener { + // Initialize data + initData() { + return 'hello' + Date.now(); + } + + // Update data + onData(setData) { + setInterval(() => { + setData('hello' + Date.now()); + }, 1000); + } +} +``` + +`DataListener` class has two methods that must be implemented: + +- Initialization method of `initData` data +- `onData` + +In the example, we initialize the data and implement the data update method. Every 1 second, we use `setData` to update the built-in data. + +In addition, most data subscriptions use timers or other external SDKs. We need to consider shutting down and cleaning up resources. + +`destroyListener` methods are provided in the code to handle. + +For example, in the above sample code, we need to turn off the timer. + +```typescript +// src/listener/memory.listner.ts +import { DataListener, Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MemoryDataListener extends DataListener { + private intervalHandler; + + // Initialize data + initData() { + return 'hello' + Date.now(); + } + + // Update data + onData(setData) { + this.intervalHandler = setInterval(() => { + setData('hello' + Date.now()); + }, 1000); + } + + // Clean up resources + async destroyListener() { + // Turn off timer + clearInterval(this.intervalHandler); + // Other cleanup, close sdk, etc + } + +} +``` + +The `initData` method above can fetch data asynchronously. + +```typescript +// ... +export class MemoryDataListener extends DataListener { + async initData() { + // ... + } +} +``` + + + +## Use data subscription + +We can use it in any code to obtain the current data through `getData` methods in the business without considering the data changes. + +For example: + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { MemoryDataListener } from '../listener/memory.listner.ts'; + +@Provide() +export class UserService { + + @Inject() + memoryDataListener: MemoryDataListener; + + async getUserHelloData() { + const helloData = this.memoryDataListener.getData(); + // helloData => helloxxxxxxxx + // ... + } +} +``` + +The data subscription mode can easily hide the changed data in the common class, and reveal the unchanged API, making the standard business code in the logic and process of the concise. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/data_source.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/data_source.md new file mode 100644 index 000000000000..6214f3c82b36 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/data_source.md @@ -0,0 +1,365 @@ +# Data source management + +In the process of using database packages, we often have multi-database connection and management requirements. Different databases have certain differences in connection pool management, connection status, and usage methods. + +Although we can use the service factory to abstract, whether it is semantics or some functions, it is slightly different from the service factory, such as the ability to load entity classes, which is unique to the data source. + +For this reason, Midway provides `DataSourceManager` abstraction to facilitate the management of data sources. + +Take `mysql2` as an example to implement a `mysql2` connection pool management class. + +The following is the official example of `mysql2`, as a preparatory work. + +```typescript +// get the client +const mysql = require('mysql2'); + +// create the connection to database +const connection = mysql.createConnection({ + host: 'localhost', + user: 'root', + database: 'test' +}); + +// simple query +connection.query ( + 'SELECT * FROM `table` WHERE `name` = "Page" AND `age` > 45', + function(err, results, fields) { + console.log(results); // results contains rows returned by server + console.log(fields); // fields contains extra meta data about results, if available + } +); +``` + +Similar to service factories, we need to implement some fixed methods. + +- 1. Method of creating a data source +- 2. check the connection method + + + +## Implement data source manager + +The data source manager is also a common export class in midway, for example, we can also put it in `src/manager/mysqlDataSourceManager.ts`. + + + +### 1. Create a data source interface + +We only need to inherit the built-in `DataSourceManager` class to implement a data source manager. + +`DataSourceManager` contain a generic type, you need to declare the data type of the data source. + +```typescript +import { DataSourceManager, Provide, Scope, ScopeEnum } from '@midwayjs/core'; +import * as mysql from 'mysql2'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MySqlDataSourceManager extends DataSourceManager { + // ... +} + +``` + +Since it is an abstract class, we need to implement several basic methods. + +```typescript +import { DataSourceManager, Provide, Scope, ScopeEnum } from '@midwayjs/core'; +import * as mysql from 'mysql2'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MySqlDataSourceManager extends DataSourceManager { + // Create a single instance + protected async createDataSource(config: any, dataSourceName: string): Promise { + return mysql.createConnection(config); + } + + getName(): string { + return 'mysql'; + } + + async checkConnected(dataSource: mysql.Connection): Promise { + // Pseudocode + return dataSource.status === 'connected'; + } + + async destroyDataSource(dataSource: mysql.Connection): Promise { + if (await this.checkConnected(dataSource)) { + await dataSource.destroy(); + } + } +} + +``` + + + +### 2. Provide initialization configuration + +You can use the `@Init` decorator and the `@Config` decorator to provide initialization configurations. + +```typescript +import { DataSourceManager, Provide, Scope, ScopeEnum, Init, Config } from '@midwayjs/core'; +import * as mysql from 'mysql2'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MySqlDataSourceManager extends DataSourceManager { + + @Config('mysql') + mysqlConfig; + + @Inject() + baseDir: string; + + @Init() + async init() { + // It should be noted that the second parameter here needs to pass in an entity class scan address + await this.initDataSource(this.mysqlConfig, this.baseDir); + } + + // ... +} + + +``` + +In the `src/config/config.default`, we can provide the configuration of multiple data sources to create multiple data sources. + +For example: + +```typescript +// config.default.ts +export const mysql = { + dataSource: { + dataSource1: { + host: 'localhost', + user: 'root', + database: 'test' + }, + dataSource2: { + host: 'localhost', + user: 'root', + database: 'test' + }, + dataSource3: { + host: 'localhost', + user: 'root', + database: 'test' + }, + } + // Other configurations +} +``` + +Data sources are naturally designed for multiple instances. Unlike service factories, there is no difference between single and multiple configurations. + + + + + +## Entity binding + +The most important part of the data source is the entity class, each data source can have its own entity class. For example, orm frameworks such as typeorm are designed based on this. + + + +### 1. Explicitly associate entity classes + +Entity classes are generally the same class as the table structure. + +For example: + +```typescript +// src/entity/user.entity.ts +// Here is the pseudo code, the decorator needs to implement it by itself. +@Entity() +export class SimpleUser { + @Column() + name: string; +} + +@Entity() +export class User { + @Column() + name: string; + + @Column() + age: number; +} +``` + +The data source manager binds these entity classes to the data source through a fixed configuration. + +```typescript +// config.default.ts +import { User, SimpleUser } from '../entity/user.entity'; + +export default { + mysql: { + dataSource: { + dataSource1: { + host: 'localhost', + user: 'root', + database: 'test', + entities: [User] + }, + dataSource2: { + host: 'localhost', + user: 'root', + database: 'test', + entities: [SimpleUser] + }, + // ... + } + } +} +``` + +The `entities` configuration of each data source can add its own entity class. + + + +### 2. Directory Scan Associated Entities + +In some cases, we can also replace it with a matching path, such: + +```typescript +// config.default.ts +import { User, SimpleUser } from '../entity/user.entity'; + +export default { + mysql: { + dataSource: { + dataSource1: { + host: 'localhost', + user: 'root', + database: 'test', + entities: [ + User + SimpleUser + 'entity', // specific directory (equivalent to directory wildcard) + '**/abc/**', // Only get files in directories containing abc characters + 'abc/**/*.ts', // specific directory + wildcard + 'abc/*.entity.ts', // match suffix + '**/*.entity.ts', // wildcard plus suffix match + '**/*.{j,t}s', // suffix match + ] + }, + // ... + // ... + } + } +} +``` + +:::caution + +Attention + +- 1. When filling in the directory string, use the second parameter of the initDataSource method as a relative path search, and the default is baseDir (src or dist) +- 2. If the suffix is matched, the path of entities should include the js and ts suffixes, otherwise the entity will not be found after compilation +- 3. The writing method of the string path does not support [single-file build deployment](./deployment#single-file build deployment) (bundle mode) + +::: + + + +### 2. Obtain the data source according to the entity + +Generally, our API is on data source objects, such as `connection.query`. + +So in many cases, such as custom decorators, you need a method to get data source objects from entities. + +```typescript +// The following is the pseudo code +import { SimpleUser } from '../entity/user.entity'; + +class UserService { + // A Model corresponding to the entity class will be injected here, including adding, deleting, modifying and checking methods. + @InjectEntityModel(SimpleUser) + userModel; + +} +``` + +If the entity class corresponds to only one data source, we can obtain the data source by `getDataSourceNameByModel`. + +```typescript +this.mysqlDataSourceManager.getDataSourceNameByModel(SimpleUser); + +// => dataSource1 +``` + +In the case of multiple data sources, the data source obtained by this method may not be accurate, and the last set data source will be obtained. + +In this case, users are generally required to manually specify the data source, such: + +```typescript +// The following is the pseudo code +import { SimpleUser } from '../entity/user.entity'; + +class UserService { + @InjectEntityModel(SimpleUser, 'dataSource2') + userModel; +} +``` + +The default data source can also be specified explicitly via the `defaultDataSourceName` configuration. + +```typescript +// config.default.ts +export const mysql = { + dataSource: { + dataSource1: { + // ... + }, + dataSource2: { + // ... + }, + dataSource3: { + // ... + }, + } + defaultDataSourceName: 'dataSource2', +} +``` + + + +## Get data source + +By injecting the data source manager, we can get the data source through the above methods. + +```typescript +import { MySqlDataSourceManager } from './manager/mysqlDataSourceManager'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + mysqlDataSourceManager: MySqlDataSourceManager; + + async invoke() { + + const dataSource = this.mysqlDataSourceManager.getDataSource('dataSource1'); + // TODO + + } +} +``` + +In addition, there are some other methods. + +```typescript +// Whether the data source exists +this.mysqlDataSourceManager.hasDataSource('dataSource1'); +// Get all data source names +this.mysqlDataSourceManager.getDataSourceNames(); +// Whether the data source is connected +this.mysqlDataSourceManager.isConnected('dataSource1') +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/debugger.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/debugger.md new file mode 100644 index 000000000000..6f69ae701fe2 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/debugger.md @@ -0,0 +1,77 @@ +# Debugger + +This section describes how to debug a Midway project in a common editor. + +## Debugging in VSCode + +### Method 1: Use JavaScript Debug Teminal + +Pull out under the VSCode terminal and hide a `JavaScript Debug Terminal`. Click on it and the created terminal will have its own debugging capability. +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01HWzQEu1cQ6C7q9OYh_!!6000000003594-2-tps-1030-364.png) + +If you enter any command, Debug is automatically enabled. For example, after you enter `npm run dev`. +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01nnkbOQ1YN79M1svVV_!!6000000003046-2-tps-1500-570.png) + + + +### Method 2: Configure debug files + +Create a startup file for vscode. +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01WzgZwN23WVMLYP4Xs_!!6000000007263-2-tps-645-344.png) +Select any one and create a `.vscode/launch.json` file, +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01pP7ntf1HRNMmTeGBT_!!6000000000754-2-tps-655-231.png) + + +Copy the following. + +```json +{ + // Use IntelliSense to understand related attributes. + // Hover to view a description of an existing attribute. + // For more information, please visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0 ", + "configurations": [{ + "name": "Midway Local ", + "type": "node ", + "request": "launch ", + "cwd": "${workspaceRoot} ", + "runtimeExecutable": "npm ", + "windows": { + "runtimeExecutable": "npm.cmd" + }, + "runtimeArgs": [ + "run ", + "dev" + ], + "env": { + "NODE_ENV": "local" + }, + "console": "integratedTerminal ", + "protocol": "auto ", + "restart": true + "port": 7001 + "autoAttachChildProcesses": true + }] +} + +``` + +Just start the breakpoint. +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01AGHSI51zZvrKgS9xx_!!6000000006729-2-tps-1470-1020.png) + + + +## Debugging in WebStorm/Idea + +Start configuring IDE. +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01bmrjiW1frz9dLpdEZ_!!6000000004061-2-tps-1110-692.png) + +Configure the npm command. +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01e4yJnU1QT3MOImlpR_!!6000000001976-2-tps-620-946.png) + +After you select `package.json`, drop down and select `Scrips`, which is the command in the `scripts` configured in `package.json`. Select the command you want, such as `dev` or `test`. +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01DBqmwD1rtbwqpuQZe_!!6000000005689-2-tps-1500-1017.png) + +Debugging can be performed after the code breakpoint. +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01sGzfeH1iLPpzSIWSg_!!6000000004396-2-tps-1327-907.png) + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/decorator_index.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/decorator_index.md new file mode 100644 index 000000000000..bc7878b91fe7 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/decorator_index.md @@ -0,0 +1,101 @@ +# Existing decorator index + +Midway provides a lot of decorator capabilities. These decorators are distributed in different packages and also provide different functions. This chapter provides a quick check list. + +## @midwayjs/core + +| Decorator | Modification position | Description | +| ------------------ | ------------ | ----------------------------------------- | +| @Provide | Class | Expose a class to enable IoC containers to obtain metadata | +| @Inject | Property | Inject objects into an IoC container | +| @Scope | Class | Specify scope | +| @Init | Method | The method that is automatically executed when the annotation object is initialized. | +| @Destroy | Method | The method performed when the annotation object is destroyed. | +| @Async | Class | **[Deprecated]** Indicates that it is an asynchronous function | +| @Autowire | Class | **[Deprecated]** The identification class is an automatic injection attribute | +| @Autoload | Class | Allows classes to self-load execution | +| @Configuration | Class | Identifies a container entry configuration class | +| @Aspect | Class | Identification interceptor | +| @Validate | Method | Identification method, need to be verified | +| @Rule | Property | Check rules that identify DTO | +| @App | Property | Inject the current application instance | +| null | Property | Get configuration | +| @Logger | Property | Get a log instance | +| @Controller | Class | Identified as a Web controller | +| @Get | Method | Register as a route of GET type | +| @Post | Method | Register as a POST type route | +| @Del | Method | Register as a route of type DELETE | +| @Put | Method | Registered as a PUT type route | +| @Patch | Method | Register as a PATCH type route | +| @Options | Method | Register as a route of OPTIONS type | +| @Head | Method | Register as a route of type HEAD | +| @All | Method | Register as a full-type route | +| @Session | Parameter | Get ctx.session from parameter | +| @Body | Parameter | Get ctx.request.body from parameters | +| @Query | Parameter | Get ctx.query from parameter | +| @Param | Parameter | Get ctx.param from parameter | +| @Headers | Parameter | Get ctx.headers from parameter | +| @File | Parameter | Get the first upload file from the parameter | +| @Files | Parameter | Get all uploaded files from parameters | +| @Fields | Parameter | Get Form Field from Parameters (when uploading) | +| @Redirect | Method | Modify response jump | +| @HttpCode | Method | Modify the response status code | +| @SetHeader | Method | Modify response header | +| @ContentType | Method | Modify the Content-Type field in the response header | +| @Schedule | Class | Identified as an egg timed task | +| @Plugin | Property | Get egg plug-in | +| @Provider | Class | Exposed microservice providers (producers) | +| @Consumer | Class | Exposed microservice caller (consumer) | +| @GrpcMethod | Method | Identify exposed gRPC methods | +| @Func | Class/Method | **[Deprecated]** is identified as a function entry | +| @Handler | Method | **[Deprecated]** Cooperate with Marking Function | +| null | Method | Identifies a function trigger | +| @Task | Method | Define a distributed task | +| @TaskLocal | Method | Define a local task | +| null | Class | Define a self-triggered task | + + + +## @midwayjs/typeorm + +| Decorator | Modification position | Role | +| --------------------- | -------- | ---------------- | +| @EntityModel | Class | Define an entity object | +| @InjectEntityModel | Property | Inject an entity object | +| @EventSubscriberModel | Class | Define event subscriptions | + + + +## @midwayjs/validate + +| Decorator | Modification position | Description | +| --------- | -------- | ---------------------- | +| @Rule | Property | Define a rule | +| @Validate | Method | Identify a method that requires verification | + + + +## @midwayjs/swagger + +| Decorator | Modification position | Description | +| ----------------------- | ----------------- | ---- | +| `@ApiBody` | Method | | +| `@ApiExcludeEndpoint` | Method | | +| `@ApiExcludeController` | Class | | +| `@ApiHeader` | Class/Method | | +| `@ApiHeaders` | Class/Method | | +| `@ApiOperation` | Method | | +| `@ApiProperty` | Property | | +| `@ApiPropertyOptional` | Property | | +| `@ApiResponseProperty` | Property | | +| `@ApiQuery` | Method | | +| `@ApiResponse` | Method | | +| `@ApiTags` | Controller/Method | | +| `@ApiExtension` | Method | | +| `@ApiBasicAuth` | Controller | | +| `@ApiBearerAuth` | Controller | | +| `@ApiCookieAuth` | Controller | | +| `@ApiOAuth2` | Controller | | +| `@ApiSecurity` | Controller | | +| `@ApiParam` | Method | | +| `@ApiParam` | Method | | diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/deployment.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/deployment.md new file mode 100644 index 000000000000..19b85274b784 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/deployment.md @@ -0,0 +1,823 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Start-up and deployment + +Midway provides a lightweight launcher for launching your app. We provide a variety of deployment models for applications. You can either deploy the application to any server (such as a server purchased by yourself) in the traditional way, or you can build the application as a Serverless application. Midway provides a cross-cloud deployment method. + + +## Local development + + +Here are two ways to use the `dev` command for local development. + + +### Quickly start a single service + + +In local development, Midway provides a `dev` command startup framework in `package.json`, such: + + + + + +```json +{ + "scripts": { + "dev": "mwtsc --watch --run @midwayjs/mock/app.js", + } +} +``` + +This is the most streamlined command. It has the following features: + + +- 1. Use the `mwtsc` tool to build the code. After success, read the built code through the `app.js` file in the `@midwayjs/mock` package to start the project +- 2. Use the built-in API (`initializeGlobalApplicationContext` of @midwayjs/core) to create a service without going through `bootstrap.js` +- 3. Single process operation + + + + + +```json +{ + "script": { + "dev": "midway-bin dev --ts" + } +} +``` +This is the most concise command, it has the following characteristics: + + +- 1. Use `--ts` to specify the TypeScript(ts-node) environment to start +- 2. Use the built-in API(@midwayjs/core `initializeGlobalApplicationContext`) to create a service without `bootstrap.js` +- 3. Single process operation + + + + + +Run the following command on the command line to execute. +```bash +$ npm run dev +``` + + + +### Specify the portal to start the service + +Because the local dev command is usually different from the initialization parameters of the `bootstrap.js` startup file, some users are worried about the inconsistency between local development and online development, such as testing links. + +In this case, you can directly pass an entry file to the `dev` command and use the entry file to start the service. + + + + + +```json +{ + "scripts": { + "dev": "mwtsc --watch --run bootstrap.js", + }, +} +``` + + + + + + +```json +{ + "script": { + "dev": "midway-bin dev --ts --entryFile=bootstrap.js" + } +} +``` + + + + + + +## Deploy to server + + +### The difference between post-deployment and local development + + +After deployment, some places are different from local development. + + +**1. Changes in the node environment** + + +The biggest difference is that after the server is deployed, node will be used directly to start the project. + +* If you use `mwtsc` to develop the project, the difference is not big +* If `@midwayjs/cli` is used, `ts-node` will not be used to start the project, which means that `*.ts` files will no longer be read + +**2. Changes in the loading directory** + + +After the server is deployed, only the built `dist` directory is loaded, while the local development `src` directory is loaded. + +| | Local | Server | +| --- | --- | --- | +| appDir | Project root directory | Project root directory | +| baseDir | src directory under the root directory of the project | dist directory under the root directory of the project | + +**3. Changes in the environment** + + +In the server environment, `NODE_ENV=production` is generally used. Many libraries will provide better performance methods in this environment, such as enabling caching, error reporting, etc. + +**4. Log files** + + +In the general server environment, logs are not printed to the logs directory of the project, but other directories that are not affected by project updates, such as `home/admin/logs`. This fixed directory also facilitates other tools to collect logs. + + +### Deploy process + + +The entire deployment is divided into several parts. Since Midway is written TypeScript, it adds a build step to the traditional JavaScript code. The entire deployment process is as follows. + +![](https://img.alicdn.com/imgextra/i3/O1CN01wSpCuM27pWGTDeDyK_!!6000000007846-2-tps-2212-242.png) +Since deployment is very relevant to the platform and environment, we will demonstrate it with Linux below, and other platforms can refer to it as appropriate. + + +### Compile code and install dependencies + + +Since Midway project is TypeScript written, we compile it before deployment. In this example, we have written the build script in advance and run the `npm run build` command. If not, add the following `build` command to `package.json`. + + + + + +```typescript +{ + "scripts": { + "build": "mwtsc --cleanOutDir", + }, +} +``` + + + + + +```json +// package.json +{ + "scripts": { + "build": "midway-bin build -c" + }, +} +``` + + + + + +:::info +Although it is not necessary, it is recommended that you perform the test and lint first. +::: + + +Generally speaking, the deployment build environment and the local development environment are two sets. We recommend building your application in a clean environment. + + +The following code is a sample script that you can save as `build.sh`. + +```bash +## Server build (code downloaded) +$ npm install # installation and development period dependency +$ npm run build # build project +$ npm prune --production # remove development dependencies + +## Local build (dev dependency has been installed) +$ npm run build +$ npm prune --production # remove development dependencies +``` + +:::info +General installation dependencies specify `NODE_ENV = production` or `npm install-production` only dependencies dependencies are installed when building formal packages. Because the modules in the devDependencies are too large and will not be used in the production environment, unknown problems may also be encountered after installation. +::: + + +After the build is completed, the `dist` directory of the Midway build product appears. +```text +➜ my_midway_app tree +. +├── src +├── dist # Midway build product directory +├── node_modules # Node.js dependency package directory. +├── test +├── bootstrap.js # Deployment Startup File +├── package.json +└── tsconfig.json +``` + + + +### Alias path problem in build + +Aliases are a habit brought by front-end tools, rather than a standard capability of Node.js. Currently, there are two optional ways to use them: + +* 1. Use the [subpath imports](https://nodejs.org/dist/latest/docs/api/packages.html#subpath-imports) that comes with Node.js +* 2. Use [extra tools](/docs/faq/alias_path) to process at compile time + + + +### Packing compression + + +After the construction is completed, you can simply package and compress it and upload it to the environment to be released. + +:::caution + +Generally speaking, the files or directories that must be included in the server operation are `package.json`, `bootstrap.js`, `dist`, `node_modules`. + +::: + + + + +### Upload and decompress + + +There are many ways to upload to the server, such as the common `ssh/FTP/git` etc. You can also use online services such as [OSS](https://www.aliyun.com/product/oss) for transfer. + + +### Start the project + +The project built by Midway is single-process. Whether it adopts `fork` mode or `cluster` mode, single-process code is always easily compatible with different systems, so it is very easy to be loaded by existing tools such as pm2/forever in the community, + + +Here we use pm2 to demonstrate how to deploy. + + +Projects generally need an entry file, for example, we create a `bootstrap.js` in the root directory as our deployment file. +``` +➜ my_midway_app tree +. +├── src +├── dist # Midway build product directory +├── test +├── bootstrap.js # Deployment Startup File +├── package.json +└── tsconfig.json +``` + + +Midway provides a simple way to meet the startup method of different scenarios. All we need to do is install the `@midwayjs/bootstrap` module provided by us (by default, it comes with it). + +```bash +$ npm install @midwayjs/bootstrap --save +``` + +Then write the code in the entry file. Note that the code here uses `JavaScript`. + +```javascript +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap.run(); +``` + +Although the code of the startup file is very simple, we still need this file, which is needed in subsequent scenarios such as link tracking. + +Note that there is no http startup port here. If you need it, you can refer to the document for modification. + +- [Modify the koa port](extensions/koa# Modify Port) + +At this time, you can directly use `NODE_ENV = production node bootstrap.js` to start the code, or you can use pm2 to perform the startup. + +We generally recommend using tools to start the Node.js project. Here are some documents for advanced reading. + +- [Use documentation for pm2](extensions/pm2) +- [cfork documentation](extensions/cfork) + + + +### Startup parameters + +In most cases, it is not necessary to configure parameters in the Bootstrap, but there are still some configurable startup parameter options that are passed in through `configure` methods. + +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap + .configure({ + imports: [/*...*/] + }) + .run(); +``` + + + +| Property | Type | Description | +| -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| appDir | `string` | Optional. The project root directory is `process.cwd()` by default. | +| baseDir | `string` | Optional. The directory of the project code, which is `src` in R & D and `dist` in R & D. | +| imports | `Component[]` | Optional, explicit component reference | +| moduleDetector | `'file' \| IFileDetector \| false` | Optional. The module loading method used. Default value: `file`. You can use the dependency injection local file scanning method. You can explicitly specify a scanner or disable scanning. | +| logger | `boolean \| ILogger` | Optional. The logger used in the bootstrap. The default value is consoleLogger | +| ignore | `string[]` | Optional. The path ignored by the dependent injection container scan. The moduleDetector is invalid if false | +| globalConfig | `Array> \| Record` | Optionally, if the global incoming configuration is an object, it is directly merged into the current configuration in the form of an object. If you want to pass in the configuration of different environments, it is passed in in the form of an array with the same structure and `importConfigs`. | + + + +**Example: Enter the global configuration (object)** + +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap + .configure({ + globalConfig: { + customKey: 'abc' + } + }) + .run(); +``` + + + +**Example, incoming sub-environment configuration** + +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap + .configure({ + globalConfig: [{ + default: {/*...*/} + unittest: {/*...*/} + }] + }) + .run(); +``` + + + + + + +## Deploy with Docker + +### Write Dockerfile and build images + + +Step 1: Add a Dockerfile to the current directory + +```dockerfile +FROM node:18 + +WORKDIR /app + +ENV TZ="Asia/Shanghai" + +COPY . . + +# If each company has its own private source, it can replace the registry address +RUN npm install --registry=https://registry.npm.taobao.org + +RUN npm run build + +# If the port is changed, this side can be updated +EXPOSE 7001 + +CMD ["npm", "run", "online"] +``` + + +Step 2: Add `.dockerignore` file (similar to git ignore file), you can copy the content of `.gitignore` to `.dockerignore` + + +Step 3: When using pm2 deployment, change the command to `pm2-runtime start`. For more information about pm2 deployment, see [pm2 container deployment instructions](https://www.npmjs.com/package/pm2#container-support). + + +Step 4: build a docker image + +```bash +$ docker build -t helloworld. +``` + +step 5: run docker image + +```bash +$ docker run -itd -P helloworld +``` + +The operation effect is as follows: +![image.png](https://cdn.nlark.com/yuque/0/2020/png/187105/1608882492099-49160b6a-601c-4f08-ba65-b95a1335aedf.png) + +Then the uppercase `-P` allows us to access `32791` ports because we are assigned a port by default (this `-P` is randomly assigned, and we can also use the `-p 7001:7001` to specify a specific port) + +![image.png](https://cdn.nlark.com/yuque/0/2020/png/187105/1608882559686-031bcf0d-2185-42cd-a838-80f008777395.png) + +For other registry pushed to dockerhub or docker, you can search for the corresponding method. + + +**Optimization** + +We can see that the mirror image we typed in front has more than 1g, which can be optimized: +- 1. We can use a simpler basic image of docker image: for example, node:18-alpine, +- 2. The source code was finally typed in the mirror image. In fact, we don't need this one. + +We can also combine the multistage functions of docker to do some optimization. Please note that this function can only be used after `Docker 17.05`. + + +```dockerfile +FROM node:18 AS build + +WORKDIR /app + +COPY . . + +RUN npm install + +RUN npm run build + +FROM node:18-alpine + +WORKDIR /app + +# Copy the source code and the error can be reported to the right line +COPY --from=build /app/src ./src +COPY --from=build /app/dist ./dist +COPY --from=build /app/bootstrap.js ./ +COPY --from=build /app/package.json ./ + +RUN apk add --no-cache tzdata + +ENV TZ="Asia/Shanghai" + +RUN npm install --production + +# If the port is changed, this side can be updated +EXPOSE 7001 + +CMD ["npm", "run", "start"] +``` + +The result of the current example is only `207MB`. Compared with the original `1.26G`, it saves a lot of space. + +### Combined with Docker-Compose operation + +On the basis of docker deployment, you can also deploy some services related to your own services in combination with docker-compose. + +The following uses midway combined with redis as an example to quickly deploy the entire project using docker-compose. + +**Step 1** + +Add dockerfile according to Docker deployment + + +**Step 2** + +The `docker-compose.yml` file is added as follows: (here we simulate our midway project using Redis) + +```yaml +version: "3" +services: + web: + build: . + ports: + -"7001:7001" + links: + -redis + redis: + image: redis + +``` + +**Step 3: Modify config** + +Modify the configuration file of redis as follows: (To configure redis, please refer to [redis component](extensions/redis)) + +```javascript +// src/config/config.default.ts +export default { + // ... + redis: { + client: { + port: 6379, //The port of the redis container + host: "redis", // This is consistent with the redis service name in the docker-compose.yml file + password: "", //There is no password by default. Please change it to the password configured for the redis container. + db: 0, + }, + }, +} + +``` + + +**Step 4: Build** + +Use command: + +```bash +$ docker-compose build +``` + +**Step 5: Run** + +```bash +$ docker-compose up -d +``` + +![image.png](https://cdn.nlark.com/yuque/0/2020/png/187105/1608884158660-02bd2d3c-08b4-4ecc-a4dd-a18d4b9d2c12.png) + +**Followup** + +For more information about docker-compose, please refer to [Official Documentation](https://docs.docker.com/compose/) + + + +## Single file build deployment + +In some scenarios, the project is built as a single file, the deployed file can be smaller, and it can be distributed and deployed more easily. In some scenarios, it is particularly efficient, such as: + +- In serverless scenarios, a single file can be deployed faster +- For private scenarios, a single file can be encrypted and confused more easily + +Midway supports building projects as a single file starting from v3. + +Cases that are not supported are: + +- egg project (@midwayjs/web) +- The path form used by `importConfigs` at the entrance imports the configured application, component +- Packages that are not explicitly depended on, or that contain convention-based files + + + +### pre-dependency + +Single-file builds have some pre-dependencies that need to be installed. + +```bash +## Used to generate entry +$ npm i @midwayjs/bundle-helper --save-dev + +## Used to build a single file +## install to the global +$ npm i @vercel/ncc -g +## Or install to project (recommended) +$ npm i @vercel/ncc --save-dev +``` + + + +### Code adjustments + +There are some possible adjustments, listed below: + +#### 1. Configuration format adjustment + +The configuration imported by the project must be adjusted to [object mode](./env_config). + +Midway's official components have been adjusted to this mode. If you have your own components, please adjust to this mode to build a single file. + +:::tip + +Both Midway v2/v3 support configuration loading in "object mode". + +::: + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; + +import * as DefaultConfig from './config/config.default'; +import * as LocalConfig from './config/config.local'; + +@Configuration({ + importConfigs: [ + { + default: DefaultConfig, + local: LocalConfig + } + ] +}) +export class MainConfiguration { +} +``` + + + +#### 2. The default export situation + +Due to the default behavior of the ncc builder, please **DO NOT** use default exports in dependency injection related code. + +for example: + +```typescript +export default class UserSerivce { + //... +} +``` + +After compiling, `UserSerivce` cannot be injected. + + + +#### 3. Data source entities related + +Data source-dependent scan paths are also not supported. + +```typescript +export default { + typeorm: { + dataSource: { + default: { + //... + entities: [ + '/abc', // not supported + ] + }, + } +} +``` + +If there are too many entities, you can write a js file, scan out the entities, generate a file to the directory, and execute it every time you build. + + + +### Modify the entry file + +Modify the entry `bootstrap.js` to the following code. + +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); + +// Explicitly introduce user code as a component +Bootstrap. configure({ + // Here is the compiled entry, local development does not use this file + imports: require('./dist/index'), + // Disable directory scanning for dependency injection + moduleDetector: false, +}).run() + +``` + + + +### Construct + +Compilation for single-file builds requires several steps: + +- 1. Build the project ts file into js +- 2. Use an additional compiler to package all js files into one file + +We can write the above process as the following two commands, and put them in the `scripts` field of `package.json`. + +```json + "scripts": { + //... + "bundle": "bundle && npm run build && ncc build bootstrap.js -o build", + "bundle_start": "NODE_ENV=production node ./build/index.js" + }, +``` + +Contains three parts + +- `bundle` is to export all project codes as components and generate a `src/index.ts` file, this command is provided by `@midwayjs/bundle-helper` +- `npm run buid` is the basic ts project build, build `src/**/*.ts` to `dist/**/*.js` +- `ncc build bootstrap.js -o build` uses `bootstrap.js` as the entry to build a single file, and finally generates it into `build/index.js` + + + +After writing, execute the command. + +```bash +$ npm run bundle +``` + +:::tip + +Note that there may be errors during the construction process, such as ts definition errors, incorrect entry generation syntax, etc., which need to be repaired manually. + +::: + +After compiling, start the project. + +```bash +$ npm run bundle_start +``` + +If boot access is fine, then you can distribute your build in the build directory. + + + +## Binary deployment + +Package Node.js into a single executable file, which can be directly copied and executed during deployment. This method includes the node runtime and business code, which is conducive to the protection of intellectual property rights. + +Common tools for packaging Node.js into executable files include `pkg`, `nexe`, `node-packer`, `enclose`, etc. Below we will take the most common `pkg` package as an example. + + + +### pre-dependency + +Binary deployment has some pre-dependencies that need to be installed. + +```bash +## Used to generate entry +$ npm i @midwayjs/bundle-helper --save-dev + +## for building binaries +## install to the global +$ npm i pkg -g +## Or install to project (recommended) +$ npm i pkg --save-dev +``` + + + +### Code adjustments + +The adjustment is the same as [Single File Build Deployment](./deployment#Single File Build Deployment), please refer to the above document. + + + +### Modify the entry file + +The adjustment is the same as [Single File Build Deployment](./deployment#Single File Build Deployment), please refer to the above document. + + + +### Construct + +First you need to configure pkg, the main content is in the `bin` and `pkg` fields of `package.json`. + +- `bin` we specify as the entry file, ie `bootstrap.js` +- The directory after `pkg.scripts` is built, using glob syntax to include all js files under `dist` +- `pkg.asserts` If there are some static resource files, you can configure them here +- The platform product built by `pkg.targets` is a combination of the following options (in the example I specified mac + node18): + - **nodeRange** (node8), node10, node12, node14, node16 or latest + - **platform** alpine, linux, linuxstatic, win, macos, (freebsd) + - **arch** x64, arm64, (armv6, armv7) +- `pkg.outputPath` is the address of the build product, in order to separate it from the ts output, we chose the build directory + + + +`package.json` reference example: + +```json +{ + "name": "my-midway-project", + //... + "devDependencies": { + //... + "@midwayjs/bundle-helper": "^1.2.0", + "pkg": "^5.8.1" + }, + "scripts": { + //... + "pkg": "pkg . -d > build/pkg.log", + "bundle": "bundle && npm run build" + }, + "bin": "./bootstrap.js", + "pkg": { + "scripts": "dist/**/*.js", + "assets": [], + "targets": [ + "node18-macos-arm64" + ], + "outputPath": "build" + }, + //... +} + +``` + +For more details, please refer to [PKG Documentation](https://github.com/vercel/pkg). + +:::tip + +In the above example, the `-d` parameter of the pkg command is to output debugging information to a specific file, which can be deleted by yourself. + +::: + + + +Compilation for binary builds requires several steps: + +- 1. Generate the `src/index.ts` entry file, and build the project ts file into js +- 2. Use pkg to generate platform-specific build products + +We can execute orders. + +```bash +$ npm run bundle +$ npm run pkg +``` + +If it is correct, we can see a `my-midway-project` file in the `build` directory (the `name` field of our `package.json`), double click it to execute. + + + +## Deployment failure + +After deployment, the situation is more complicated because it is related to the environment. If you encounter problems after deployment to the server, see [Troubleshoot server startup failure](/docs/ops/ecs_start_err). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/env_config.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/env_config.md new file mode 100644 index 000000000000..f7d1f9837346 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/env_config.md @@ -0,0 +1,682 @@ +# Multi-environment configuration + +Configuration is a commonly used function, and different configuration information is often used in different environments. + +In this article, we will introduce how Midway loads business configurations for different environments. + + + +## Configuration file + +The simplest is to use the business configuration file capabilities provided by the framework. + +This capability is available across all business code and components throughout the Midway lifecycle. + +Configuration files can be exported in two formats, **object form** and **function form**. + +:::tip + +After our practice, **object form** will be simpler and more friendly, and can avoid many wrong usages. + +We will display it in this form in most documents. + +::: + + + +### Configuration file directory + +We can customize a directory and put the configuration file in it. + +For example, the `src/config` directory. + +``` +➜ my_midway_app tree +. +├── src +│ ├── config +│ │ ├── config.default.ts +│ │ ├── config.prod.ts +│ │ ├── config.unittest.ts +│ │ └── config.local.ts +│ ├── interface.ts +│ └── service +├── test +├── package.json +└── tsconfig.json +``` + +There are some specific conventions for configuration file names. + +`config.default.ts` is the default configuration file, and all environments will load this configuration file. + +For the rest of the file names, use `config.environment` as the file name. For the concept of specific environment, please see [Running Environment](environment). + +Configuration is not **required**, please add the environment configuration you need as appropriate. + + + + +### Object form + + +The configuration file export format is object, for example: + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + keys: '1639994056460_8009', + koa: { + port: 7001, + }, +} as MidwayConfig; +``` + + + +### Function form + + +The configuration file is a function with `appInfo` parameters. This function will be automatically executed when the framework is initialized, and the return value will be merged into the complete configuration object. + +```typescript +// src/config/config.default.ts +import { MidwayAppInfo, MidwayConfig } from '@midwayjs/core'; + +export default (appInfo: MidwayAppInfo): MidwayConfig => { + return { + keys: '1639994056460_8009', + koa: { + port: 7001, + }, + view: { + root: path.join(appInfo.appDir, 'view'), + }, + }; +} + +``` + +The parameter of this function is `MidwayAppInfo` type, and the value is as follows. + + +| **appInfo** | **Description** | +| ----------- | ------------------------------------------------------------ | +| pkg | package.json | +| name | application name, same as pkg.name | +| baseDir | src (local development) or dist (online) directory of the application code | +| appDir | directory for application code | +| HOME | user directory, such as /home/admin for the admin account | +| root | Application root directory, baseDir only in local and unittest environments, and HOME in others. | + + + +### Configuration file definition + +Midway provides `MidwayConfig` as a unified configuration item definition, and all components will merge their definitions into this configuration item definition. Whenever a component is enabled (`imports` in `configuration.ts`), `MidwayConfig` will automatically include the configuration definition for that component. + +For this reason, please use the format recommended by the documentation as much as possible to achieve the best usage effect. + +Whenever a new component is enabled, the configuration definition will automatically add the configuration items of the component. Through this behavior, you can also check whether a certain component is enabled in disguise. + +For example, we enable the effect of the view component. + +![](https://img.alicdn.com/imgextra/i2/O1CN013sHGlA1o3uQ4Pg0nO_!!6000000005170-2-tps-1416-572.png) + +:::tip + +Why use objects instead of plain key exports? + +1. If the user does not understand the configuration items, he still needs to check the document to understand the meaning of each item. Except for the first level of prompts, the subsequent levels of prompts do not have obvious efficiency improvements. + +2. The form of key export has no advantage in displaying under an overly deep structure + +3. The key export may be repeated, but there will be no warnings or errors at the code level, which is difficult to troubleshoot. The object form is more friendly + +::: + + + +### Load configuration file in object form + + +The framework provides the function of loading configuration files for different environments, which needs to be enabled in the `src/configuration.ts` file. + + +There are two ways to load configuration, **object format** and **specified directory format** loading. + +Starting from Midway v3, we will use **object form** as the main configuration loading form. + +In scenarios such as single-file construction and ESM, only this standard module loading method is supported to load configurations. + +The configuration file of each environment **must explicitly specify to add**, and subsequent frameworks will be merged according to the actual environment. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; + +import * as DefaultConfig from './config/config.default'; +import * as LocalConfig from './config/config.local'; + +@Configuration({ + importConfigs: [ + { + default: DefaultConfig, + local: LocalConfig + } + ] +}) +export class MainConfiguration { +} +``` + +Configuration objects are passed in the array in `importConfigs`. The key of each object is the environment, and the value is the configuration value corresponding to the environment. Midway will load the corresponding configuration according to the environment during startup. + + + +### Specify directory and file loading configuration + +Specify a directory to load, and all `config.*.ts` in the directory will be scanned and loaded. + +ESM, single-file deployment, etc. do not support directory configuration loading. + +:::info +`importConfigs` here just specify the files that need to be loaded, and the actual runtime will **automatically select the current environment** to find the corresponding file suffix. +::: + + +The rules for the configuration file are: + + +- 1. You can specify a directory, the traditional `src/config` directory is recommended, or you can specify a file +- 2. The file specification does not require the ts suffix +- 3. Configuration file **Must be explicitly specified to add** + + + +**Example: specify directory** + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; + +@Configuration({ + importConfigs: [ + join(__dirname, './config/'), + ] +}) +export class MainConfiguration { +} +``` + + +**Example: specifying a specific file** + + +When manually specifying a batch of files, if the files do not exist at this time, an error will be reported. + + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; + +@Configuration({ + importConfigs: [ + join(__dirname, './config/config.default'), + join(__dirname, './config/config.local'), + join(__dirname, './config/custom.local') // You can use custom naming, as long as the middle part has an environment + ] +}) +export class MainConfiguration { +} +``` + + +You can also use the configuration outside the project, but please use the absolute path and `*.js` suffix. + + +For example, the directory structure is as follows (note the `customConfig.default.js` file): + +``` + base-app + ├── package.json + ├── customConfig.default.js + └── src + ├── configuration.ts + └── config + └── config.default.ts +``` + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; + +@Configuration({ + importConfigs: [ + join(__dirname, './config/'), + join(__dirname, '../customConfig.default'), + ] +}) +export class MainConfiguration { +} +``` + + + + + +### Configure loading order + + +There is a priority for configuration (Application Code > Component), and the priority will be higher relative to this running environment. + + +For example, the loading sequence for loading a configuration in the prod environment is as follows. The later loaded configuration will overwrite the previous configuration with the same name. + + +```typescript +-> Component config.default.ts +-> Apply config.default.ts +-> component config.prod.ts +-> apply config.prod.ts +``` + + + +### Configure merge rules + + +By default, the `**/config.defaut.ts` file and the `**/config.{environment}.ts` file will be loaded. + +For example, the following code will search for `config.default.*` and `config.local.*` files in the `local` environment. If it is in other environments, it will only search for `config.default.*` and `config.{Current environment}.*`, if the file does not exist, it will not be loaded and no error will be reported. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; + +@Configuration({ + importConfigs: [ + join(__dirname, './config/'), + ] +}) +export class MainConfiguration { +} +``` + + +For forward compatibility, we have done some processing on the configuration reading of some special environments. The value of the environment here refers to the [result](environment#AxjGQ) based on the values of `NODE_ENV` and `MIDWAY_SERVER_ENV`. + +| **Environment value** | **Configuration file read** | +| --------------------- | ------------------------------------------ | +| prod | *.default.ts + *.prod.ts | +| production | *.default.ts + *.production.ts + *.prod.ts | +| unittest | *.default.ts + *.unittest.ts | +| test | *.default.ts + *.test.ts + *.unittest.ts | + +Except for the above table, the rest are the values of `*.default.ts + *.{current environment}.ts`. + + +In addition, the configuration is merged using the [extend2](https://github.com/eggjs/extend2) module for deep copying, and the [extend2](https://github.com/eggjs/extend2) fork from [extend](https ://github.com/justmoon/node-extend), there will be differences when handling arrays. + +```javascript +const a = { + arr: [ 1, 2 ], +}; +const b = { + arr: [ 3 ], +}; +extend(true, a, b); +// => { arr: [ 3 ] } +``` + +According to the example above, the framework directly overwrites the array instead of doing the merge. + + + +## get configuration + + +Midway will save the configuration in the internal configuration service. The entire structure is an object, which is injected using the `@Config` decorator when used by Midway business code. + + + +### Single configuration value + + +By default, it will be obtained from the configuration object according to the string parameter value of the decorator. + + +```typescript +import { Config } from '@midwayjs/core'; + +export class IndexHandler { + + @Config('userService') + userConfig; + + async handler() { + console.log(this.userConfig); // { appname: 'test'} + } +} +``` + + + +### Deep level configuration values + + +If the value of the configuration object is deep in the object, it can be obtained in a cascaded manner. + + +For example, the data source is: + + +```json +{ + "userService": { + "appname": { + "test": { + "data": "xxx" + } + } + } +} +``` + +You can write complex acquisition expressions to acquire values, examples are as follows. + +```typescript +import { Config } from '@midwayjs/core'; + +export class IndexHandler { + + @Config('userService.appname.test.data') + data; + + async handler() { + console.log(this.data); // xxx + } +} + +``` + + + +### The entire configuration object + + +You can also get the entire configuration object through the special attribute `ALL`. + +```typescript +import { Config, ALL } from '@midwayjs/core'; + +export class IndexHandler { + + @Config(ALL) + allConfig; + + async handler() { + console.log(this.allConfig); // { userService: { appname: 'test'}} + } +} +``` + + + +## Change setting + +During the coding process, we have some places where the configuration can be dynamically modified for use in different scenarios. + + + +### Modification during life cycle + + +midway has added an asynchronous configuration loading life cycle, which can be executed after the configuration is loaded. + +```typescript +// src/configuration.ts +import { Configuration, IMidwayContainer } from '@midwayjs/core'; +import { join } from 'path'; +import { RemoteConfigService } from '../service/remote'; // Customized access to remote configuration service + +@Configuration({ + importConfigs: [ + join(__dirname, './config/'), + ] +}) +export class MainConfiguration { + + async onConfigLoad(container: IMidwayContainer) { + // Here you can modify the global configuration + const remoteConfigService = await container. getAsync(RemoteConfigService); + const remoteConfig = await remoteConfigService.getData(); + + // The return value here will be merged with the global config + // const remoteConfig = { + // typeorm: { + // dataSource: { + // default: { + // type: "mysql", + // host: "localhost", + // port: 3306, + // username: "root", + // password: "123456", + // database: "admin", + // synchronize: false, + // logging: false, + // entities: "/**/**.entity.ts", + // dateStrings: true + // } + // } + // } + // } + + return remoteConfig; + } +} +``` + +:::caution + +The `onConfigLoad` lifecycle is executed after the egg plugin (if any) is initialized and cannot be used to override the configuration of the egg plugin. + +::: + + + +### Modify at startup + +You can add configuration using Bootstrap's `configure` method before starting the code. + +The `configure` method can pass a `globalConfig` attribute, which can pass a global configuration before the application starts. + +If you pass an array, you can differentiate between environments. + +```typescript +// bootstrap.js +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap + .configure({ + globalConfig: [ + { + default: { + abc: '123' + }, + unittest: { + abc: '321' + } + } + ] + }) + .run(); + +// in unittest, app.getConfig('abc') => '321' +``` + +If an object is passed, it is overridden directly. + +```typescript +// bootstrap.js +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap + .configure({ + globalConfig: { + abc: 'text' + } + }) + .run(); + +// app.getConfig('abc') => 'text' +``` + + + +### Modify using API + + +To modify the configuration in other scenarios, you can use the [API](./built_in_service#midwayconfigservice) provided by midway. + + + +## Environment variables and configuration + + +There are some libraries in the community, such as `dotenv`, which can load `.env` files and inject them into the environment, thereby placing some keys in the environment, which can be directly relied on in Midway. + +```bash +$ npm i dotenv --save +``` + +You can add a `.env` file in the project root directory, such as the following: + +``` +OSS_SECRET=12345 +OSS_ACCESSKEY=54321 +``` + +We can initialize it in the entry, such as `bootstrap.js` or `configuration`. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as dotenv from 'dotenv'; + +// load .env file in process.cwd +dotenv.config(); + +@Configuration({ + //... +}) +export class MainConfiguration { + async onReady(container) { + + } +} + +``` + + +We can use it in the environment configuration. + +```typescript +// src/config/config.default + +export const oss = { + accessKey: process.env.OSS_ACCESSKEY, // 54321 + secret: process.env.OSS_SECRET // 12345 +} +``` + + + +## Common mistakes + + +There are many possibilities for the configuration not taking effect. The troubleshooting ideas are as follows: + +- 1. Check whether the files or directories related to `importConfigs` are explicitly configured in the configuration file. +- 2. Check whether the application startup environment is consistent with the configuration file. For example, the prod configuration will definitely not appear in local. +- 3. Check whether ordinary export and method callback export are mixed use, such as the following mixed use situation + + + +### 1. Obtain the value injected by @Config in the constructor + +**Please don’t **get the properties injected by `@Config()` in the constructor, which will make the result undefined. The reason is that the attributes injected by the decorator will not be assigned until the instance is created (new). In this case, use the `@Init` decorator. + +```typescript +@Provide() +export class UserService { + + @Config('redisConfig') + redisConfig; + + constructor() { + console.log(this.redisConfig); // undefined + } + + @Init() + async initMethod() { + console.log(this.redisConfig); // has value + } + +} +``` + + + +### 2. Mix callback and export writing methods + +**The following is incorrect usage. ** + +```typescript +export default (appInfo) => { + const config = {}; + + // xxx + return config; +}; + +export const keys = '12345'; +``` + +Values defined with `export const` are ignored. + + + +### 3. Mix export default and export const + +**The following is incorrect usage. ** + +```typescript +export default { + keys: '12345', +} + +export const anotherKey = '54321'; +``` + +Configurations located later will be ignored. + +### 4. Export= mixed with others + +When `export=` is mixed, if there are other configurations later, the value of `export=` will be ignored. + +```typescript +export = { + a: 1 +} +export const b = 2; +``` + +Compiled result: + +```typescript +export const b = 2; +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/environment.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/environment.md new file mode 100644 index 000000000000..4cb781c2e175 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/environment.md @@ -0,0 +1,79 @@ +# Operating environment + +Node.js applications generally obtain environment variables through `NODE_ENV` to meet different needs in different environments. For example, in `production` environment, cache is turned on to optimize performance, while in `development` environment, all log switches are turned on to output detailed error messages, etc. + + + +## Specify the operating environment + + +Since `NODE_ENV` will be intercepted and injected by some toolkits in some cases, under Midway system, we will acquire the environment first according to the `MIDWAY_SERVER_ENV`, while `NODE_ENV` will acquire as the second priority. + + +We can specify it by adding environment variables at startup. + +```bash +MIDWAY_SERVER_ENV=prod npm start // first priority +NODE_ENV=local npm start // second priority +``` +In windows, you must use the [cross-env](null) module to achieve the same effect. +```bash +cross-env MIDWAY_SERVER_ENV=prod npm start // first priority +cross-env NODE_ENV=local npm start // second priority +``` + + + +## Get the environment in the code + + +Midway provides the `getEnv()` method to obtain the environment for app objects. Midway handles different upper-level frameworks to ensure that the `getEnv()` method is available in different scenarios. . + + +```typescript +import { Application } from '@midwayjs/koa'; + +// process.env.MIDWAY_SERVER_ENV=prod + +@Provide() +export class UserService { + + @App() + app: Application; + + async invoke() { + console.log(this.app.getEnv()); // prod + } +} +``` + + +If neither the `NODE_ENV` nor the `MIDWAY_SERVER_ENV` is assigned, the return value of the method is `prod` by default. + +:::info +Note that you cannot get the environment directly through `NODE_ENV` and `MIDWAY_SERVER_ENV`, both values may be empty, and Midway will not set it in reverse. To obtain the environment, please obtain the API method provided by other frameworks through app.getEnv(). +::: + + + +## Common environmental variable values + +In general, each company has its own environmental variable values, and here are some common environmental variable values and their corresponding descriptions. + +| Value | Description | +| --- | --- | +| local | Local development environment | +| dev/daily/development | Daily development environment | +| pre/prepub | Pre-production environment | +| prod/production | Production environment | +| test/unittest | Unit test environment | +| benchmark | Performance test environment | + + + +## Dependent on the acquisition environment from the injection container + + +In the process of dependent injection container initialization, Midway initializes a `EnvironmentService` service to parse the environment by default and maintains the service object throughout the life cycle. + +For more information, see [Environment Services](./built_in_service#midwayenvironmentservice). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/error_code.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/error_code.md new file mode 100644 index 000000000000..b6b2acaa0891 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/error_code.md @@ -0,0 +1,289 @@ +# Framework Error Code + +The following are the errors built into the framework, which will increase over time. + +| Error code | Error name | Error description | +| ------------ | ------------------------------------- | ---------------------------- | +| MIDWAY_10000 | Occupying use | Unknown error | +| MIDWAY_10001 | MidwayCommonError | Unclassified errors | +| MIDWAY_10002 | MidwayParameterError | Parameter type error | +| MIDWAY_10003 | MidwayDefinitionNotFoundError | Dependency injection definition not found | +| MIDWAY_10004 | MidwayFeatureNoLongerSupportedError | Features are no longer supported | +| MIDWAY_10005 | MidwayFeatureNotImplementedError | Function not implemented | +| MIDWAY_10006 | MidwayConfigMissingError | Configuration item missing | +| MIDWAY_10007 | MidwayResolverMissingError | Dependency injection attribute resovler not found | +| MIDWAY_10008 | MidwayDuplicateRouteError | Duplicate route | +| MIDWAY_10009 | MidwayUseWrongMethodError | The wrong method was used | +| MIDWAY_10010 | MidwaySingletonInjectRequestError | Scope confusion | +| MIDWAY_10011 | MidwayMissingImportComponentError | Component not imported | +| MIDWAY_10012 | MidwayUtilHttpClientTimeoutError | http client call timed out | +| MIDWAY_10013 | MidwayInconsistentVersionError | Incorrect dependency version used | +| MIDWAY_10014 | MidwayInvalidConfigError | Invalid configuration | +| MIDWAY_10015 | MidwayDuplicateClassNameError | Duplicate class name | +| MIDWAY_10016 | MidwayDuplicateControllerOptionsError | Repeated controller parameters | + + + +## MIDWAY_10001 + +**Problem Description** + +The most common framework error is thrown without classification, and the details of the error are generally written into the error message. + +**Solution** + +The error message shall prevail. + + + +## MIDWAY_10002 + +**Problem Description** + +The parameter of the method is passed in error, the type may be wrong or the parameter format is wrong. + +**Solution** + +Reference method definitions or document incoming parameters. + + + +## MIDWAY_10003 + +**Problem Description** + +Generally, when a class is started or dynamically retrieved from the container, if the class is not registered in the container, an error `xxx is not valid in current context` will be reported. + +**Solution** + +Possible situations, such as in business code or component use: + +```typescript +// ... + +export class UserService {} + +// ... +@Controller() +export class HomeController { + @Inject() + userService: UserService; +} +``` + +The above error occurs if the `UserService` does not write the `@Provide` or implicitly contains the `@Provide` decorator. + +The general error report is similar to the following. + +``` +userService in class HomeController is not valid in current context +``` + +So, it means that the `userService` property in `HomeController` is not found in the container, you can follow this clue to troubleshoot. + + + +## MIDWAY_10004 + +**Problem Description** + +Abandoned functions used. + +**Solution** + +This function is not used. + + + +## MIDWAY_10005 + +**Problem Description** + +The method or function used has not been implemented for the time being. + +**Solution** + +This function is not used. + + + +## MIDWAY_10006 + +**Problem Description** + +The required configuration items were not provided. + +**Solution** + +Check whether the environment corresponding to the configuration contains the configuration. If not, add the configuration to the configuration file. + + + +## MIDWAY_10007 + +**Problem Description** + +The resolution type for container injection was not found. This error does not occur in the current version. + +**Solution** + +None. + + + +## MIDWAY_10008 + +**Problem Description** + +Check for duplicate routes. + +**Solution** + +Remove duplicate routing parts. + + + +## MIDWAY_10009 + +**Problem Description** + +The wrong method was used. + +**Solution** + +When you include an asynchronous call in the synchronous get method, you will be prompted to use the `getAsync` method and modify it. + + + +## MIDWAY_10010 + +**Problem Description** + +This error occurs when an unexplicitly declared request scope instance is injected into a single instance. The reason for the error is [Scope Degradation](./container# scope downgrade). + +For example, the following code will throw this error: + +```typescript +// ... +@Provide() +export class UserService {} + +// ... +@Provide() +@Scope(ScopeEnum.Singleton) +export class LoginService { + @Inject() + userService: UserService; +} +``` + +This problem often occurs in `configuration` or middleware files. + +This error is to avoid the risk of automatic domain degradation and caching instance data. + +**Solution** + +- 1. If you injected the request scope instance into the singleton by mistake, please modify the request scope code to a singleton. +- 2. If you want to inject the request scope to use in a singleton and can clearly know the consequences of scope degradation (cached), please explicitly declare the scope option on the class (indicating that degradation is allowed). + +```typescript +@Provide() +@Scope(ScopeEnum.Request, { allowDowngrade: true }) +export class UserService {} +``` + + + +## MIDWAY_10011 + +**Problem Description** + +This error occurs when the component is not `imports` in the `configuration` file and the class in the component is used. + +**Solution** + +Explicitly introduces components in `imports` parts of `src/configuration`. + + + +## MIDWAY_10012 + +**Problem Description** + +The built-in Http Client timeout throws this error. + +**Solution** + +Normal timeout error, check why timeout, do a good job of error handling. + + + +## MIDWAY_10013 + +**Problem Description** + +This error is thrown when the installed component and the frame version do not match. + +It usually appears after the new version of the framework is released, when the project opens the lock file, uses the old version of the framework, and installs a new component. + +**Solution** + +Delete the lock file and reinstall the dependency. + + + +## MIDWAY_10014 + +**Problem Description** + +This error is thrown when the `export default` and `export const` export methods exist in the configuration file. + +**Solution** + +Do not mix the two export methods. + + + +## MIDWAY_10015 + +**Problem Description** + +When duplicate class name checking is started (conflictCheck), the error will be thrown if the same class name is found in the dependency injection container during code scanning. + +```typescript +// src/configuration.ts +@Configuration({ + // ... + conflictCheck: true +}) +export class MainConfiguration { + // ... +} +``` + +**Solution** + +Modify the class name or turn off duplicate class name checking. + + + +## MIDWAY_10016 + +**Problem Description** + +When different controllers are added, the same `prefix` is used, and different `options`, such as middleware, are added, the error will be thrown. + +**Solution** + +Merge the controller codes of the same `prefix` or remove all `options`. + + + + + + + + + + + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/error_filter.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/error_filter.md new file mode 100644 index 000000000000..0a23eb312a0f --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/error_filter.md @@ -0,0 +1,322 @@ +# Exception handling + +Midway provides a built-in exception handler that handles all unhandled exceptions in the application. When your application code throws an exception handler, the handler catches the exception and waits for the user to handle it. + +The execution position of the exception handler is behind the middleware, so it can intercept all errors thrown by the middleware and business. + +![err_filter](https://img.alicdn.com/imgextra/i2/O1CN013pvSjT1nWvsLRE4vo_!!6000000005098-2-tps-2000-524.png) + + + +## Http exception + +In Http requests, Midway provides a common `MidwayHttpError` type of exception, which inherits from standard `MidwayError`. + +```typescript +export class MidwayHttpError extends MidwayError { + // ... +} +``` + +We can throw this error during the request. Since the error contains a status code, the Http program will automatically return the status code. + +For example, the following code throws an error containing the 400 status code. + +```typescript +import { MidwayHttpError } from '@midwayjs/core'; + +// ... + +async findAll() { + throw new MidwayHttpError('my custom error', HttpStatus.BAD_REQUEST); +} + +// got status: 400 +``` + +However, we seldom do this in general. Most business errors are reused and error messages are basically fixed. In order to reduce duplicate definitions, we can customize some exception types. + +For example, to customize an Http exception with a status code of 400, you can define an error as follows. + +```typescript +// src/error/custom.error.ts +import { HttpStatus } from '@midwayjs/core'; + +export class CustomHttpError extends MidwayHttpError { + constructor() { + super('my custom error', HttpStatus.BAD_REQUEST); + } +} +``` + +Then throw the use in the business. + +```typescript +import { CustomHttpError } from './error/custom.error'; + +// ... + +async findAll() { + throw new CustomHttpError(); +} +``` + + + +## Exception handler + +The built-in exception handler is used in standard request response scenarios and can catch errors thrown in all requests. + +You can use the `@Catch` decorator to define a specific type of exception handler. You can easily catch a specific type of error and handle it. You can also catch a global error and return a unified format. + +At the same time, the framework also provides some default Http errors, which are placed under the `httpError` object. + +For example, capture `InternalServerErrorError` errors thrown. + +We can place this type of exception handler in the `filter` directory, such as `src/filter/internal.filter.ts`. + +```typescript +// src/filter/internal.filter.ts +import { Catch, httpError, MidwayHttpError } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Catch(httpError.InternalServerErrorError) +export class InternalServerErrorFilter { + async catch(err: MidwayHttpError, ctx: Context) { + + // ... + return 'got 500 error, '+ err.message; + } +} +``` + +The parameters of the `catch` method are the current error and the context `Context` to which the exception handler is currently applied. We can simply return the response data. + +If you do not write parameters, all errors will be captured, whether HttpError or not, and only errors thrown in the request will be captured here. + +```typescript +// src/filter/all.filter.ts +import { Catch } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Catch() +export class AllErrorFilter { + async catch(err: Error, ctx: Context) { + // ... + } +} +``` + +The defined exception handler is just a piece of common code, and we also need to apply it to the app of one of our frameworks, such as the app of http protocol. + +We can apply the error handling filter in `src/configuration.ts`. Since the parameters can be arrays, we can apply multiple error processors. + +```typescript +// src/configuration.ts +import { Configuration, App, Catch } from '@midwayjs/core'; +import { join } from 'path'; +import * as koa from '@midwayjs/koa'; +import { InternalServerErrorFilter } from './filter/internal.filter'; + +@Configuration({ + imports: [ + koa + ], +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + this.app.useFilter([InternalServerErrorFilter]); + } +} + +``` + +:::info + +Note that some non-Midway middleware or status codes set inside the framework cannot be intercepted because they do not use the form of error throwing. If more than 400 states are returned in the business, please use the standard form of error throwing as much as possible to facilitate the interceptor to handle. + +::: + + + +### 404 processing + +Inside the framework, if there is no match to the route, a `NotFoundError` exception will be thrown. With the exception handler, we can customize its behavior. + +For example, jump to a page, or return a specific result: + +```typescript +// src/filter/notfound.filter.ts +import { Catch, httpError, MidwayHttpError } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Catch(httpError.NotFoundError) +export class NotFoundFilter { + async catch(err: MidwayHttpError, ctx: Context) { + // 404 error will come here + ctx.redirect('/404.html'); + + // or directly return a content + return { + message: '404, '+ ctx.path + } + } +} +``` + + + +### 500 processing + +When decorator parameters are not passed, all errors will be captured. + +For example, capture all errors and return a specific JSON structure, as shown in the following example. + +```typescript +// src/filter/default.filter.ts + +import { Catch } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Catch() +export class DefaultErrorFilter { + async catch(err: Error, ctx: Context) { + + // ... + return { + status: err.status ?? 500, + message: err.message; + } + } + +} +``` + +We can apply the error handling filter in `src/configuration.ts`. Since the parameters can be arrays, we can apply multiple error processors. + +```typescript +import { Configuration, App, Catch } from '@midwayjs/core'; +import { join } from 'path'; +import * as koa from '@midwayjs/koa'; +import { DefaultErrorFilter } from './filter/default.filter'; +import { NotFoundFilter } from './filter/notfound.filter'; + +@Configuration({ + imports: [ + koa + ], +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + this.app.useFilter([NotFoundFilter, DefaultErrorFilter]); + } +} + +``` + +There is no need to consider the order when using an exception handler. The general error handler must be matched at last, and there can only be one general error handler on an app. + + + +## Match with Prototype + +By default, exceptions only make absolute matches. + +Sometimes we need to capture all derived classes, this time we need additional settings. + +```typescript +import { Catch, MidwayError } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +class CustomError extends MidwayError {} + +class CustomError2 extends MidwayError {} + +// All subclasses will be captured here +@Catch([MidwayError], { + matchPrototype: true +}) +class TestFilter { + catch(err, ctx) { + // ... + } +} +``` + +The configuration `matchPrototype` can match all derived classes. + + + +## Exception log + +Midway has built-in default exception handling behavior. + +If the exception handler is **not matched**, it will be blocked and recorded by the exception middleware. + +Conversely, if the exception handler is customized, the error will be regarded as normal business logic. Please note that the exception thrown by the bottom layer will be used as the normal processing logic of the business at this time, and **will not** be treated as normal business logic by logging. + +You can print the log in the exception handler yourself. + +```typescript +import { Catch } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Catch() +export class DefaultErrorFilter { + async catch(err: Error, ctx: Context) { + + // ... + ctx.logger.error(err); + // ... + return 'got 500 error, '+ err.message; + } +} +``` + + + +## Built-in Http exception + +The Http exceptions built into the following frameworks can be found and used in `@midwayjs/core`. Each exception already contains the default error message and status code. + +- `BadRequestError` +- `UnauthorizedError` +- `NotFoundError` +- `ForbiddenError` +- `NotAcceptableError` +- `RequestTimeoutError` +- `ConflictError` +- `GoneError` +- `PayloadTooLargeError` +- `UnsupportedMediaTypeError` +- `UnprocessableEntityError` +- `InternalServerErrorError` +- `NotImplementedError` +- `BadGatewayError` +- `ServiceUnavailableError` +- `GatewayTimeoutError` + +For example: + +```typescript +import { httpError } from '@midwayjs/core'; + +// ... + +async findAll() { + // something wrong + throw new httpError.InternalServerErrorError(); +} + +// got status: 500 + +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/esm.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/esm.md new file mode 100644 index 000000000000..bc064f53a668 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/esm.md @@ -0,0 +1,151 @@ +# ESModule usage guide + +For the past few years, Node.js has been working on supporting running ECMAScript modules (ESM). This is a difficult feature to support because the foundation of the Node.js ecosystem is built on a different module system called CommonJS (CJS). + +The interoperability between the two modular systems poses great challenges and has many functional differences. + +Since Node.js v16, the support of ESM has been relatively stable, and some matching functions of TypeScript have also been implemented one after another. + +On this basis, Midway supports file loading in ESM format, and businesses can also use this new module loading method to build their own business. + +:::caution + +It is not recommended for users to use it without understanding ESM. + +::: + +Recommended reading: + +* [TypeScript Official ESM Guide](https://www.typescriptlang.org/docs/handbook/esm-node.html) +* [Node.js official ESM documentation](https://nodejs.org/api/esm.html) + + + +## Scaffolding + +Due to many changes, Midway provides a brand-new scaffolding in ESM format. If there is a need for ESM, we recommend that users re-create it before developing business. + +```bash +$ npm init midway@latest -y +``` + +Select the scaffolding in the esm group. + + + +## Differences from CJS projects + +### 1. Changes in package.json + + type in `package.json` must be set to `module`. + +```json +{ + "name": "my-package", + "type": "module", + //... + "dependencies": { + } +} +``` + + + +### 2. Changes in tsconfig.json + +`compilerOptions` compile-related options need to be set to `Node16` or `NodeNext`. + +```json +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Node16", + "esModuleInterop": true, + //... + } +} +``` + + + +### 3. Changes in the tool chain + +Since the original development toolchain only supports CJS code, and some modules in the community do not support ESM, Midway uses a new toolchain in ESM mode. + +* Development command, use mwtsc (only do tsc necessary package) +* Test and coverage commands, using mocha + ts-node, both test code and test configuration have been adjusted +* build command, use tsc + +Some features that are no longer supported + +* alias path, please use [subpath export](https://nodejs.org/api/packages.html#subpath-exports) that comes with Node.js instead +* Copy non-js files when building, put non-code files outside src, or add custom commands when building + +For specific differences, please refer to [Scaffolding](https://github.com/midwayjs/midway-boilerplate/blob/master/v3/midway-framework-koa-esm/boilerplate/_package.json) to check. + + + +### 4. Some code differences + +Here's a quick list of some of the differences between ESM and CJS in development. + + + +1. In ts, the import file must specify a suffix name, and the suffix name is js. + +```typescript +import { helper } from "./foo.js"; // works in ESM & CJS +``` + + + +2. You can no longer use `module.exports` or `exports.` to export. + +```typescript +// ./foo.ts +export function helper() { + //... +} +// ./bar.ts +import { helper } from "./foo"; // only works in CJS +``` + + + +3. You cannot use `require` in your code + +Only the `import` keyword can be used. + + + +4. You cannot use `__dirname`, `__filename`, etc. and path-related keywords in the code + +```typescript +// ESM solution +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(fileURLToPath(import.meta.url)) +``` + +Therefore, the configuration part must use the object mode. + +```typescript +import { Configuration } from '@midwayjs/core'; +import DefulatConfig from './config/config.default.js'; +import UnittestConfig from './config/config.unittest.js'; + +@Configuration({ + importConfigs: [ + { + default: DefulatConfig, + unittest: UnittestConfig, + }, + ], +}) +export class MainConfiguration { + //... +} +``` \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/alinode.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/alinode.md new file mode 100644 index 000000000000..29f8ae0326cb --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/alinode.md @@ -0,0 +1,83 @@ +# Alinode + +## Preparation + +The application that needs to be accessed is to be deployed in an independent service acquisition cloud environment and can access Internet services. + +## create service + +**first step** + +Log in to Alibaba Cloud and click to activate the service of [Alibaba Cloud's Node.js Performance Platform](https://www.aliyun.com/product/nodejs). + +**Second step** + +Create a new app, get the App ID and App Secret. + + +## Install monitoring dependencies + +**first step** + +Install the components required by the Node.js Performance Platform + +```bash +# Install the version management tool tnvm, please refer to the installation process error: https://github.com/aliyun-node/tnvm +$ wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash +$ source ~/.bashrc + +# tnvm ls-remote alinode # View the required version +$ tnvm install alinode-v6.5.0 # Install the required version +$ tnvm use alinode-v6.5.0 # use the required version + +$ npm install @alicloud/agenthub -g # install agenthub +```` + +There are three parts + +- 1. Install tnvm (alinode source) +- 2. Use tnvm to install alinode (replace the default node) +- 3. Install the data collector required by alinode + +After the installation is complete, you can check it, you need to make sure that `.tnvm` is included in the path of `which node` and `which agenthub`. + +```bash +$ which node +/root/.tnvm/versions/alinode/v3.11.4/bin/node + +$ which agenthub +/root/.tnvm/versions/alinode/v3.11.4/bin/agenthub +```` + +Save the `App ID` and `App Secret` obtained in `Create a new application` as `yourconfig.json` as shown below. For example, in the project root directory. + +```typescript +{ + "appid": "****", + "secret": "****", +} +```` + +Start the plugin: + +```typescript +agenthub start yourconfig.json +```` + +## start node service + +In the installed server, when starting the Node service, you need to add the ENABLE_NODE_LOG=YES environment variable. + +for example: + +```bash +$ NODE_ENV=production ENABLE_NODE_LOG=YES node bootstrap.js +```` + +## Docker container approach + +For the method of docker container, please refer to [document](https://help.aliyun.com/document_detail/66027.html?spm=a2c4g.11186623.6.580.261ba70feI6mWt). + +## other + +For more information, please refer to the [documentation](https://help.aliyun.com/document_detail/60338.html?spm=a2c4g.11186623.6.548.599312e6IkGO9v) of the Alibaba Cloud Node.js Performance Platform. \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/axios.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/axios.md new file mode 100644 index 000000000000..8efb0dca72df --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/axios.md @@ -0,0 +1,432 @@ +# HTTP request + + + +## Simple HTTP request + +Midway has a built-in simple HTTP request client, which can be used without introducing a third-party package. + +The default Get request, and the returned data is Buffer. + +The built-in Http client only provides the simplest capabilities and can only meet most of the front-end interface data acquisition needs. If you need complex functions, such as file upload, please use other clients, such as fetch, axios, got, etc. + + + +### Simple method form + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +const result = await makeHttpRequest('http://127.1:7001/'); + +// Buffer.isBuffer(result.data) => true +``` + +Get request, with Query, the return type is JSON. + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +const result = await makeHttpRequest('http://127.1:7001/', { + data: { + a: 1 + B: 2 + }, + dataType: 'json', // returned data format +}); + +// typeof result.data => 'object' +// result.data.url => /?a=1&b=2 +``` + +You can specify the type + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +const result = await makeHttpRequest('http://127.1:7001/', { + method: 'GET', + dataType: 'json', +}); +``` + +Returns the text format. + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +const result = await makeHttpRequest('http://127.1:7001/', { + method: 'GET', + dataType: 'text', +}); +``` + +POST requests and returns JSON. + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +const result = await makeHttpRequest('http://127.1:7001/', { + method: 'POST', + data: { + a: 1 + B: 2 + }, + dataType: 'json', + contentType:'json', // the post sent is json +}); + +// result.data... +``` + +:::caution +Note, please do not return the result object directly in the request. The result object is a standard httpResponse, which cannot be directly serialized in most scenarios, and an object loop error will be thrown. +::: + +Set the request timeout time. + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +let err; +// Timeout will report an error, pay attention to catch +try { + const result = await makeHttpRequest('http://127.1:7001/', { + method: 'GET', + dataType: 'text', + timeout: 500 + }); +} catch (e) { + err = e; +} +``` + + + +### Instance form + +```typescript +import { HttpClient } from '@midwayjs/core'; + +const httpclient = new HttpClient(); +const result = await httpclient.request('http://127.1:7001/'); + +// Buffer.isBuffer(result.data) => true +``` + +Same as method form parameters. + +```typescript +import { HttpClient } from '@midwayjs/core'; + +const httpclient = new HttpClient(); +const result = await httpclient.request('http://127.1:7001/', { + method: 'POST', + data: { + a: 1 + B: 2 + }, + dataType: 'json', + contentType:'json', // the post sent is json +}); + +// result.data... +``` + +In the example form, you can reuse the created object, and you can bring some fixed parameters, such as header, with each request. + +```typescript +import { HttpClient } from '@midwayjs/core'; + +const httpclient = new HttpClient({ + headers: { + 'x-timeout': '5' + }, + method: 'POST', + timeout: 2000 +}); + +// Bring headers with you every time +const result = await httpclient.request('http://127.1:7001/'); + +``` + + + + + +## Axios support + +Midway wraps the [axios](https://github.com/axios/axios) package, allowing you to simply use the axios interface in your code. + +Some relationships with axios are as follows: + +- The interfaces are exactly the same. +- Adaptation dependency injection writing, complete type definition +- Facilitate unified instance management and configuration + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + + +### Installation dependency + +```bash +$ npm i @midwayjs/axios@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/axios": "^3.0.0", + // ... + }, +} +``` + + + +### Introducing components + + +First, introduce components and import them in `configuration.ts`: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as axios from '@midwayjs/axios'; +import { join } from 'path' + +@Configuration({ + imports: [ + axios // import axios components + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + +It can then be injected into the business code. + + + +### Use the default Axios instance + + +The API is the same as [axios](https://github.com/axios/axios). + + +```typescript +axios.request(config) +axios.get(url[, config]) +axios.delete(url[, config]) +axios.head(url[, config]) +axios.options(url[, config]) +axios.post(url[, data[, config]]) +axios.put(url[, data[, config]]) +axios.patch(url[, data[, config]]) +axios.postForm(url[, data[, config]]) +axios.putForm(url[, data[, config]]) +axios.patchForm(url[, data[, config]]) +``` + + +Use example: +```typescript +import { HttpService } from '@midwayjs/axios'; + +@Provide() +export class UserService { + + @Inject() + httpService: HttpService; + + async invoke() { + const url = 'https://midwayjs.org/resource/101010100.json'; + const result = await this.httpService.get(url); + // TODO result + } +} +``` + + + +### Configure the default Axios instance + + +The HttpService instance is equivalent to `axios.create`, so you can have some configuration parameters. These parameters are the same as axios itself. We can configure it in `src/config.default.ts`. + + +For example: +```typescript +export default { + // ... + axios: { + default: { + // The configuration of all instances reuse. + }, + clients: { + // The configuration of the default instance. + default: { + baseURL: 'https://api.example.com', + // 'headers' are custom headers to be sent + headers: { + 'X-Requested-With': 'XMLHttpRequest' + }, + timeout: 1000, // default is '0' (no timeout) + + // 'withCredentials' indicates whether or not cross-site Access-Control requests + // should be made using credentials + withCredentials: false, // default + }, + } + } +} +``` +For more information, see [axios global config](https://github.com/axios/axios#config-defaults). + + + +### Create different instances + +it is the same as multiple instances of other services. configure different keys. + +```typescript +export default { + // ... + axios: { + default: { + // The configuration of all instances reuse. + }, + clients: { + default: { + // Default instance + }, + customAxios: { + // Custom instance + } + } + } +} +``` + +The usage is as follows: + +```typescript +import { HttpServiceFactory, HttpService } from '@midwayjs/axios'; +import { InjectClient } from '@midwayjs/core'; + +@Provide() +export class UserService { + + @InjectClient(HttpServiceFactory, 'customAxios') + customAxios: HttpService; + + async invoke() { + const url = 'https://midwayjs.org/resource/101010100.json'; + const result = await this.customAxios.get(url); + // TODO result + } +} +``` + + + +### Configure global interceptors + +If you are using the default Axios instance, you can configure it as follows. + +```javascript +import { Configuration, IMidwayContainer } from '@midwayjs/core'; +import * as axios from '@midwayjs/axios'; +import { join } from 'path'; + +@Configuration({ + imports: [ + axios // import axios components + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { + + async onReady(container: IMidwayContainer) { + const httpService = await container.getAsync(axios.HttpService); + httpService.interceptors.request.use ( + config => { + // Do something before request is sent + return config; + }, + error => { + // Do something with request error + return Promise.reject(error); + } + ); + } +} +``` + +If you want to configure other instances, you can refer to the following code. + +```typescript +import { Configuration, IMidwayContainer } from '@midwayjs/core'; +import * as axios from '@midwayjs/axios'; +import { join } from 'path'; + +@Configuration({ + imports: [ + axios // import axios components + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { + + async onReady(container: IMidwayContainer) { + const httpServiceFactory = await container.getAsync(axios.HttpServiceFactory); + const customAxios = httpServiceFactory.get('customAxios'); + customAxios.interceptors.request.use( + config => { + //... + }, + error => { + //... + } + ); + } +} +``` + +### Use Axios directly + +`@midayjs/axios` also exported the original instance of `axios`, which could be useful in helper functions. + +```typescript +import { Axios } from '@midwayjs/axios'; +import { ReadStream, createWriteStream } from 'fs'; +import { finished } from 'stream/promises'; + +async function download(url: string, filename: string) { + const writer = await createWriteStream(filename); + const res = Axios.get(url, { + responseType: 'stream', + }); + res.data.pipe(writer); + await finished(writer); + return res; +} diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/bull.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/bull.md new file mode 100644 index 000000000000..9fb938b4967b --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/bull.md @@ -0,0 +1,878 @@ +# Task Queues + +Queues are a powerful design pattern that can help you meet common application scaling and performance challenges. Some of the problems queues can help you solve. + +Examples are as follows. + +- Smoothing out peaks. You can start resource-intensive tasks at any time and then add them to a queue instead of executing them synchronously. Let task processes pull tasks from the queue in a controlled manner. It is also easy to add new queue consumers to extend back-end task processing. +- Decompose single tasks that might block the Node.js event loop. For example, if a user request requires CPU-intensive work like audio transcoding, this task can be delegated to another process, freeing up the user-facing process to maintain a response. +- Provide reliable communication channels across various services. For example, you can queue tasks (jobs) in one process or service and use them in another process or service. You can receive notifications (by listening for status events) when a job completes, errors, or other status changes during the job lifecycle of any process or service. When a queue producer or consumer fails, their state is retained and job processing can be automatically restarted when the node is restarted. + +Midway provides the @midwayjs/bull package as an abstraction/wrapper on top of [Bull](https://github.com/OptimalBits/bull), a popular, well-supported, high performance NPP-based application. well-supported, high-performance implementation of the Node.js-based queueing system. This package makes it easy to integrate Bull Queues into your application. + +Bull uses Redis to hold job data, and when using Redis, the Queue architecture is fully distributed and platform independent. For example, you can run some Queue producers, consumers in one (or more) nodes (processes), and other producers and consumers on other nodes. + +This chapter introduces the @midwayjs/bull package. We also recommend reading the [Bull documentation](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md) for more background and implementation details. + +:::tip + +- 1. As of v3.6.0, the original task scheduling `@midwayjs/task` module is deprecated, so if you check the history documentation, please refer to [here](. /legacy/task). +- 2. bull is a distributed task management system and must rely on redis + +::: + + + +Related information. + +| description | | +| ----------------- | ---- | +| Available for standard projects | ✅ | +| Available for Serverless | ❌ | +| Available for Integration | ✅ | +| Include standalone mainframe | ✅ | +| Includes standalone logging | ✅ | + + + +## Installing components + +```bash +$ npm i @midwayjs/bull@3 --save +``` + +Or reinstall it after adding the following dependencies to ``package.json``. + +```json +{ + "dependencies": { + "@midwayjs/bull": "^3.0.0", + // ... + }, +} +``` + + + +## Using components + +Configure the bull component into the code. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as bull from '@midwayjs/bull'; + +@Configuration({ + imports: [ + // ... + bull + ] +}) +export class MainConfiguration { + // ... +} +``` + + + +## Some concepts + +Bull divides the entire queue into three parts + +- 1, Queue queue, which manages tasks +- 2, Job, each task object, you can start and stop control of the task +- 3、Processor, task processing, the actual logical execution part + + + +## Basic configuration + +bull is a distributed task manager with a strong dependency on redis, configured in the ``config.default.ts`` file. + +```typescript +// src/config/config.default.ts +export default { + // ... + bull: { + // default queue configuration + defaultQueueOptions: { + redis: 'redis://127.0.0.1:32768', + } + }, +} +``` + +With account password case. + +```typescript +// src/config/config.default.ts +export default { + // ... + bull: { + defaultQueueOptions: { + redis: { + port: 6379, + host: '127.0.0.1', + password: 'foobared', + }, + } + }, +} +``` + +All queues will reuse this configuration. + + + +## Writing task processors + +Use the `@Processor` decorator to decorate a class for quickly defining a task processor (we don't use Job here to avoid subsequent ambiguity). + +The `@Processor` decorator needs to be passed the name of a Queue (queue) that will be created automatically when the framework starts if there is no queue named `test`. + +For example, we write the following code in the `src/queue/test.queue.ts` file. + +```typescript +// src/queue/test.queue.ts +import { Processor, IProcessor } from '@midwayjs/bull'; + +@Processor('test') +export class TestProcessor implements IProcessor { + async execute() { + // ... + } +} +``` + +At startup, the framework automatically finds and initializes the above processor code, and automatically creates a Queue named `test`. + + + + + +## Executing tasks + +After defining the Processor, we need to execute it manually since it is not specified how to execute it. + +By getting the corresponding queue, we can easily execute the task. + + + +### Executing tasks manually + +For example, we can execute it after the project is started. + +```typescript +import { Configuration, Inject } from '@midwayjs/core'; +import * as bull from '@midwayjs/bull'; + +@Configuration({ + imports: [ + // ... + bull + ] +}) +export class MainConfiguration { + + @Inject() + bullFramework: bull; + + //... + + async onServerReady() { + // Get the Processor-related queue + const testQueue = this.bullFramework.getQueue('test'); + // Execute this task immediately + await testQueue?.runJob(); + } +} +``` + + + +### Adding execution parameters + +We can also attach some default parameters to the execution. + +```typescript +@Processor('test') +export class TestProcessor implements IProcessor { + async execute(params) { + // params.aaa => 1 + } +} + + +// invoke +const testQueue = this.bullFramework.getQueue('test'); +// Execute this task immediately +await testQueue?.runJob({ + aaa: 1, + bbb: 2, +}); +``` + + + +### Task status and management + +After executing `runJob`, we can get a `Job` object. + +```typescript +// invoke +const testQueue = this.bullFramework.getQueue('test'); +const job = await testQueue?.runJob(); +``` + +With this Job object, we can do progress management. + +```typescript +// Update progress +await job.progress(60); +// Get the progress +const progress = await job.process(); +// => 60 +``` + +Gets the job status. + +```typescript +const state = await job.getState(); +// state => 'delayed' Delayed state +// state => 'completed' completed state +``` + +For more Job API, please see [documentation](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md). + + + +### Delayed execution + +There are also some additional options when executing tasks. + +For example, delay execution by 1s. + +```typescript +const testQueue = this.bullFramework.getQueue('test'); +// Execute this task immediately +await testQueue?.runJob({}, { delay: 1000 }); +``` + + + +### Middleware and error handling + +The Bull component contains a framework that can be started independently, with its own App object and Context structure. + +We can configure separate middleware and error filters for bull's App. + +```typescript +@Configuration({ + imports: [ + // ... + bull + ] +}) +export class MainConfiguration { + + @App('bull') + bullApp: bull.Application; + + //... + + async onReady() { + this.bullApp.useMiddleare( /*middleware*/); + this.bullApp.useFilter( /*filter*/); + } +} +``` + +### Context + +The task processor execution is in the request scope, which has a special Context object structure. + +```typescript +export interface Context extends IMidwayContext { + jobId: JobId; + job: Job, + from: new (...args) => IProcessor; +} +``` + +We can access the current Job object directly from the ctx. + +```typescript +// src/queue/test.queue.ts +import { Processor, IProcessor, Context } from '@midwayjs/bull'; + +@Processor('test') +export class TestProcessor implements IProcessor { + + @Inject() + ctx: Context; + + async execute() { + // ctx.jobId => xxxx + } +} +``` + + + +### More task options + +In addition to the above delay, there are more execution options. + +| options | type | description | +| ---------------- | --------------------- | ------------------------------------------------------------ | +| priority | number | The optional priority value. The range is from 1 (highest priority) to MAX_INT (lowest priority). Note that using priority has a slight performance impact, so please use it with caution. | delay +| delay | number | The amount of time (in milliseconds) to wait for this job to be processed. Note that both the server and the client should synchronize their clocks in order to get an accurate delay. | attempts +| attempts | number | The total number of attempts before the job completes. |Repeat +| repeat | RepeatOpts | Repeat task configuration according to the cron specification, see [RepeatOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd) for more information, and the following Repeat tasks are described below. |backoff +| backoff | number \| BackoffOpts | Backoff settings for automatic retries on task failure. See [BackoffOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd). | lifo +| lifo | boolean | If true, add the task to the right end of the queue instead of the left end (default is false). | timeout +| timeout | number | The number of milliseconds for which the task failed due to a timeout error. |jobId +| jobId | number \| string | Override job id - By default, the job id is a unique integer, but you can use this setting to override it. If you use this option, it is up to you to ensure that the jobId is unique. If you try to add a job with an id that already exists, it will not be added. | removeOnComplete +| removeOnComplete | boolean \| number | If true, removes the job upon successful completion. If set to number, the number of tasks to keep for the specified task. The default behavior is to keep the task information in the completed list. | +| removeOnFail | boolean \| number | If true, removes the task if it fails after all attempts. If set to number, specify the number of tasks to keep. The default behavior is to keep the task information in the failed list. | +| stackTraceLimit | number | Limits the number of stack trace lines that will be recorded in the stack trace. | ## + + + +## Repeatedly executed tasks + +In addition to manual execution, we can also quickly configure repeated execution of tasks with the ``@Processor`` decorator parameter. + +```typescript +import { Processor, IProcessor } from '@midwayjs/bull'; +import { FORMAT } from '@midwayjs/core'; + +@Processor('test', { + repeat: { + cron: FORMAT.CRONTAB.EVERY_PER_5_SECOND + } +}) +export class TestProcessor implements IProcessor { + @Inject() + logger; + + async execute() { + // ... + } +} +``` + + + +## Common Cron expressions + +For Cron expressions, the format is as follows. + +```typescript +* * * * * * +┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ | +│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) +│ │ │ │ └───── month (1 - 12) +│ │ │ └────────── day of month (1 - 31) +│ │ └─────────────── hour (0 - 23) +│ └──────────────────── minute (0 - 59) +└───────────────────────── second (0 - 59, optional) +``` + + + +Common expressions. + +- Execute every 5 seconds: `*/5 * * * * * *` +- Execute every 1 minute: `0 */1 * * * * * ` +- Once every hour at 20 minutes: `0 20 * * * * * ` +- Once a day at 0:00: `0 0 0 * * * *` +- Once a day at 2:35: `0 35 2 * * * *` + +You can use the [online tool](https://cron.qqe2.com/) to confirm the time of the next execution. + +Midway provides some common expressions on the framework side in `@midwayjs/core` for your use. + +```typescript +import { FORMAT } from '@midwayjs/core'; + +// cron expressions executed per minute +FORMAT.CRONTAB.EVERY_MINUTE +``` + + + +There are some other expressions built in. + +| expression | corresponding time | +| ------------------------------ | --------------- | +| CRONTAB.EVERY_SECOND | per second | +| CRONTAB.EVERY_MINUTE | per minute | +| CRONTAB.EVERY_HOUR | Hourly +| EVERY_DAY | Every day at 0:00 | +| EVERY_DAY_ZERO_FIFTEEN | 0:15 PM per day | +| EVERY_DAY_ONE_FIFTEEN | 1:15 PM per day +| EVERY_PER_5_SECOND | every 5 seconds +| EVERY_PER_10_SECOND | every 10 seconds | +| EVERY_PER_30_SECOND | every 30 seconds | +| CRONTAB.EVERY_PER_5_MINUTE | every 5 minutes | +| EVERY_PER_10_MINUTE | every 10 minutes | +| EVERY_PER_30_MINUTE | every 30 minutes | + + + +## Advanced Configuration + + +### Clean up previous tasks + +By default, the framework automatically cleans up any previously unscheduled **repeating tasks**, keeping the queue of repeating tasks up to date for each one. If you don't need to clean up in some environments, you can turn it off separately. + +For example, you do not need to clean up duplicates of. + +```typescript +// src/config/config.prod.ts +export default { + // ... + bull: { + clearRepeatJobWhenStart: false, + }, +} +``` + +:::tip + +If you don't clean up, if the previous queue is executed at 10s and now it is modified to 20s, both timings will be stored in Redis, resulting in duplicate code execution. + +In daily development, if you do not clean up, it is easy to have this problem of repeated code execution. However, in a cluster deployment scenario, where multiple servers are restarted in turn, it may cause the timing task to be cleaned up accidentally, please evaluate the timing of the switch. + +::: + + + +It is also possible to clean up all tasks manually at startup. + +```typescript +// src/configuration.ts +import { Configuration, App, Inject } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { join } from 'path'; +import * as bull from '@midwayjs/bull'; + +@Configuration({ + imports: [koa, bull], + importConfigs: [join(__dirname, '. /config')], +}) +export class MainConfiguration { + @App() + app: koa; + + @Inject() + bullFramework: bull; + + async onReady() { + // At this stage, the decorator queue has not been created yet, use the API to create the queue manually in advance, the decorator will reuse the queue with the same name + const queue = this.bullFramework.createQueue('user'); + // perform cleanup manually via queue + await queue.obliterate({ force: true }); + } +} +``` + + + + + +### Clearing task history + +When Redis is turned on, by default, bull will record all successful and failed task keys, which may cause a key spike in redis, we can configure the option to clean up after success or failure. + +By default + +- 3 task records are kept on success +- 10 task records are retained on failure + +This can also be configured via parameters. + +For example, in the decorator configuration. + +```typescript +import { FORMAT } from '@midwayjs/core'; +import { IProcessor, Processor } from '@midwayjs/bull'; + +@Processor('user', { + repeat: { + cron: FORMAT.CRONTAB.EVERY_MINUTE, + }, + removeOnComplete: 3, // remove task records after success, keep up to 3 recent records + removeOnFail: 10, // remove task records after failure +}) +export class UserService implements IProcessor { + execute(data: any) { + // ... + } +} + +``` + +Can also be configured in the global config. + +```typescript +// src/config/config.default.ts +export default { + // ... + bull: { + defaultQueueOptions: { + // default job configuration + defaultJobOptions: { + // Keep 10 records + removeOnComplete: 10, + }, + }, + }, +} +``` + + + + + +### Redis Clustering + +You can use the `createClient` method provided by bull to access custom redis instances so you can access Redis clusters. + +For example. + +```typescript +// src/config/config.default +import Redis from 'ioredis'; + +const clusterOptions = { + enableReadyCheck: false, // must be false + retryDelayOnClusterDown: 300, + retryDelayOnFailover: 1000, + retryDelayOnTryAgain: 3000, + slotsRefreshTimeout: 10000, + maxRetriesPerRequest: null // must be null +} + +const redisClientInstance = new Redis. + Cluster([ + port: 7000, + host: '127.0.0.1' + }, + { + port: 7002, + host: '127.0.0.1' + }, +], clusterOptions); + +export default { + bull: { + defaultQueueOptions: { + createClient: (type, opts) => { + return redisClientInstance; + }, + // The keys stored for these tasks all start with the same key to distinguish the user's original redis configuration + prefix: '{midway-bull}', + }, + } +} +``` + + + +## Queue Management + +Queues are inexpensive, each Job is bound to a queue, and in some cases we can also manage queues manually by performing operations on them. + + + +### Manual queue creation + +In addition to simply defining a queue using `@Processor`, we can also create it using the API. + +```typescript +import { Configuration, Inject } from '@midwayjs/core'; +import * as bull from '@midwayjs/bull'; + +@Configuration({ + imports: [ + // ... + bull + ] +}) +export class MainConfiguration { + + @Inject() + bullFramework: bull.Framework; + + async onReady() { + const testQueue = this.bullFramework.createQueue('test', { + redis: { + port: 6379, + host: '127.0.0.1', + password: 'foobared', + }, + prefix: '{midway-bull}', + }); + + // ... + } +} +``` + +After creating a queue manually with `createQueue`, the queue will still be saved automatically. If the queue name is used by `@Processor` at startup, the already created queue is automatically used. + +For example. + +```typescript +// will automatically use the queue with the same name created manually above +@Processor('test') +export class TestProcessor implements IProcessor { + async execute(params) { + } +} +``` + + + +### Get the queue + +We can simply get the queue based on the queue name. + +```typescript + const testQueue = bullFramework.getQueue('test'); +``` + +You can also get it through a decorator. + +```typescript +import { InjectQueue, BullQueue } from '@midwayjs/bull'; +import { Provide } from '@midwayjs/core'; + +@Provide() +export class UserService { + @InjectQueue('test') + testQueue: BullQueue; + + async invoke() { + await this.testQueue.pause(); + // ... + } +} +``` + + + +### Queue common operations + +Suspend the queue. + +```typescript +await testQueue.pause(); +``` + +Continue the queue. + +```typescript +await testQueue.resume(); +``` + +Queue events. + +```typescript +// Local events pass the job instance... +testQueue.on('progress', function (job, progress) { + console.log(`Job ${job.id} is ${progress * 100}% ready!`); +}); + +testQueue.on('completed', function (job, result) { + console.log(`Job ${job.id} completed! Result: ${result}`); + job.remove(); +}); +``` + +See [here](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md) for the full queue API. + + + +## Component logging + +The component has its own log, which by default will be `ctx.logger` in `midway-bull.log`. + +We can configure this logger object separately. + +```typescript +export default { + midwayLogger: { + clients: { + // ... + bullLogger: { + fileLogName: 'midway-bull.log', + }, + }, + } +} +``` + +The output format of this log, we can also configure separately. + +```typescript +export default { + bull: { + // ... + contextLoggerFormat: info => { + const { jobId, from } = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} [${jobId} ${from.name}] ${info.message}`; + }, + } +} +``` + + + +## About Redis version + +Please choose the latest version (>=5) if possible. Currently, there is a problem of scheduled task creation failure on lower versions of redis. + + + +## Bull UI + +In a distributed scenario, we can leverage the Bull UI to simplify management. + +Similar to the bull component, it needs to be installed and enabled independently. + +```bash +$ npm i @midwayjs/bull-board@3 --save +``` + +Or reinstall it after adding the following dependencies to ``package.json``. + +```json +{ + "dependencies": { + "@midwayjs/bull-board": "^3.0.0", + // ... + }, +} +``` + +Configure the bull-board component into the code. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as bull from '@midwayjs/bull'; +import * as bullBoard from '@midwayjs/bull-board'; + +@Configuration({ + imports: [ + // ... + bull, + bullBoard, + ] +}) +export class MainConfiguration { + //... +} +``` + +The default access path is: `http://127.1:7001/ui`. + +The effect is as follows. + +![](https://img.alicdn.com/imgextra/i2/O1CN01j4wEFb1UacPxA06gs_!!6000000002534-2-tps-1932-1136.png) + +The base path can be modified by configuration. + +```typescript +// src/config/config.prod.ts +export default { + // ... + bullBoard: { + basePath: '/ui', + }, +} +``` + +In addition, the component provides the `BullBoardManager` class, which can add queues dynamically created. + +```typescript +import { Configuration, Inject } from '@midwayjs/core'; +import * as bull from '@midwayjs/bull'; +import * as bullBoard from '@midwayjs/bull-board'; + +@Configuration({ + imports: [ + // ... + bull, + bullBoard + ] +}) +export class MainConfiguration { + + @Inject() + bullFramework: bull.Framework; + + @Inject() + bullBoardManager: bullBoard.BullBoardManager; + + async onReady() { + const testQueue = this.bullFramework.createQueue('test', { + // ... + }); + + this.bullBoardManager.addQueue(testQueue); + } +} +``` + + + + + +## Common problems + +### 1. EVALSHA error + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01KfjCKT1yypmNPDkIL_!!6000000006648-2-tps-3540-102.png) + +This problem is basically clear, the problem will appear on the clustered version of redis. + +The reason is that redis does hash on the key to determine the storage slot, and the key of @midwayjs/bull hits a different slot in this step under the cluster. + +Solution: The prefix configuration in the task is included with {} to force redis to only calculate the hash in {}, for example `prefix: '{midway-task}'`. + +### 2. EVAL inside MULTI is not allowed error + +This shows that task queue API calls such as `queue.createBulk()` and `job.moveToFailed()` are invalid and the following error occurs. + +``` +ReplyError: EXECABORT Transaction discarded because of previous errors. + at parseError (/node_modules/redis-parser/lib/parser.js:179:12) + at parseType (/node_modules/redis-parser/lib/parser.js:302:14) { + command: { name: 'exec', args: [] }, + previousErrors: [ + ReplyError: ERR 'EVAL' inside MULTI is not allowed + at parseError (/node_modules/redis-parser/lib/parser.js:179:12) + at parseType (/node_modules/redis-parser/lib/parser.js:302:14) { + command: [Object] + } + ] +} +``` + +:::tip + +Often occurs when using Alibaba Cloud Redis service. + +::: + +Since EVAL or EVALSHA are used in the Redis Lua scripts that these APIs depend on, when Alibaba Cloud Redis uses proxy mode to connect, additional restrictions will be placed on Lua script calls, including [EVAL commands are not allowed to be executed in MULTI transactions] (https:// help.aliyun.com/zh/redis/support/usage-of-lua-scripts?#section-8f7-qgv-dlv), the document also mentions that this verification can be turned off through parameter configuration script_check_enable, but the verification is invalid. + +Solution: + +* 1. Open the direct connection address in the Alibaba Cloud console and switch the service to direct connection mode. +* 2. Switch the client to cluster mode. Refer to the above "Redis Cluster" chapter to switch the configuration mode. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/busboy.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/busboy.md new file mode 100644 index 000000000000..7c3d5dd49531 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/busboy.md @@ -0,0 +1,749 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# File upload + +A general upload component for `@midwayjs/faas`, `@midwayjs/web`, `@midwayjs/koa` and `@midwayjs/express` frameworks, supporting two modes: `file` (temporary server file) and `stream`. + +Related information: + +| Web support | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | 💬 | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + +:::caution + +💬 Some function computing platforms do not support streaming request responses, please refer to the corresponding platform capabilities. + +::: + +:::tip + +This module replaces the upload component since 3.17.0. + +The differences from the upload component are: + +* 1. The configuration key is adjusted from `upload` to `busboy` + +* 2. The middleware is no longer loaded by default, and can be manually configured to the global or route + +* 3. the input parameter definition type is adjusted to `UploadStreamFileInfo` + +* 4. The configuration of `fileSize` has been adjusted + +::: + + + +## Install dependencies + +```bash +$ npm i @midwayjs/busboy@3 --save +``` + +Or add the following dependencies to `package.json` and reinstall. + +```json +{ +"dependencies": { + "@midwayjs/busboy": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + +## Enable component + +```typescript +// src/configuratin.ts + +import { Configuration } from '@midwayjs/core'; +import * as busboy from '@midwayjs/busboy'; + +@Configuration({ + imports: [ + // ...other components + busboy + ], + // ... +}) +export class MainConfiguration {} +``` + +## Configure middleware + +The `UploadMiddleware` middleware is provided in the component, which can be configured globally or to a specific route. It is recommended to configure it to a specific route to improve performance. + + + +**Route Middleware** + +```typescript +import { Controller, Post } from '@midwayjs/core'; +import { UploadMiddleware } from '@midwayjs/busboy'; + +@Controller('/') +export class HomeController { + + @Post('/upload', { middleware: [UploadMiddleware] }) + async upload(/*...*/) { + // ... + } +} +``` + +**Global Middleware** + + + + +```typescript +// src/configuratin.ts + +import { Configuration } from '@midwayjs/core'; +import * as busboy from '@midwayjs/busboy'; +import { Application } from '@midwayjs/koa'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App('koa') + app: Application; + + async onReady() { + this.app.useMiddleware(busboy.UploadMiddleware); + } +} +``` + + + +```typescript +// src/configuratin.ts + +import { Configuration } from '@midwayjs/core'; +import * as busboy from '@midwayjs/busboy'; +import { Application } from '@midwayjs/web'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App('egg') + app: Application; + + async onReady() { + this.app.useMiddleware(busboy.UploadMiddleware); + } +} +``` + + + +```typescript +// src/configuratin.ts + +import { Configuration } from '@midwayjs/core'; +import * as busboy from '@midwayjs/busboy'; +import { Application } from '@midwayjs/express'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App('express') + app: Application; + + async onReady() { + this.app.useMiddleware(busboy.UploadMiddleware); + } +} +``` + + + +```typescript +// src/configuratin.ts + +import { Configuration } from '@midwayjs/core'; +import * as busboy from '@midwayjs/busboy'; +import { Application } from '@midwayjs/faas'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App('faas') + app: Application; + + async onReady() { + this.app.useMiddleware(busboy.UploadMiddleware); + } +} +``` + + + + + +## Configuration + +The component uses `busboy` as the configuration key. + +### Upload mode + +### Upload Modes + +There are three upload modes: file mode, stream mode, and the newly added async iterator mode. + +In the code, the `@Files()` decorator is used to obtain the uploaded files, and the `@Fields` decorator is used to get other upload form fields. + + + + +`file` is the default value, with `mode` configured as the string `file`. + +```typescript +// src/config/config.default.ts +export default { + // ... + busboy: { + mode: 'file', + }, +} + +``` + +In the code, the uploaded files can be retrieved, and multiple files can be uploaded simultaneously. + + +```typescript +import { Controller, Post, Files, Fields } from '@midwayjs/core'; +import { UploadFileInfo } from '@midwayjs/busboy'; + +@Controller('/') +export class HomeController { + + @Post('/upload', /*...*/) + async upload(@Files() files: Array, @Fields() fields: Record) { + /* + files = [ + { + filename: 'test.pdf', // file name + data: '/var/tmp/xxx.pdf', // Server temporary file address + mimeType: 'application/pdf', // mime + fieldName: 'file' // field name + }, + // ...Support uploading multiple files at the same time under file + ] + */ + } +} +``` + +When using the file mode, the retrieved `data` represents the `temporary file path` of the uploaded file on the server. You can later handle the file contents using methods like `fs.createReadStream`. Multiple files can be uploaded at once, and they will be stored in an array. + +Each object in the array contains the following fields: + +```typescript +export interface UploadFileInfo { + /** + * The name of the uploaded file + */ + filename: string; + /** + * The MIME type of the uploaded file + */ + mimeType: string; + /** + * The path where the file is saved on the server + */ + data: string; + /** + * The form field name of the uploaded file + */ + fieldName: string; +} +``` + + + + + +Available since `v3.18.0`, this mode replaces the previous `stream` mode and supports streaming uploads of multiple files. + +Configure the `mode` as the string `asyncIterator`. + +```typescript +// src/config/config.default.ts +export default { + // ... + busboy: { + mode: 'asyncIterator', + }, +} +``` + +Retrieve the uploaded files in the code. + +```typescript +import { Controller, Post, Files, Fields } from '@midwayjs/core'; +import { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy'; + +@Controller('/') +export class HomeController { + + @Post('/upload', /*...*/) + async upload( + @Files() fileIterator: AsyncGenerator, + @Fields() fieldIterator: AsyncGenerator + ) { + // ... + } +} +``` + +In this mode, both `@Files` and `@File` decorators provide the same `AsyncGenerator`, and `@Fields` also provides an `AsyncGenerator`. + +By looping through the `AsyncGenerator`, you can handle the `ReadStream` of each uploaded file. + +```typescript +import { Controller, Post, Files, Fields } from '@midwayjs/core'; +import { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy'; +import { tmpdir } from 'os'; +import { createWriteStream } from 'fs'; + +@Controller('/') +export class HomeController { + + @Post('/upload', /*...*/) + async upload( + @Files() fileIterator: AsyncGenerator, + @Fields() fieldIterator: AsyncGenerator + ) { + for await (const file of fileIterator) { + const { filename, data } = file; + const p = join(tmpdir, filename); + const stream = createWriteStream(p); + data.pipe(stream); + } + + for await (const { name, value } of fieldIterator) { + // ... + } + + // ... + } +} +``` + +Note that if any file throws an error during the upload process, the entire upload stream will close, and all incomplete file uploads will fail. + +The upload object in the async iterator contains the following fields. + +```typescript +export interface UploadStreamFieldInfo { + /** + * The name of the uploaded file + */ + filename: string; + /** + * The MIME type of the uploaded file + */ + mimeType: string; + /** + * The file stream of the uploaded file + */ + data: Readable; + /** + * The form field name of the uploaded file + */ + fieldName: string; +} +``` + +The object for `@Fields` in the async iterator is slightly different, with the returned data containing `name` and `value` fields. + +```typescript +export interface UploadStreamFieldInfo { + /** + * Form name + */ + name: string; + /** + * Form value + */ + value: any; +} +``` + + + + + +:::caution + +No longer recommended for use. + +::: + +Configure `mode` as the string `stream`. + +When using stream mode, the `data` obtained from `@Files` is a `ReadStream`, and the data can be transferred to other `WriteStream` or `TransformStream` through `pipe` and other methods. + +When using stream mode, only one file is uploaded at a time, that is, there is only one file data object in the `@Files` array. + +In addition, stream mode `does not` generate temporary files on the server, so there is no need to manually clean up the temporary file cache after obtaining the uploaded content. + +:::tip + +The implementation method of Faas scenarios depends on the platform. If the platform does not support streaming request/response but the business has enabled `mode: 'stream'`, it will be downgraded by reading it into memory first and then simulating streaming transmission. + +::: + +Get the uploaded file in the code. Only a single file is supported in streaming mode. + +```typescript +import { Controller, Post, Files, Fields } from '@midwayjs/core'; +import { UploadStreamFileInfo } from '@midwayjs/busboy'; + +@Controller('/') +export class HomeController { + + @Post('/upload', /*...*/) + async upload(@Files() files: Array, @Fields() fields: Record + + + + + +### Upload file suffix check + +Use the `whitelist` property to configure the file suffixes allowed for upload. If `null` is configured, the suffix will not be checked. + +:::caution + +If `null` is configured, the uploaded file suffix will not be checked. If the file upload mode (mode=file) is adopted, it may be exploited by attackers to upload WebShells with suffixes such as `.php` and `.asp` to implement attack behaviors. + +Of course, since the component will `re-randomly generate` the file name for the uploaded temporary file, as long as the developer `does not return` the uploaded temporary file address to the user, even if the user uploads some unexpected files, there is no need to worry too much about being exploited. + +::: + +If the uploaded file suffix does not match, it will respond with `400` error. The default values are as follows: + +```ts +'.jpg', +'.jpeg', +'.png', +'.gif', +'.bmp', +'.wbmp', +'.webp', +'.tif', +'.psd', +'.svg', +'.js', +'.jsx', +'.json', +'.css', +'.less', +'.html', +'.htm', +'.xml', +'.pdf', +'.zip', +'.gz', +'.tgz', +'.gzip', +'.mp3', +'.mp4', +'.avi', +``` + +The default suffix whitelist can be obtained through the `uploadWhiteList` exported in the component. + +In addition, in order to prevent some `malicious users` from using certain technical means to `forge` some extensions that can be truncated, the midway upload component will filter the binary data of the obtained extensions, and only support characters in the range of `0x2e` (that is, English dot `.`), `0x30-0x39` (that is, numbers `0-9`), and `0x61-0x7a` (that is, lowercase letters `a-z`) as extensions. Other characters will be automatically ignored. + +You can pass a function to dynamically return the whitelist based on different conditions. + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/busboy'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + busboy: { + whitelist: (ctx) => { + if (ctx.path === '/') { + return [ + '.jpg', + '.jpeg', + ]; + } else { + return [ + '.jpg', + ] + }; + }, + // ... + }, +} +``` + + + + + +### Upload file MIME type check + +Some malicious users will try to modify the extension of WebShell such as `.php` to `.jpg` to bypass the whitelist filtering rules based on extensions. In some server environments, this jpg file will still be executed as a PHP script, causing security risks. + +The component provides the `mimeTypeWhiteList` configuration parameter **[Please note that this parameter has no default value, that is, it is not checked by default]**. You can use this configuration to set the allowed file MIME format. The rule is a `secondary array` composed of the array `[extension, mime, [...moreMime]]`, for example: + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/busboy'; +export default { + // ... + busboy: { + // ... + // Extension whitelist + whitelist: uploadWhiteList, + // Only the following file types are allowed to be uploaded + mimeTypeWhiteList: { + '.jpg': 'image/jpeg', + // You can also set multiple MIME types. For example, the following allows files with the .jpeg suffix to be either jpg or png. + '.jpeg': ['image/jpeg', 'image/png'], + // Other types + '.gif': 'image/gif', + '.bmp': 'image/bmp', + '.wbmp': 'image/vnd.wap.wbmp', + '.webp': 'image/webp', + } + }, +} +``` + +You can also use the `DefaultUploadFileMimeType` variable provided by the component as the default MIME validation rule, which provides MIME data for commonly used file extensions such as `.jpg`, `.png`, and `.psd`: + +```typescript +// src/config/config.default.ts +import { uploadWhiteList, DefaultUploadFileMimeType } from '@midwayjs/busboy'; +export default { + // ... + busboy: { + // ... + // Extension whitelist + whitelist: uploadWhiteList, + // Only the following file types are allowed to be uploaded + mimeTypeWhiteList: DefaultUploadFileMimeType, + }, +} +``` + +You can query the file format and the corresponding MIME mapping through the website `https://mimetype.io/`. For the MIME identification of files, we use the npm package [file-type@16](https://www.npmjs.com/package/file-type). Please pay attention to the file types it supports. + +:::info + +MIME type verification rules only apply to file upload mode `mode=file`. After setting this verification rule, the upload performance will be slightly affected because the file content needs to be read for matching. + +However, we still recommend that you set the `mimeTypeWhiteList` parameter when conditions permit, which will improve the security of your application. + +::: + +You can pass a function that can dynamically return MIME rules based on different conditions. + +```typescript +// src/config/config.default.ts +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + busboy: { + mimeTypeWhiteList: (ctx) => { + if (ctx.path === '/') { + return { + '.jpg': 'image/jpeg', + }; + } else { + return { + '.jpeg': ['image/jpeg', 'image/png'], + } + }; + } + }, +} + +``` + + + +### Busboy upload limit + +By default, there is no limit, which can be modified through configuration, digital type, unit is byte. + +```typescript +// src/config/config.default.ts +export default { + // ... + busboy: { + // ... + limits: { + fileSize: 1024 + } + }, +} +``` + +In addition, you can set some other [limits]((https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports).). + +### Temporary files and cleanup + +If you use the `file` mode to get the uploaded files, the uploaded files will be stored in the folder pointed to by the `tmpdir` option in the `upload` component configuration you set in the `config` file. + +You can control the automatic temporary file cleanup time by using `cleanTimeout` in the configuration. The default value is `5 * 60 * 1000`, that is, the uploaded files will be automatically cleaned up after `5 minutes`. Setting it to `0` will disable the automatic cleanup function. + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/busboy'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + busboy: { + mode: 'file', + tmpdir: join(tmpdir(), 'midway-busboy-files'), + cleanTimeout: 5 * 60 * 1000, + }, +} + +``` + +You can also call `await ctx.cleanupRequestFiles()` in the code to actively clean up temporary files uploaded by the current request. + +### Setting configurations for different routes + +Different middleware instances can be used to configure different routes differently. In this scenario, the global configuration will be merged and only a small part of the configuration can be covered. + +```typescript +import { Controller, Post, Files, Fields } from '@midwayjs/core'; +import { UploadFileInfo, UploadMiddleware } from '@midwayjs/busboy'; + +@Controller('/') +export class HomeController { + @Post('/upload1', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'file'}) ]}) + async upload1(@Files() files Array) { + // ... + } + + @Post('/upload2', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'stream'}) ]}) + async upload2(@Files() files Array) { + // ... + } +} +``` + +Currently the configurations that can be passed include `mode` and `busboy`'s own [configuration](https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports). + + + +## Built-in errors + +The following errors will be automatically triggered in different modes. + +* `MultipartInvalidFilenameError` Invalid file name +* `MultipartInvalidFileTypeError` Invalid file type +* `MultipartFileSizeLimitError` File size exceeds limit +* `MultipartFileLimitError` Number of files exceeds limit +* `MultipartPartsLimitError` Number of uploaded parts exceeds limit +* `MultipartFieldsLimitError` Number of fields exceeds limit +* `MultipartError` Other busbuy errors + +## Security tips + +1. Please pay attention to whether to enable `extension whitelist` (whiteList). If the extension whitelist is set to `null`, it may be used by attackers to upload `.php`, `.asp` and other WebShells. + +2. Please pay attention to whether to set `match` or `ignore` rules, otherwise ordinary `POST/PUT` and other interfaces may be used by attackers, causing server load to increase and space to occupy a lot of problems. +3. Please pay attention to whether `file type rules` (fileTypeWhiteList) are set, otherwise the attacker may forge the file type for uploading. + +## Front-end file upload example + +### 1. HTML form format + +```html +
+ Name:
+ File:
+ +
+``` + +### 2. fetch FormData method + +```js +const fileInput = document.querySelector('#your-file-input') ; +const formData = new FormData(); +formData.append('file', fileInput.files[0]); + +fetch('/api/upload', { + method: 'POST', + body: formData, +}); +``` + + + +## Postman test example + +![](https://img.alicdn.com/imgextra/i4/O1CN01iv9ESW1uIShNiRjBF_!!6000000006014-2-tps-2086-1746.png) + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cache.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cache.md new file mode 100644 index 000000000000..8134c86a0ba5 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cache.md @@ -0,0 +1,249 @@ +# Cache + +Midway Cache is a component that facilitates developers to perform caching operations, and it is beneficial to improve the performance of the project. It provides us with a data center for efficient data access. + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + +## Installation + +First install the relevant component modules. + +```bash +$ npm i @midwayjs/cache@3 cache-manager --save +$ npm i @types/cache-manager --save-dev +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/cache": "^3.0.0", + "cache-manager": "^3.4.1 ", + // ... + }, + "devDependencies": { + "@types/cache-manager": "^3.4.0 ", + // ... + } +} +``` + + + +## Use Cache + +Midway provides a unified API for different cache stores. By default, a data center based on memory data storage is built in. If you want to use another data center, developers can also switch to modes such as mongodb and fs. + + +First, the Cache component is introduced and imported in the `configuration.ts`: + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as cache from '@midwayjs/cache'; +import { join } from 'path' + +@Configuration({ + imports: [ + // ... + cache // import cache component + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + +It can then be injected into the business code. + +```typescript +import { Inject, Provide } from '@midwayjs/core'; +import { IUserOptions } from '../interface'; +import { CacheManager } from '@midwayjs/cache'; + +@Provide() +export class UserService { + + @Inject() + cacheManager: CacheManager; // inject CacheManager +} +``` + +Set through the provided API to obtain cached data. + + +```typescript +import { Inject, Provide } from '@midwayjs/core'; +import { IUserOptions } from '../interface'; +import { CacheManager } from '@midwayjs/cache'; + +@Provide() +export class UserService { + + @Inject() + cacheManager: CacheManager; + + async getUser(options: IUserOptions) { + // Set cache content + await this.cacheManager.set('name', 'stone-jin'); + + // Get cached content + let result = await this.cacheManager.get('name'); + + return result; + } + + async getUser2() { + // Get cached content + let result = await this.cacheManager.get('name'); + return result; + } + + async reset() { + Await this.cacheManager.reset(); // Clear the contents of the corresponding store + } +} +``` + + + +### Set cache + + +You can use the `await this.cache.set(key, value)` method to set this parameter. The default expiration time is 10s. + + +You can also manually set TTL (Expiration Time), as follows: +```typescript +Await this.cacheManager.set(key, value, {ttl: 1000}); // ttl is in seconds +``` +If you don't want to Cache expired, set TTL to null. +```typescript +await this.cacheManager.set(key, value, {ttl: null}); +``` +At the same time, you can also set it through the global `config.default.ts`. +```typescript +export default { + // ... + cache: { + store: 'memory', + options: { + max: 100 + Ttl: 10, // Modify the default ttl configuration + }, + } +} +``` + + +### Get cache + +```typescript +const value = await this.cacheManager.get(key); +``` +If not, undefined. + + + +### Remove cache + + +To remove the cache, you can use the del method. +```typescript +await this.cacheManager.del(key); +``` + + + +### Empty the overall store data (here is the overall clear, need to focus on⚠) + + +For example, if the user sets a redis as store, the call will be cleared, including those set by non-cache modules. +```typescript +await this.cacheManager.reset(); // This piece needs attention +``` + + + +## Global configuration + + +When we refer to this cache component, we can configure it globally. The configuration method is similar to other components. + + +Default configuration: +```typescript +export default { + // ... + cache: { + store: 'memory', + options: { + max: 100 + ttl: 10 + }, + } +} +``` +For example, users can modify the default TTL, that is, the expiration time. + + + +## Other Cache + + +You can also modify the store method to configure components in `config.default.ts`: +```typescript +import * as redisStore from 'cache-manager-ioredis'; + +export default { + // ... + cache: { + store: redisStore + options: { + host: 'localhost', // default value + port: 6379, // default value + password: '', + db: 0 + keyPrefix: 'cache :', + ttl: 100 + }, + } +} +``` +Or modify it to the mongodb cache. + + +:::danger +**note again⚠️: When using redis as cache, the reset method is used with caution in the code, because the entire redis will be flushdb, or the data will be cleared for short. ** +::: + + + +## Related documents + + +Because Midway Cache is based on cache-manager encapsulation, users can also query [cache-manger](https://www.npmjs.com/package/cache-manager). + + + +## Frequently Asked Questions + + + +### 1. Can set and get not get the same value? + +The user uses the cache module, which is in memory by default. For example, the dev mode is used locally. Since it is single-process, the set and ge t can eventually reach the same value. However, after the user is deployed to the server, there will be many workers, which is equivalent to the first request, falling on process 1, and then falling on process 2 for the second time, thus getting empty. + + +Solution: Refer to other cache sections to configure the store to be distributed, such as the store for redis. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/caching.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/caching.md new file mode 100644 index 000000000000..c1ebc576d0e5 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/caching.md @@ -0,0 +1,536 @@ +# Caching + +Caching is a great and simple technique that helps improve the performance of your application. This component provides cache-related capabilities. You can cache data to different data sources, and you can also create multi-level caches for different scenarios to improve data access speed. + +:::tip + +Midway provides a module based on [cache-manager v5](https://github.com/node-cache-manager/node-cache-manager) that re-encapsulates the cache component. The original cache module is developed based on v3 and is no longer iterated, such as To view old documentation, please visit [here](/docs/extensions/cache). + +::: + +Related Information: + +| Description | | +| ------------------------------- | ---- | +| Available for standard projects | ✅ | +| Serverless available | ✅ | +| Available for integration | ✅ | +| Contains independent main frame | ❌ | +| Contains standalone logs | ❌ | + + + +## Install + +First install the relevant component modules. + +```bash +$ npm i @midwayjs/cache-manager@3 --save +``` + +Or add the following dependencies in `package.json` and reinstall. + +```json +{ + "dependencies": { + "@midwayjs/cache-manager": "^3.0.0", + // ... + }, +} +``` + + + +## Enable component + + +First, introduce the component and import it in `configuration.ts`: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as cacheManager from '@midwayjs/cache-manager'; +import { join } from 'path' + +@Configuration({ + imports: [ + // ... + cacheManager, + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + +## Use cache + + + +### 1. Configure cache + +Before use, you need to configure the location of the cache, such as the built-in memory cache, or introduce the Redis cache. Each cache corresponds to a cache Store. + +The following example code configures an in-memory cache named `default`. + +```typescript +// src/config/config.default.ts +export default { + cacheManager: { + clients: { + default: { + store: 'memory', + }, + }, + } +} +``` + +In the most commonly used scenario, the cache will contain two parameters. Configure `max` to modify the number of caches, and configure `ttl` to modify the expiration time of the cache, in milliseconds. + +```typescript + // src/config/config.default.ts +export default { + cacheManager: { + clients: { + default: { + store: 'memory', + options: { + max: 100, + ttl: 10, + }, + }, + }, + } +} +``` + +:::tip + +* The eviction algorithm used by the memory cache is LRU +* The unit of `ttl` is milliseconds + +::: + +### 2. Use cache + +Instances can be obtained through the service factory decorator, and caches can be obtained and saved through simple `get` and `set` methods. + +```typescript +import { InjectClient, Provide } from '@midwayjs/core'; +import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + + @InjectClient(CachingFactory, 'default') + cache: MidwayCache; + + async invoke(name: string, value: string) { + // Set up cache + await this.cache.set(name, value); + // Get cache + const data = await this.cache.get(name); + // ... + } +} + +``` + +Dynamically set `ttl` expiration time. + +```typescript +await this.cache.set('key', 'value', 1000); +``` + +To disable cache expiration, you can set the `ttl` configuration property to 0. + +```typescript +await this.cache.set('key', 'value', 0); +``` + +Delete a single cache. + +```typescript +await this.cache.del('key'); +``` + +To clear the entire cache, use the `reset` method. + +```typescript +await this.cacheManager.reset(); +``` + +:::danger + +Note that clearing the entire cache is very dangerous. If Redis is used as the cache store, the entire Redis data will be cleared. + +::: + +In addition to decorators, cache instances can also be obtained through the API. + +```typescript +import { InjectClient, Provide } from '@midwayjs/core'; +import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + + @Inject() + cachingFactory: CachingFactory; + + async invoke() { + const caching = await this.cachingFactory.get('default'); + // ... + } +} +``` + + + +### 3. Configure multiple caches + +Like other components, the component supports configuring multiple cache instances. + +```typescript +// src/config/config.default.ts +export default { + cacheManager: { + clients: { + default: { + store: 'memory', + }, + otherCaching: { + store: 'memory', + } + }, + } +} +``` + +Different cache instances can be injected. + +```typescript +import { InjectClient, Provide } from '@midwayjs/core'; +import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + + @InjectClient(CachingFactory, 'default') + cache: MidwayCache; + + @InjectClient(CachingFactory, 'otherCaching') + customCaching: MidwayCache; + +} + +``` + + + +### 4. Configure different Stores + +The component is based on [cache-manager](https://github.com/node-cache-manager/node-cache-manager) and can be configured with different cache stores. For example, the most common one can be configured with Redis Store. + +If the project has been configured with a 'Redis', you can quickly create a Redis Store by using the built-in 'createRedisStore' method of the component. + +```typescript +import { createRedisStore } from '@midwayjs/cache-manager'; + +// src/config/config.default.ts +export default { + cacheManager: { + clients: { + default: { + store: createRedisStore('default'), + options: { + ttl: 10, + } + }, + }, + }, + redis: { + clients: { + default: { + port: 6379, + host: '127.0.0.1', + } + } + } +} +``` + +The `createRedisStore` method can pass an already configured redis instance name, and the instance can be reused with the redis component. + + + +### 5. Configure third-party Store + +In addition to Redis, users can also choose the Store of Cache-Manager. The list can be found [here](https://github.com/node-cache-manager/node-cache-manager?tab=readme-ov-file#store-engines). + +Below is an example of configuring [node-cache-manager-ioredis-yet](https://github.com/node-cache-manager/node-cache-manager-ioredis-yet). + +```typescript +// src/config/config.default.ts +import { redisStore } from 'cache-manager-ioredis-yet'; + +export default { + cacheManager: { + clients: { + default: { + store: redisStore, + options: { + port: 6379, + host: 'localhost', + ttl: 10, + }, + }, + }, + }, +} +``` + + + +### 6. Multi-level cache + +[cache-manager](https://github.com/node-cache-manager/node-cache-manager) supports aggregating multiple cache stores to achieve multi-level caching. + +For example, I can create a multi-level cache to merge multiple cache stores together. + +```typescript +// src/config/config.default.ts +import { createRedisStore } from '@midwayjs/cache-manager'; +export default { + cacheManager: { + clients: { + memoryCaching: { + store: 'memory', + }, + redisCaching: { + store: createRedisStore('default'), + options: { + ttl: 10, + } + }, + multiCaching: { + store: ['memoryCaching', 'redisCaching'], + options: { + ttl: 100, + }, + }, + }, + }, + redis: { + clients: { + default: { + port: 6379, + host: '127.0.0.1', + } + } + } +} +``` + +In this way, the cache instance `multiCaching` contains two levels of cache. The cache priority is from top to bottom. When searching, it will first search `memoryCaching`. If the key does not exist in the memory cache, it will continue to search `redisCaching`. + + + +### 7. Use multi-level cache + +Similar to ordinary cache, multi-level cache also adds `mset`, `mget` and `mdel` methods in addition to `set`, `get` and `del` methods. + +```typescript +import { InjectClient, Provide } from '@midwayjs/core'; +import { CachingFactory, MidwayMultiCache } from '@midwayjs/cache-manager'; + +const userId2 = 456; +const key2 = 'user_' + userId; +const ttl = 5; + +@Provide() +export class UserService { + + @InjectClient(CachingFactory, 'multiCaching') + multiCache: MidwayMultiCache; + + async invoke() { + // Set to all levels of caching + await this.multiCache.set('foo2', 'bar2', ttl); + + // Get the key from the highest priority cache Store + console.log(await this.multiCache.get('foo2')); + // >> "bar2" + + // Call the del method of each Store to delete + await this.multiCache.del('foo2'); + + // Set multiple keys in all caches, including multiple key-value pairs + await this.multiCache.mset( + [ + ['foo', 'bar'], + ['foo2', 'bar2'], + ], + ttl + ); + + // mget() fetches from highest priority cache. + // If the first cache does not return all the keys, + // the next cache is fetched with the keys that were not found. + // This is done recursively until either: + // - all have been found + // - all caches has been fetched + console.log(await this.multiCache.mget('key', 'key2')); + // >> ['bar', 'bar2'] + + // Call the mdel method of each Store to delete + await this.multiCache.mdel('foo', 'foo2'); + } +} + +``` + +### 8. Automatic refresh + +Whether it is a normal cache or a multi-level cache, the background refresh function is supported. You only need to configure the `refreshThreshold` time, in milliseconds. + +```typescript +// src/config/config.default.ts +export default { + cacheManager: { + clients: { + default: { + store: 'memory', + options: { + refreshThreshold: 3 * 1000, + }, + }, + }, + } +} +``` + +If `refreshthreshold` is set, each time the value is obtained from the cache, the value of `ttl` will be checked. If the remaining `ttl` is less than `refreshthreshold`, the system will update the cache asynchronously and the system will return the old value until` ttl` Expired. + +:::tip + +* In case of multi-level cache, the store that will be checked for refresh is the one where the key will be found first (highest priority). + +* If the threshold is low and the worker function is slow, the key may expire and you may encounter a racing condition with updating values. +* The background refresh mechanism currently does not support providing multiple keys. +* If no `ttl` is set for the key, the refresh mechanism will not be triggered. For redis, the `ttl` is set to -1 by default. + +::: + + + +## Automatic caching + +### Use decorator cache methods + +You can cache the results of methods through the `@Caching` decorator, such as caching the results of http responses or service calls. + +```typescript +import { Provide } from '@midwayjs/core'; +import { Caching } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + @Caching('default') + async getUser(name: string) { + return name; + } +} + +``` + +When the `getUser` method is called for the first time, the logic will be executed normally and the result will be returned. The decorator will cache the result. When it is executed for the second time, if the cache is not invalidated, it will be returned directly from the cache. + +### Specify cached ttl + +`ttl` can also be set individually. + +```typescript +import { Provide } from '@midwayjs/core'; +import { Caching } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + @Caching('default', 100) + async getUser(name: string) { + return name; + } +} +``` + +### Manually specify cache key + +If you are not satisfied with the automatically generated key, you can manually specify the cached key. + +```typescript +import { Provide } from '@midwayjs/core'; +import { Caching } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + @Caching('default', 'customKey', 100) + async getUser(name: string) { + return name; + } +} +``` + +### Cache with logic + +If you want to cache based on some specific logic, such as specific parameters or specific headers, you can pass a tool function for logical judgment. + +```typescript +import { Provide } from '@midwayjs/core'; +import { Caching } from '@midwayjs/cache-manager'; + +function cacheBy({methodArgs, ctx, target}) { + if (methodArgs[0] === 'harry' || methodArgs[0] === 'mike') { + return 'cache1'; + } +} + +@Provide() +export class UserService { + @Caching('default', cacheBy, 100) + async getUser(name: string) { + return 'hello ' + name; + } +} +``` + +In the above example, the `cacheBy` method customizes the caching logic. When the method input parameter value is `harry` or `mike`, the cached `key` will be returned, while for other parameters, the cache will be skipped. + +The result of execution at this time is: + +```typescript +await userService.getUser('harry')); // hello harry +await userService.getUser('mike')); // hello harry +await userService.getUser('lucy')); // hello lucy +``` + +The `@Caching` decorator can pass a method in the second parameter. The input parameter options of this method are: + +* `methodArgs` The actual parameters of the currently called method +* `ctx` If it is a request scope, it is the context object of the current call. If it is a singleton, the object is an empty object. +* `target` The currently called instance + +The return value of the method is a string or a Boolean value. When a string is returned, it means that the result of the method is cached with the key. When `undefined` or `null` is returned, it means that the cache is skipped. + +By judging these parameters, we can implement very flexible custom caching logic. + + + +## Common problem + + + +### 1. Memory cache set and get cannot obtain the same value under multi-process + +This is a normal phenomenon, the data of each process is independent and is only saved in the current process. If you need cross-process caching, use a distributed cache system like Redis. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/captcha.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/captcha.md new file mode 100644 index 000000000000..e5fead675b2c --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/captcha.md @@ -0,0 +1,288 @@ +# Captcha + +Universal captcha components for `@midwayjs/faas` , `@midwayjs/web` , `@midwayjs/koa` and `@midwayjs/express` frameworks, support `image captcha`, `calculation expression` and other types of verification codes. + +You can also use this component to implement verification capabilities such as `SMS verification code`, `Email verification code`, but note that this component itself does not contain the function of sending SMS messages and emails. + +Related information: + +| Description | | +| ----------------------------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## Install dependencies + +```bash +$ npm i @midwayjs/captcha@3 --save +``` + +Or add the following dependencies in `package.json` and reinstall. + +```json +{ + "dependencies": { + "@midwayjs/captcha": "^3.0.0", + // ... + }, +} +``` + +## Enable components + +Import components in `src/configuration.ts`. + +```typescript +import * as captcha from '@midwayjs/captcha'; + +@Configuration({ + imports: [ + // ...other components + captcha + ], +}) +export class MainConfiguration {} +``` + +## Call service + +```typescript +import { Controller, Inject } from '@midwayjs/core'; +import { CaptchaService } from '@midwayjs/captcha'; + +@Controller('/') +export class HomeController { + @Inject() + ctx; + + @Inject() + captchaService: CaptchaService; + + // Example: Get Image Captcha + @Get('/get-image-captcha') + async getImageCaptcha() { + const { id, imageBase64 } = await this.captchaService.image({ width: 120, height: 40 }); + return { + id, // verification code id + imageBase64, // The base64 data of the captcha SVG image can be directly put into the img tag of the front end + } + } + + // Example: Get Calculation Expression Verification Code + @Get('/get-formula-captcha') + async getFormulaCaptcha() { + const { id, imageBase64 } = await this.captchaService.formula({ noise: 1 }); + return { + id, // verification code id + imageBase64, // The base64 data of the captcha SVG image can be directly put into the img tag of the front end + } + } + + // Verify that the verification code is correct + @Post('/check-captcha') + async getCaptcha() { + const { id, answer } = this.ctx.request.body; + const passed: boolean = await this.captchaService.check(id, answer); + if (passed) { + return 'passed'; + } + return 'error'; + } + + // Example: SMS verification code + @Post('/sms-code') + async sendSMSCode() { + // Verify that the verification code is correct + const { id, text: code } = await this.captchaService.text({ size: 4 }); + await sendSMS(18888888888, code); + return { id } + } + + // Example: Email verification code + @Post('/email-code') + async sendEmailCode() { + // Verify that the verification code is correct + const { id, text: code } = await this.captchaService.text({ type: 'number'}); + await sendEmail('admin@example.com', code); + return { id } + } + + // Example: Stuffing arbitrary text content into a captcha + @Get('/test-text') + async testText() { + // Save the content and get the verification code id + const id: string = await this.captchaService.set('123abc'); + // According to the verification code id, verify whether the content is correct + const passed: boolean = await this.captchaService.check(id, '123abc'); + return { + passed: passed === true, + } + } +} +``` + +## Available configurations + +```typescript +interface CaptchaOptions { + default?: { // default config + // The number of interference lines, the default is 1 + noise?: number; + // width, default is 120px + width?: number; + // width, default is 40px + height?: number; + // Graphic verification code configuration, the graphic contains some characters + }, + image?: { + // Verification code character length, default 4 characters + size?: number; + // The character type in the image verification code, the default is 'mixed' + // - 'mixed' means 0-9, A-Z and a-z + // - 'letter' means A-Z and a-z + // - 'number' means 0-9 + type?: 'mixed', + // The number of interference lines, the default is 1 + noise?: number; + // width, default is 120px + width?: number; + // width, default is 40px + height?: number; + }, + // Calculation formula verification code configuration, for example, the returned image content is 1+2, the user needs to fill in 3 + formula?: { + // The number of interference lines, the default is 1 + noise?: number; + // width, default is 120px + width?: number; + // width, default is 40px + height?: number; + }, + // Plain text verification code configuration, based on the plain text verification code, SMS verification code and email verification code can be implemented + text?: { + // Verification code character length, default 4 characters + size?: number; + // The character type in the image verification code, the default is 'mixed' + // - 'mixed' means 0-9, A-Z and a-z + // - 'letter' means A-Z and a-z + // - 'number' means 0-9 + type?: 'mixed', + }, + // Verification code expiration time, the default is 1h + expirationTime?: 3600, + // key prefix stored in verification code + idPrefix: 'midway:vc', +} + +export const captcha: CaptchaOptions = { + default: { // default config + size: 4, + noise: 1, + width: 120, + height: 40, + }, + image: { // Will merge default configuration + type: 'mixed', + }, + formula: {}, // Will merge default configuration + text: {}, // Will merge default configuration + expirationTime: 3600, + idPrefix: 'midway:vc', +} +``` + +### Configuration Example 1 + +Get an image captcha code containing `5 pure English letters`. The image's width is `200` pixels, the height is `50` pixels, and it contains `3` noise lines. + +Because the configuration of the image captcha code(`image`) will be merged with the `default` configuration, so you can only modify the `default` configuration: + +```typescript +export const captcha: CaptchaOptions = { + default: { + size: 5, + noise: 3, + width: 200, + height: 50 + }, + image: { + type: 'letter' + } +} +``` +Of course, you can also configure the width, height, etc. in the `image` configuration, `without` modifying the default configuration to achieve the `same` effect: +```typescript +export const captcha: CaptchaOptions = { + image: { + size: 5, + noise: 3, + width: 200, + height: 50 + type: 'letter' + } +} +``` + +### Configuration Example 2 + +Get a formula captcha code, which width is `100` pixels, the height is `60` pixels, and it contains `2` noise lines. + +Because the configuration of the formula captcha codewill be merged with the `default` configuration, so you can only modify the `default` configuration: + +```typescript +export const captcha: CaptchaOptions = { + default: { + noise: 2, + width: 100, + height: 60 + }, +} +``` +Of course, you can also configure the width, height, etc. in the `formula` configuration, `without` modifying the default configuration to achieve the `same` effect: + +```typescript +export const captcha: CaptchaOptions = { + formula: { + noise: 2, + width: 100, + height: 60 + } +} +``` + + + +## Component Dependency + +The content storage of the verification code is based on the '@ midwayjs/cache-manager' component. By default, a cache instance named 'captcha' is created and the data is stored in 'memory. + +```typescript +export default { + cacheManager: { + clients: { + captcha: { + store: 'memory', + }, + }, + }, +}; +``` + +If you want to replace it with 'redis' or other services, please refer to the [documentation](/docs/extensions/caching) of `@midwayjs/cache-manager` to configure the cache. + + + +## Effect + +**Picture verification code** + +![图片验证码](https://gw.alicdn.com/imgextra/i4/O1CN014cEzLH23vEniOgoyp_!!6000000007317-2-tps-120-40.png) + +**Calculation expression** + + ![计算表达式](https://gw.alicdn.com/imgextra/i4/O1CN01u3Mj0q24lRx1md9pX_!!6000000007431-2-tps-120-40.png) \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/casbin.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/casbin.md new file mode 100644 index 000000000000..2af96f97679d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/casbin.md @@ -0,0 +1,545 @@ +# Role authentication + +Casbin is a powerful and efficient open source access control framework, and its rights management mechanism supports multiple access control models. + +Official website document: https://casbin.org/ + + + +## What is Casbin + +Casbin can: + +1. supports custom request formats. the default request format is `{subject, object, action}`. +2. It has two core concepts: access control model model and policy policy. +3. Support multi-layer role inheritance in RBAC, not only the main body can have roles, resources can also have roles. +4. Supports built-in superusers such as `root` or `administrator`. Super users can perform any operation without an explicit permission declaration. +5. Supports a variety of built-in operators, such as `keyMatch`, to facilitate the management of path-based resources, such as `/foo/bar`, which can be mapped to `/foo *` + +Casbin cannot: + +1. For authentication authentication (that is, to verify the user's user name and password),Casbin is only responsible for access control. There should be other specialized components responsible for identity authentication, and then Casbin will perform access control. The two are in a coordinated relationship. +2. Manage user lists or role lists. Casbin believes that it is more appropriate to manage the list of users and roles by the project itself. Users usually have their passwords, but Casbin's design idea is not to use it as a container for storing passwords. Instead, it stores the mapping relationship between users and roles in the RBAC scheme. + +:::tip + +Note: + +- 1. Available after Midway v3.6.0 +- 2. Midway only encapsulates the Casbin API and provides simple support. For more information about how to write policy rules, see [Official documentation](https://casbin.org/). +- 3. Casbin does not provide login, but only provides authentication for existing users. It needs to be used with components such as passport to obtain user information. + +::: + + + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## Installation dependency + +```bash +$ npm i @midwayjs/casbin@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/casbin": "^3.0.0", + // ... + }, +} +``` + + + +## Enable components + + +First, introduce components and import them in `configuration.ts`: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as casbin from '@midwayjs/casbin'; +import { join } from 'path' + +@Configuration({ + imports: [ + // ... + casbin + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + + +## Prepare models and strategies + +Before using Casbin, you need to define the model and policy. The contents of these two files run through this article. It is recommended to go to the official website to learn about the relevant contents. + +Let's take a basic model as an example, such: + +``` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _,_ +g2 = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act || r.sub == "root" +``` + +Save it in the `basic_model.conf` file in the project root directory. + +and a policy file containing the following. + +``` +p, superuser, user, read:any +p, manager, user_roles, read:any +p, guest, user, read:own + +g, alice, superuser +g, bob, guest +g, tom, manager + +g2, users_list, user +g2, user_roles, user +g2, user_permissions, user +g2, roles_list, role +g2, role_permissions, role +``` + +Save it in the `basic_policy.csv` file in the project root directory. + + + +## Configure models and policies + +Here our strategy will be demonstrated in file form. + +The configuration is as follows: + +```typescript +import { MidwayAppInfo } from '@midwayjs/core'; +import { join } from 'path'; + +export default (appInfo: MidwayAppInfo) => { + return { + // ... + casbin: { + modelPath: join(appInfo.appDir, 'basic_model.conf') + policyAdapter: join(appInfo.appDir, 'basic_policy.csv') + } + }; +} + +``` + + + +## Authentication by decorator + +There are many forms to use Casbin, here with a decorator as an example. + +### Define resources + +First, define resources, for example, put them in the `src/resource.ts` file, corresponding to the resources in the policy file. + +```typescript +export enum Resource { + USERS_LIST = 'users_list', + USER_ROLES = 'user_roles', + USER_PERMISSIONS = 'user_permissions', + ROLES_LIST = 'roles_list', + ROLE_PERMISSIONS = 'role_permission', +} +``` + + + +### Configure how to obtain users + +When using decorator authentication, we need to configure a way to obtain users. For example, after passport components, we will obtain the user name from `ctx.user`. + +```typescript +import { MidwayAppInfo } from '@midwayjs/core'; +import { join } from 'path'; + +export default (appInfo: MidwayAppInfo) => { + return { + // ... + casbin: { + modelPath: join(appInfo.appDir, 'basic_model.conf') + policyAdapter: join(appInfo.appDir, 'basic_policy.csv') + usernameFromContext: (ctx) => { + return ctx.user; + } + } + }; +} + +``` + + + +### Add guards + +Decorator authentication depends on guards, which can be turned on globally or on some routes. Please refer to the guard section for global guards. + +For example, we only enable authentication on the following `findAllUsers` methods, `AuthGuard` the guards provided by `@midwayjs/casbin`, which can be used directly. + +```typescript +import { Controller, Get, UseGuard } from '@midwayjs/core'; +import { AuthGuard } from '@midwayjs/casbin'; +import { Resource } from './resouce'; + +@Controller('/') +export class HomeController { + + @UseGuard(AuthGuard) + @Get('/users') + async findAllUsers() { + // ... + } +} +``` + + + +### Define permissions + +Use the `UsePermission` decorator to define the permissions required for routing. + +```typescript +import { Controller, Get, UseGuard } from '@midwayjs/core'; +import { AuthActionVerb, AuthGuard, AuthPossession, UsePermission } from '@midwayjs/casbin'; +import { Resource } from './resouce'; + +@Controller('/') +export class HomeController { + + @UseGuard(AuthGuard) + @UsePermission({ + action: AuthActionVerb.READ + resource: Resource.USER_ROLES + possession: AuthPossession.ANY + }) + @Get('/users') + async findAllUsers() { + // ... + } +} +``` + +Users who do not have permission to read `USER_ROLES` cannot call findAllUsers methods and will return 403 status codes when requesting. + +For example, the above `bob` user access will return 403, while the `tom` user access will return normally. + + + +`UsePermission` need to provide an object parameter, including `action`, `resource`, `possession`, and an optional `isOwn` object. + +- `action` is a `AuthActionVerb` enumeration that includes read and write operations. +- `resource` resource string +- `possession` is a `AuthPossession` enumeration +- `IsOwn` is a function that accepts `Context` (the parameter of the guard `canActivate`) as a unique parameter and returns a boolean value. `AuthZGuard` use it to determine whether the user is the owner of the resource. If it is not defined, the default function that returns `false` is used. + +Multiple permissions can be defined at the same time, but the route can only be accessed if all permissions are satisfied. + +For example: + +```typescript +@UsePermissions({ + action: AuthActionVerb.READ + resource: 'USER_ADDRESS', + possession: AuthPossession.ANY +}, { + action; AuthActionVerb.READ + resource: 'USER_ROLES + possession: AuthPossession.ANY +}) +``` + +The route can only be accessed if the user is granted the read `USER_ADDRESS` and `USER_ROLES` permissions. + + + +## API authentication + +Casbin itself provides some common API and permission-related functions. + +We can use it by injecting `CasbinEnforcerService` services directly. + +For example, we can code in guards or middleware. + +```typescript +import { CasbinEnforcerService } from '@midwayjs/casbin'; +import { Guard, IGuard } from '@midwayjs/core'; + +@Guard() +export class UserGuard extends IGuard { + + @Inject() + casbinEnforcerService: CasbinEnforcerService; + + async canActivate(ctx, clz, methodName) { + // If the user is logged in and is a specific method, check the permissions + if (ctx.user && methodName === 'findAllUsers') { + return await this.casbinEnforcerService.enforce(ctx.user, 'USER_ROLES', 'read'); + } + // Unlogged users are not allowed to access + return false; + } +} +``` + +After the guard is enabled, the effect is the same as the decorator above. + +In addition, `CasbinEnforcerService` have more APIs, such as reloading policies. + +```typescript +await this.casbinEnforcerService.loadPolicy(); +``` + + + +## Distributed policy storage + +In scenarios where multiple machines are deployed, policies need to be stored externally. + +Currently implemented adapters are: + +- Redis +- Typeorm + + + +### Redis Adapter + +You need to rely on the `@midwayjs/casbin-redis-adapter` package and Redis components. + +``` +$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save +``` + +enable the redis component. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as redis from '@midwayjs/redis'; +import * as casbin from '@midwayjs/casbin'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + redis + casbin + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + +Configure the Redis connection and casbin adapter. + +```typescript +import { MidwayAppInfo } from '@midwayjs/core'; +import { join } from 'path'; +import { createAdapter } from '@midwayjs/casbin-redis-adapter'; + +export default (appInfo: MidwayAppInfo) => { + return { + // ... + redis: { + clients: { + // Defines a connection for casbin + node-casbin-official ': { + host: '127.0.0.1', + port: 6379 + password: '', + db: '0', + } + } + }, + casbin: { + policyAdapter: createAdapter({ + // The connection name above is configured + clientName: 'node-casbin-official' + }), + // ... + }, + }; +} + +``` + + + +### TypeORM Adapter + +You need to rely on `@midwayjs/casbin-typeorm-adapter` packages and typeorm components. + +``` +$ npm i @midwayjs/casbin-typeorm-adapter @midwayjs/typeorm --save +``` + +Enable typeorm components. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as typeorm from '@midwayjs/typeorm'; +import * as casbin from '@midwayjs/casbin'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + typeorm + casbin + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + +Configure the adapter. Take sqlite storage as an example. You can view the typeorm components for mysql configuration. + +```typescript +import { MidwayAppInfo } from '@midwayjs/core'; +import { join } from 'path'; +import { CasbinRule, createAdapter } from '@midwayjs/casbin-typeorm-adapter'; + +export default (appInfo: MidwayAppInfo) => { + return { + // ... + typeorm: { + dataSource: { + // Defines a connection for casbin + node-casbin-official ': { + type: 'sqlite', + synchronize: true + database: join(appInfo.appDir, 'casbin.sqlite') + // Note that Entity is explicitly introduced here. + entities: [CasbinRule] + } + } + }, + casbin: { + policyAdapter: createAdapter({ + // The connection name above is configured + dataSourceName: 'node-casbin-official' + }), + // ... + } + }; +} +``` + + + +## Monitor + +Use a distributed messaging system such as [etcd](https://github.com/coreos/etcd) to maintain consistency across multiple Casbin executor instances. Therefore, our users can use multiple Casbin executors at the same time to handle a large number of permission checking requests. + +Midway currently only provides one Redis update strategy. If you have other needs, you can submit an issue to us. + +### Redis Watcher + +It needs to depend on `@midwayjs/casbin-redis-adapter` package and redis component. + +```bash +$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save +``` + +Enable the redis component. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as redis from '@midwayjs/redis'; +import * as casbin from '@midwayjs/casbin'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + redis, + casbin, + ], + // ... +}) +export class MainConfiguration { +} +``` + +Example usage: + +```typescript +import { MidwayAppInfo } from '@midwayjs/core'; +import { join } from 'path'; +import { createAdapter, createWatcher } from '@midwayjs/casbin-redis-adapter'; + +export default (appInfo: MidwayAppInfo) => { + return { + // ... + redis: { + clients: { + 'node-casbin-official': { + host: '127.0.0.1', + port: 6379, + db: '0', + }, + 'node-casbin-sub': { + host: '127.0.0.1', + port: 6379, + db: '0', + } + } + }, + casbin: { + // ... + policyAdapter: createAdapter({ + clientName: 'node-casbin-official' + }), + policyWatcher: createWatcher({ + pubClientName: 'node-casbin-official', + subClientName: 'node-casbin-sub', + }) + }, + }; +} +``` + +Note that pub/sub connections require different clients, the code above defines two clients. + +The pub client can be reused with common Redis client connections, while the sub requires an independent client. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cfork.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cfork.md new file mode 100644 index 000000000000..e1aa02607de1 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cfork.md @@ -0,0 +1,62 @@ +# cfork + +Many students have not heard of cfork. cfork library is the library used to start the main process in egg-scripts. it is one of the basic libraries used by egg. its function is to start the process and maintain the preservation of multiple processes. + + +The document is [https:// github.com/node-modules/cfork](https://github.com/node-modules/cfork). + + +Due to the characteristics of bootstrap.js, it is sometimes not very suitable for pm2 deployment (for example, within the group, the global installation is not installed and API startup is required). + + +We can add a `server.js` as the portal for the main process and use `bootstrap.js` as the startup portal for each subprocess. + + +```javascript +// server.js + +'use strict'; + +const cfork = require('cfork'); +const util = require('util'); +const path = require('path'); +const os = require('os'); + +// Get cpu cores +const cpuNumbers = os.cpus().length; + +cfork({ + exec: path.join(__dirname, './bootstrap.js') + count: cpuNumbers +}) + .on('fork', (worker) => { + console.warn('[%s] [worker:%d] new worker start', Date(), worker.process.pid); + }) + .on('disconnect', (worker) => { + console.warn ( + '[%s] [master:%s] wroker:%s disconnect, exitedAfterDisconnect: %s, state: %s .', + Date() + process.pid + worker.process.pid + worker.exitedAfterDisconnect + worker.state + ); + }) + .on('exit', (worker, code, signal) => { + const exitCode = worker.process.exitCode; + const err = new Error ( + util.format ( + 'worker %s died (code: %s, signal: %s, exitedAfterDisconnect: %s, state: %s)', + worker.process.pid + exitCode + signal + worker.exitedAfterDisconnect + worker.state + ) + ); + err.name = 'WorkerDiedError'; + console.error('[%s] [master:%s] wroker exit: %s', Date(), process.pid, err.stack); + }); +``` + +Finally, start `node server.js`. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/code_dye.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/code_dye.md new file mode 100644 index 000000000000..4bdcdb4ad72d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/code_dye.md @@ -0,0 +1,134 @@ +# Code dyeing + +Code dyeing components for `@midwayjs/faas`, `@midwayjs/web`, `@midwayjs/koa` and `@midwayjs/express` frameworks. + +It is used to display the time-consuming call link and the input and output parameters of each method in the HTTP scenario, helping you locate code problems faster. + +for example: + ++ code execution is slow + - I don’t know which method executes slowly: After code dyeing, you can check the execution time of each `method`. ++ code execution error + - It may be that the method is not transferred: after code dyeing, you can view each `method call chain`. + - It may be that the method call parameter is wrong: After the code is dyed, check the input parameter and return value of each `method` + +Effect: + +![](https://gw.alicdn.com/imgextra/i1/O1CN017Zd6y628M2PvqJO7I_!!6000000007917-2-tps-2392-844.png) + + + + +Related Information: + +| web support | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + + + +## Install dependencies + +```bash +$ npm i @midwayjs/code-dye@3 --save +``` + +Or add the following dependencies in `package.json` and reinstall. + +```json +{ + "dependencies": { + "@midwayjs/code-dye": "^3.0.0" + //... + }, +} +``` + + + +## Enable component + +Configure the code-dye component into the code. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as codeDye from '@midwayjs/code-dye'; + +@Configuration({ + imports: [ + //... + { + component: codeDye, + enabledEnvironment: ['local'], // only enabled locally + } + ], +}) +export class MainConfiguration {} +``` + + + + + +:::tip + +- You can enable this component in the `local` or `development` environment, which is convenient for locating problems during development, but it is `not recommended` to enable it online, which will affect the performance of online access. + +::: + + + +## Configure coloring + +You can use `matchQueryKey` configuration to control when the `query` parameter contains the value corresponding to `matchQueryKey` configuration, to enter the dye link, for example, the configuration is: + +```typescript +// src/config/config.local.ts +export default { + codeDye: { + matchQueryKey: 'codeDyeABC', + } +} +``` + +When requesting the interface `http://127.0.0.1:7001/test?codeDyeABC=html`, it will judge whether `codeDyeABC` parameter exists in `query` to decide whether to dye or not, and respond differently according to the corresponding value of the parameter the staining results. + +You can also use the `matchHeaderKey` configuration to control when the `headers` parameter contains the value corresponding to the `matchHeaderKey` configuration, to enter the dye link. For example, the configuration is: + +```typescript +// src/config/config.local.ts +export default { + codeDye: { + matchHeaderKey: 'codeDyeHeader', + } +} +``` + +When requesting the interface `http://127.0.0.1:7001/test`, it will judge whether there is a `codeDyeHeader` parameter in the `headers` of the request to decide whether to dye, and respond to different dyeing according to the corresponding value of the parameter result. + + + +## Dye report + +After code coloring is enabled, the result of link coloring can be configured by enabling different parameter values for coloring. Currently, the following three types are supported: + ++ `html`: `process` the result of the current request, add the coloring information to the result, and the response is `html`, which can be viewed on the browser, and the effect can be viewed in the picture above this document. ++ `json`: `process` the result of the current request, add coloring information to the result, and respond as `json` structured information. ++ `log`: `Do not process` the result of the current request, and the dyed information will be output to the log without affecting the request. + +For example, configured as: + +```typescript +// src/config/config.local.ts +export default { + codeDye: { + matchQueryKey: 'codeDyeXXX', + } +} +``` + +When the interface `http://127.0.0.1:7001/test?codeDyeXXX=html` is requested, it will be judged that the `codeDyeXXX` parameter value in `query` is `html`, and the dyeing result will be output in the response of the current request , and the content is in `html` format. \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/consul.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/consul.md new file mode 100644 index 000000000000..dae58b035651 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/consul.md @@ -0,0 +1,367 @@ +# Consul + +consul is used for service governance under microservices. Its main features include service discovery, service configuration, health check, key value storage, secure service communication, and multiple data centers. + +This article describes how to use consul as the service registration discovery center of midway and how to use consul to do soft load functions. + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ❌ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +Thank you for the components provided by [boostbob](https://github.com/boostbob). + + +The effect is as follows: +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01e5cFZx1I0draeZynr_!!6000000000831-2-tps-1500-471.png) + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01iLYF8r1HQ0B3b47Fh_!!6000000000751-2-tps-1500-895.png) + + +## Installation Components + +First install consul components and types: + +```bash +$ npm i @midwayjs/consul@3 --save +$ npm i @types/consul --save-dev +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/consul": "^3.0.0", + // ... + }, + "devDependencies": { + "@types/consul": "^0.40.0 ", + // ... + } +} +``` + + + +## Capacity currently supported + +- Registration capability (optional) +- Anti-registration when service is stopped (optional) +- Service Selection (Random) +- Expose the original consul object + + + +## Enable components + +```typescript +import * as consul from '@midwayjs/consul' + +@Configuration({ + imports: [ + // .. + consul + ], + importConfigs: [join(__dirname, 'config')] +}) +export class MainConfiguration {} +``` + + + +## Configuration + +Configure the `config.default.ts` file: + +```typescript +// src/config/config.default +export default { + // ... + consul: { + provider: { + // Register for this service + register: true + // Apply normal offline anti-registration + deregister: true + // consul server service address + host: '192.168.0.10', + // consul server service port + port: '8500', + // Policy for invoking the service (random is selected by default) + strategy: 'random', + }, + service: { + // This is the address of the current midway application. + address: '127.0.0.1', + // The port of the current midway application + port: 7001, + // Use for lane isolation, etc. + tags: ['tag1', 'tag2'], + name: 'my-midway-project' + // others consul service definition + } + }, +} +``` + +Open the ui address of our consul server with the following effect: + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01QI7A1d1dU3ECG8QxQ_!!6000000003738-2-tps-1500-471.png) + +It can be observed that my-midway-project project has been registered. + +If we stop our midway project. + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01EDocUO1TIvRvpxXbw_!!6000000002360-2-tps-1500-401.png) + +We can see that the status of our project turns red. + +We demonstrate the situation of multiple units, as follows:(1 online +1 offline) + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01kfmul91eSxu5EiJE3_!!6000000003871-2-tps-1500-405.png) + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01PZrdpp21Sir5n3y9I_!!6000000006984-2-tps-1500-360.png) + + + +## As a client + +For example, as client A, we need to call the interface of service B. Then we first find out the healthy service of B and then make an http request. + + +Here, for the convenience of understanding, we simulate the successful service that has just been registered: +```typescript +import { Controller, Get, Inject, Provide } from '@midwayjs/core'; +import { BalancerService } from '@midwayjs/consul' + +@Provide() +@Controller('/') +export class HomeController { + + @Inject() + balancerService: BalancerService; + + @Get('/') + async home() { + const service = await this.balancerService.getServiceBalancer().select('my-midway-project'); + + // output + console.log(service) + + // ... + } +} + +``` +The content of the output service is: +```typescript +{ + ID: 'c434e36b-1b62-c4e1-c4ec-76c5d3742ff8', + Node: '1b2d5b8771cb', + Address: '127.0.0.1', + Datacenter: 'dc1', + TaggedAddresses: { + lan: '127.0.0.1', + lan_ipv4: '127.0.0.1', + wan: '127.0.0.1', + wan_ipv4: '127.0.0.1' + }, + NodeMeta: { 'consul-network-segment': '' }, + ServiceKind: '', + ServiceID: 'my-midway-project:xxx:7001', + ServiceName: 'my-midway-project', + ServiceTags: [ 'tag1', 'tag2' ], + ServiceAddress: 'xxxxx', + ServiceTaggedAddresses: { + lan_ipv4: { Address: 'xxxxx', Port: 7001 }, + wan_ipv4: { Address: 'xxxxxx', Port: 7001 } + }, + ServiceWeights: { Passing: 1, Warning: 1 }, + ServiceMeta: {}, + ServicePort: 7001, + ServiceEnableTagOverride: false, + ServiceProxy: { MeshGateway: {}, Expose: {} }, + ServiceConnect: {}, + CreateIndex: 14, + ModifyIndex: 14 +} +``` +At this time, we only need to connect to service B through Address and ServicePort, such as making http requests. + + +If you need to query for unhealthy ones, the second parameter of the `select` method is passed the value of false: +```typescript +import { Controller, Get, Inject, Provide } from '@midwayjs/core'; +import { BalancerService } from '@midwayjs/consul' + +@Provide() +@Controller('/') +export class HomeController { + + @Inject() + balancerService: BalancerService; + + @Get('/') + async home() { + + const service = await this.balancerService + .getServiceBalancer() + .select('my-midway-project', false); + + console.log(service); + + // ... + } +} + +``` + + + +## Configuration center + + +At the same time, consul can also be used as a service configuration place, as follows: +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; +import * as Consul from 'consul'; + +@Controller('/') +export class HomeController { + + @Inject('consul:consul') + consul: Consul.Consul; + + @Get('/') + async home() { + await this.consul.kv.set(`name`, `juhai`) + // let res = await this.consul.kv.get('name'); + // console.log(res); + return 'Hello Midwayjs!'; + } +} + +``` +You can call the `kv.set` method to set the corresponding configuration. You can use the `kv.get` method to obtain the corresponding configuration. + + +Note: In the code, some students appear and get the corresponding configuration in each request. How much pressure does your QPS put on Consul server. + + +Therefore, in the case of large QPS, it can be handled as follows: +```typescript +import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; +import * as Consul from 'consul'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class ConfigService { + + @Inject('consul:consul') + consul: Consul.Consul; + + config: any; + + @Init() + async init() { + setInterval(()=> { + this.consul.kv.get('name').then(res=> { + this.config = res; + }) + }, 5000); + this.config = await this.consul.kv.get('name'); + } + + async getConfig() { + return this.config; + } +} + +``` +The above code is equivalent to obtaining the corresponding configuration regularly. When each request comes in, the `getConfig` method of obtaining Scope as the ScopeEnum.Singleton service is obtained. This reduces the pressure on the service every 5S. + +The following figure on the Consul interface: + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01V3P6uK1rIVs19JiWn_!!6000000005608-2-tps-1500-374.png) + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN014O2GyH1sMvIhmlbs4_!!6000000005753-2-tps-1500-667.png) + + +A total of the following methods are provided: + +- [get](https://www.npmjs.com/package/consul#kv-get) to obtain the value of the key. +- [keys](https://www.npmjs.com/package/consul#kv-keys): queries the key list of a prefix. +- [set](https://www.npmjs.com/package/consul#kv-set) to set the value of the key. +- [del](https://www.npmjs.com/package/consul#kv-del): deletes the key. + + + +## Other instructions + + +The advantage of this is that A->B, B can also be extended, and can be isolated by tags. For example, do unit isolation, etc. And the corresponding weight control can be done through ServiceWeights. + + +Consul can also function as the configuration center of Key/Value. We will consider supporting this later. + + +## Building Consul Test Service + + +The following describes the construction process of the stand-alone version of consul. +```bash +docker run -itd -P consul +``` +Then run the `docker ps` +```bash +➜ my_consul_app docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +1b2d5b8771cb consul "docker-entrypoint.s…" 4 seconds ago Up 2 seconds 0.0.0.0:32782->8300/tcp, 0.0.0.0:32776->8301/udp, 0.0.0.0:32781->8301/tcp, 0.0.0.0:32775->8302/udp, 0.0.0.0:32780->8302/tcp, 0.0.0.0:32779->8500/tcp, 0.0.0.0:32774->8600/udp, 0.0.0.0:32778->8600/tcp cocky_wing +``` +Then we open the port corresponding to the 8500: (for example, in the above figure, my corresponding port is 32779) + +[http://127.0.0.1:32779/ui/](http://127.0.0.1:32779/ui/dc1/kv) + +After opening, the effect is as follows: + +![](https://img.alicdn.com/imgextra/i2/O1CN014O2GyH1sMvIhmlbs4_!!6000000005753-2-tps-1500-667.png) + +Then the port in our `config.default.ts` is the 32779 port. + + + +## Offline service +If you want to manually offline services that are not needed on the consul interface, you can use the following methods: +```typescript +import { Controller, Get, Inject, Provide } from '@midwayjs/core'; +import * as Consul from 'consul' + +@Provide() +@Controller('/') +export class HomeController { + + @Inject('consul:consul') + consul: Consul.Consul; + + @Get("/222") + async home2() { + let res = await this.consul.agent.service.deregister('my-midway-project:30.10.72.195:7002'); + console.log(res); + + // ... + } + +} + +``` +The `deregister` method corresponds to the name on the consul interface. + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01d5QMUJ1DULTKPSJsr_!!6000000000219-2-tps-1500-465.png) diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cos.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cos.md new file mode 100644 index 000000000000..6cf792db84ad --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cos.md @@ -0,0 +1,149 @@ +# Tencent Cloud Object Storage (COS) + +This article describes how to use midway to access Tencent Cloud COS. + +Related information: + +| Description | | +| ----------------- | --- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## Installation dependency + +```bash +$ npm i @midwayjs/cos@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/cos": "^3.0.0", + // ... + }, +} +``` + + + +## Introducing components + + +First, introduce components and import them in `configuration.ts`: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as cos from '@midwayjs/cos'; +import { join } from 'path' + +@Configuration({ + imports: [ + // ... + cos // import cos components + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + +## Configuration + +For example: + + +**Single-client configuration** +```typescript +// src/config/config.default +export default { + // ... + cos: { + client: { + SecretId: '***********', + SecretKey: '***********', + }, + }, +} +``` + + +**Configure multiple clients.** + +```typescript +// src/config/config.default +export default { + // ... + cos: { + clients: { + instance1: { + SecretId: '***********', + SecretKey: '***********', + }, + instance2: { + SecretId: '***********', + SecretKey: '***********', + }, + }, + }, +} +``` +For more parameters, see the [cos-nodejs-sdk-v5](https://github.com/tencentyun/cos-nodejs-sdk-v5) document. + + +## Use COS service + + +We can inject it into any code. +```typescript +import { Provide, Controller, Inject, Get } from '@midwayjs/core'; +import { COSService } from '@midwayjs/cos'; + +@Provide() +export class UserService { + + @Inject() + cosService: COSService; + + async invoke() { + await this.cosService.sliceUploadFile({ + Bucket: 'test-1250000000', + Region: 'ap-guangzhou', + Key: '1.zip', + FilePath: './1.zip' + }, + } +} +``` + + +You can use `COSServiceFactory` to get different instances. +```typescript +import { COSServiceFactory } from '@midwayjs/cos'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + cosServiceFactory: COSServiceFactory; + + async save() { + const cos1 = await this.cosServiceFactory.get('instance1'); + const cos2 = await this.cosServiceFactory.get('instance3'); + + //... + + } +} +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cron.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cron.md new file mode 100644 index 000000000000..1b11438d1050 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cron.md @@ -0,0 +1,288 @@ +# Local task + +Unlike the bull component, the cron component provides local task capabilities, that is, every process on every machine will execute. If you need to execute tasks only once between different machines or different processes, please use [bull component](./bull) . + + + +Related Information: + +| Description | | +| ------------------------------- | ---- | +| Available for Standard Items | ✅ | +| Available for Serverless | ❌ | +| Can be used for integration | ✅ | +| Contains independent main frame | ✅ | +| Contains standalone logs | ✅ | + + + +## Install components + +```bash +$ npm i @midwayjs/cron@3 --save +``` + +Or add the following dependencies in `package.json` and reinstall. + +```json +{ + "dependencies": { + "@midwayjs/cron": "^3.0.0", + //... + }, +} +``` + + + +## Using components + +Configure components into code. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as cron from '@midwayjs/cron'; + +@Configuration({ + imports: [ + //... + cron + ] +}) +export class MainConfiguration { + //... +} +``` + + + +## Write task processing class + +Decorate a class with the `@Job` decorator to quickly define a job handler. + +For example, create a `sync.job.ts` in the `src/job` directory for some data synchronization tasks, the code is as follows: + +```typescript +// src/job/sync.job.ts +import { Job, IJob } from '@midwayjs/cron'; +import { FORMAT } from '@midwayjs/core'; + +@Job({ + cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE, + start: true, +}) +export class DataSyncCheckerJob implements IJob { + async onTick() { + //... + } +} +``` + +The `@Job` decorator is used to decorate a task class, and the framework will automatically convert it into a task when it is initialized. + +The task class needs to implement the `IJob` interface and implement the `onTick` method. Whenever the task is triggered, the `onTick` method will be called automatically. + +Additionally, there is an optional `onComplete` method to be executed after `onTick` has completed. + +```typescript +@Job({ + cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE, + start: true, +}) +export class DataSyncCheckerJob implements IJob { + async onTick() { + //... + } + + async onComplete() { + // Record some data, etc., not very useful + } +} +``` + + + +Common parameters of the `@Job` decorator are as follows: + +| Parameter | Type | Description | +| --------- | ------- | ----------------------------------------- | +| cronTime | string | crontab expression | +| start | boolean | Whether to automatically start the task | +| runOnInit | boolean | Whether to execute once at initialization | + +For more parameters, please refer to [Cron](https://github.com/kelektiv/node-cron). + + + +## Task Management + +In addition to timing tasks, we also manually manage tasks through the API provided by the framework. + +For example, the following code only defines a task, but does not start execution. + +```typescript +@Job('syncJob', { + cronTime: '*/2 * * * * *', // execute every 2s +}) +export class DataSyncCheckerJob implements IJob { + async onTick() { + //... + } +} +``` + +We define a job called `syncJob` and give it a default schedule. + + + +### Get task object + +We can get the task object in two ways. + +It is used to inject a task through `@InjectJob`, and the parameter is the class itself or the task name. + +```typescript +// src/configuration.ts +import { Configuration, Inject } from '@midwayjs/core'; +import * as cron from '@midwayjs/cron'; +import { InjectJob, CronJob } from '@midwayjs/cron'; +import { DataSyncCheckerJob } from './job/sync.job'; + +@Configuration({ + imports: [ + cron + ], +}) +export class ContainerConfiguration { + @InjectJob(DataSyncCheckerJob) + syncJob: CronJob; + + @InjectJob('syncJob') + syncJob2: CronJob; + + async onServerReady() { + // this.syncJob === this.syncJob2 + } +} + +``` + +Obtained through the Framework API. + +```typescript +// src/configuration.ts +import { Configuration, Inject } from '@midwayjs/core'; +import * as cron from '@midwayjs/cron'; +import { InjectJob, CronJob } from '@midwayjs/cron'; +import { DataSyncCheckerJob } from './job/sync.job'; + +@Configuration({ + imports: [ + cron + ], +}) +export class ContainerConfiguration { + @Inject() + cronFramework: cron.Framework; + + async onServerReady() { + const syncJob = this.cronFramework.getJob(DataSyncCheckerJob); + const syncJob2 = this.cronFramework.getJob('syncJob'); + + // syncJob === syncJob2 + } +} + +``` + +:::caution + +Note that the task object must be obtained after the `onServerReady` life cycle or startup. + +::: + + + +### Start and stop tasks + +We can start this task after initialization or after some program execution is completed. + +```typescript +// src/configuration.ts +import { Configuration, Inject } from '@midwayjs/core'; +import * as cron from '@midwayjs/cron'; +import { InjectJob, CronJob } from '@midwayjs/cron'; +import { DataSyncCheckerJob } from './job/sync.job'; + +@Configuration({ + imports: [ + cron + ], +}) +export class ContainerConfiguration { + @InjectJob(DataSyncCheckerJob) + syncJob: CronJob; + + async onServerReady() { + this.syncJob.start(); + + //... + this.syncJob.stop(); + } +} + +``` + + + +## context + +Task execution is in request scope, which has a special Context object structure. + +```typescript +export interface Context extends IMidwayContext { + job: CronJob; +} +``` + +The `CronJob` type here comes from the `node-cron` package. + + + +## Component logger + +By default, the `ctx.logger` will be recorded in `midway-cron.log`. + +We can configure this logger object individually. + +```typescript +export default { + midwayLogger: { + //... + clients: { + //... + cronLogger: { + fileLogName: 'midway-cron.log', + }, + } + } +} +``` + + + +## Global configuration + +Some global configurations can be made for the Job, which will be merged with the configuration of each Job. + +```typescript +export default { + cron: { + defaultCronJobOptions: { + //... + } + } +} +``` + +Please refer to [CronJobParameters](https://github.com/kelektiv/node-cron/blob/main/lib/job.js#L51) for `defaultCronJobOptions` configuration items here \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cross_domain.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cross_domain.md new file mode 100644 index 000000000000..0ee03cee720c --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/cross_domain.md @@ -0,0 +1,196 @@ +# Cross-domain + +Common cross-domain components are applicable to `@midwayjs/faas`, `@midwayjs/web`, `@midwayjs/koa`, and `@midwayjs/express` frameworks, and support multiple modes of `cors` and `jsonp`. + +Related information: + +| Web support | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + + + +## Installation dependency + +```bash +$ npm i @midwayjs/cross-domain --save +``` + +## Introducing components + +Introducing components in `src/configuration.ts`. + +```typescript +import * as crossDomain from '@midwayjs/cross-domain'; +@Configuration({ + imports: [ + // ...other components + crossDomain + ], +}) +export class MainConfiguration {} +``` + + + +## What is Cross-Origin + +Suppose there are two websites: + +- **A.com**: This is your website, where you want to access some resources. +- **B.com**: This is another website that possesses the resources you wish to access. + +### Scenario Setup + +1. **A.com** is your main website, where you run some JavaScript code. +2. **B.com** has some API interfaces that you want to call through JavaScript code on A.com. + +Due to the same-origin policy, browsers do not allow JavaScript code from A.com to directly access resources from B.com by default. This is because browsers, for security reasons, prevent malicious websites from reading sensitive data from other sites. You want to initiate a request to B.com's API interface from JavaScript code running on A.com, and this request is considered a cross-origin request. + +In simple terms, accessing B.com's interface from A.com is cross-origin. + +Additionally, there are some conditions for cross-origin: + +* 1. The same-origin policy is part of the browser's security mechanism, so cross-origin issues generally only arise when accessing from a browser. +* 2. When a browser initiates a cross-origin request, it automatically adds an `Origin` header to the request based on the current origin, and the server also judges the source based on the `origin` header and processes accordingly. + +So, when you find that cross-origin settings are not effective, please check: + +* 1. Whether it is indeed a cross-origin request. +* 2. Whether it is indeed initiated from a browser. +* 3. Whether the request carries an `origin` header. + + + +## What is CORS + +The following errors are often encountered when front-end code calls back-end services. This is the most common cross-domain [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) error. . + +``` +Access to fetch at 'http://127.0.0.1:7002/' from origin 'http://127.0.0.1:7001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. +``` + +For security reasons, browsers restrict cross-origin HTTP requests made within scripts. For example, `XMLHttpRequest` and [Fetch API](https://developer.mozilla.org/en-CN/docs/Web/API/Fetch_API) follow the [Same Origin Policy](https://developer.mozilla.org/ en-CN/docs/Web/Security/Same-origin_policy). This means that web applications using these APIs can only request HTTP resources from the same domain where the application is loaded unless the response message contains the correct CORS response headers. + +The CORS mechanism allows web application servers to perform cross-origin access control so that cross-origin data transmission can be carried out securely. Modern browsers support this in API containers such as [`XMLHttpRequest`](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) or [Fetch](https://developer.mozilla .org/zh-CN/docs/Web/API/Fetch_API)) uses CORS to reduce the risks caused by cross-origin HTTP requests. + + + +## Common CORS configurations + +Below are several cross-domain solutions. Let’s take the `fetch` method as an example. + +### `credentials` not used + +client. + +```javascript +fetch(url); +``` + +Server configuration. + +```typescript +// src/config/config.default.ts +export default { + // ... + cors: { + origin: '*', + }, +} +``` + +### Using `credentials` + +client. + +```javascript +fetch(url, { + credentials: "include", +}); +``` + +Server configuration + +```typescript +// src/config/config.default.ts +export default { + // ... + cors: { + credentials: true, + }, +} +``` + +### Limit `origin` + +Suppose our web page address is `http://127.0.0.1:7001` and the interface is `http://127.0.0.1:7002`. + +client. + +```javascript +fetch('http://127.0.0.1:7002/', { + credentials: 'include' +}) +``` + +Server configuration, please note that since `credentials` is enabled, the `origin` field cannot be `*` at this time. + +```typescript +// src/config/config.default.ts +export default { + // ... + cors: { + origin: 'http://127.0.0.1:7001', + credentials: true, + }, +} +``` + + + +## More CORS configuration + +The available configurations are as follows: + +```typescript +export const cors = { + // Allow cross-domain methods, [default value] is GET, HEAD, PUT, POST, DELETE, PATCH + allowMethods: string |string[]; + // Set the value of Access-Control-Allow-Origin, and [Default] will get the origin on the request header + // It can also be configured as a callback method. The input parameter is request and the origin value needs to be returned. + // For example: http://test.midwayjs.org + // If credentials is set, origin cannot be set * + origin: string|Function; + // Set the value of Access-Control-Allow-Headers, [Default] will get Access-Control-Request-Headers on the request head + allowHeaders: string |string[]; + // Set the value of Access-Control-Expose-Headers + exposeHeaders: string |string[]; + // Set Access-Control-Allow-Credentials, [Default] false + // It can also be configured as a callback method. The input parameter is request and the return value is true or false. + credentials: boolean|Function; + // Whether to write cross-domain header information to the headers attribute of the error pair when an error is reported, [default value] false + keepHeadersOnError: boolean; + // set Access-Control-Max-Age + maxAge: number; +} +``` + + +## JSONP configuration + +JSONP can be configured in `src/config/config.default`. + +```typescript +// src/config/config.default.ts +export default { + // ... + jsonp: { + callback: 'jsonp', + limit: 512 + }, +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/egg.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/egg.md new file mode 100644 index 000000000000..9868afaabf55 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/egg.md @@ -0,0 +1,859 @@ +# EggJS + +Midway can use EggJS as the upper-level Web framework. EggJS provides many commonly used plug-ins and API to help users quickly build enterprise-level Web applications. This chapter mainly introduces how EggJS uses its own capabilities in Midway. + +| Description | | +| ----------------- | ---- | +| Contains independent main framework | ✅ | +| Contains independent logs | ✅ | + + + +## Installation dependency + +```bash +$ npm i @midwayjs/web@3 egg --save +$ npm i @midwayjs/egg-ts-helper --save-dev +``` + +For the EggJS scenario, these packages are listed below. + +```json + "dependencies": { + "@midwayjs/web": "^3.0.0", + "@midwayjs/core": "^3.0.0", + "egg": "^2.0.0 ", + "egg-scripts": "^2.10.0" + }, + "devDependencies": { + "@midwayjs/egg-ts-helper": "^1.0.1 ", + }, +``` + +| @midwayjs/web | **Required** ,Midway EggJS adaptation layer | +| ----------------------- |-----------------------------------------------------------------------------------| +| @midwayjs/core | **Required** ,Midway core package | +| egg | **Required** ,EggJS dependent package, and other capabilities such as definition. | +| egg-scripts | **Optional** ,EggJS startup script | +| @midwayjs/egg-ts-helper | **Optional** ,EggJS defines the generation tool. | + +Examples can also be created directly using scaffolding. + +```bash +# npm v6 +$ npm init midway --type=egg-v3 my_project + +# npm v7 +$ npm init midway -- --type=egg-v3 my_project +``` + + + +## Open the component + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as web from '@midwayjs/web'; +import { join } from 'path'; + +@Configuration({ + imports: [web] + importConfigs: [join(__dirname, './config')] +}) +export class MainConfiguration { + @App() + app: web.Application; + + async onReady() { + // ... + } +} + +``` + + + +## The difference from the default EggJS + + +- 1. starting from v3, midway provides more components, and most egg built-in plug-ins are disabled by default +- 2. The baseDir is adjusted to `src` directory by default, and the server is `dist` directory. +- 3. disable egg-logger, replace all with @midwayjs/logger, and cannot switch + + + +The entire architecture is as follows: +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1614842824740-fc0c1432-3ace-4f77-b51f-15212984b168.png) + + +## Directory structure + + +In addition to the directory structure provided by Midway, EggJS also has some special directory structures (immutable). The entire structure is as follows. +``` +➜ my_midway_app tree +. +├── src +| ├── app.ts ## EggJS Extended Worker Lifecycle File (optional) +| ├── agent.ts ## EggJS Extended Agent Lifecycle File (Optional) +| ├── app ## EggJS fixed root directory (optional) +| │ ├── public ## The default directory for EggJS static hosting plug-ins (available) +| │ | └── reset.css +| │ ├── view (optional) ## ## The default directory for EggJS template rendering (available) +| │ | └── home.tpl +| │ └── extend (optional) ## EggJS 扩展目录(可配) +| │ ├── helper.ts (optional) +| │ ├── request.ts (optional) +| │ ├── response.ts (optional) +| │ ├── context.ts (optional) +| │ ├── application.ts (optional) +| │ └── agent.ts (optional) +| │ +| ├── config +| | ├── plugin.ts +| | ├── config.default.ts +| │ ├── config.prod.ts +| | ├── config.test.ts (可选) +| | ├── config.local.ts (可选) +| | └── config.unittest.ts (可选) +│ ├── controller ## Midway controller directory (recommended) +│ ├── service ## Midway service directory (recommended) +│ └── schedule +│ +├── typings ## EggJS defines the generation directory. +├── test +├── package.json +└── tsconfig.json +``` +The above is a complete picture of the directory structure of EggJS, which contains many specific directories of EggJS, some of which have been replaced by corresponding capabilities in the Midway system and can be replaced directly. The entire structure is basically equivalent to moving the directory structure of EggJS to the `src` directory. + + +Since EggJS is a convention-based framework, the directory structure of the entire project is fixed. Here are some commonly used convention directories. + +| `src/app/public/**` | Optional. For more information, see [egg-static](https://github.com/eggjs/egg-static). | +| --- | --- | +| `src/config/config.{env}.ts` | For more information about how to write a configuration file, see [Configuration](https://eggjs.org/zh-cn/basics/config.html). | +| `src/config/plugin.js` | For more information, see [Plug-ins](https://eggjs.org/zh-cn/basics/plugin.html). | +| `test/**` | For more information, see [Unit testing](https://eggjs.org/zh-cn/core/unittest.html). | +| `src/app.js` and `src/agent.js` | It is used to customize initialization during startup. Optional. For more information, see [Start customization](https://eggjs.org/zh-cn/basics/app-start.html). For more information about the role of `agent.js`, see [Agent mechanism](https://eggjs.org/zh-cn/core/cluster-and-ipc.html#agent-%E6%9C%BA%E5%88% B6). | + + + +## Configuration Definition + + +Midway provides the standard TS configuration writing of EggJS in the scaffold. The MidwayConfig includes the definition and attribute tips of the configuration in egg. The structure is as follows. +```typescript +// src/config/config.default.ts +import { MidwayConfig, MidwayAppInfo } from '@midwayjs/core'; + +export default (appInfo: MidwayAppInfo) => { + return { + // use for cookie sign key, should change to your own and keep security + keys: appInfo.name + '_xxxx', + egg: { + port: 7001 + }, + // security: { + // csrf: false + // }, + } as MidwayConfig; +}; + +``` +In the form of this return method, it will be automatically executed during the run-time and merged into the complete configuration object. + + +The parameter of this function is of `MidwayAppConfig` type and the value is the following. + +| **appInfo** | **Description** | +| --- | --- | +| pkg | package.json | +| name | Application name, same as pkg.name | +| baseDir | The src (locally developed) or Dist (after online) Directory of the application code | +| appDir | Directory of application code | +| HOME | The user directory, for example, the admin account is/home/admin. | +| root | The root directory of the application is only baseDir in local and unittest environments, and the others are HOME. | + + + +:::info +Note that the `baseDir` here is different from the `appDir` and EggJS applications. +::: + + + + +## Using Egg plugin + +Plugin are one of EggJS's features. `@midwayjs/web` also supports EggJS's plug-in system, but in the case of Midway components, Midway components are used as much as possible. + + +Plug-ins are generally reused by npm modules. +```bash +$ npm i egg-mysql --save +``` +Then, you must declare that it is enabled in the `src/config/plugin.js` of the application or framework. + + +If there is an `export default`, please write it in it. +```typescript +import { EggPlugin } from 'egg'; +export default { + static: false, // default is true + mysql: { + enable: true + package: 'egg-mysql' + } +} as EggPlugin; + +``` +If there is no `export default`, you can export it directly. +```typescript +// src/config/plugin.ts +// Use mysql plug-in +export const mysql = { + enable: true + package: 'egg-mysql', +}; +``` + + +After opening the plug-in, we can use the functions provided by the plug-in in the business code. Generally, the plug-in mounts the object to the `app` and `ctx` of EggJS, and then uses it directly. + + +```typescript +app.mysql.query(sql, values); // Methods provided by egg +``` +In Midway, you can use `@App` to obtain the `app` object, and in the request scope, you can use `@Inject() ctx` to obtain the `ctx` object, so you can obtain the plug-in object by injection. + + +```typescript +import { Provide, Inject, Get } from '@midwayjs/core'; +import { Application, Context } from '@midwayjs/web'; + +@Provide() +export class HomeController { + + @App() + app: Application; + + @Inject() + ctx: Context; + + @Get('/') + async home() { + This. app.mysql.query (SQL, values); // Call methods on app (if any) + This. ctx.mysql.query (SQL, values); // Call the method mounted on ctx (if any) + } +} +``` +In addition, you can directly inject plugins mounted by `app` through the `@Plugin` decorator. By default, if no parameters are passed, the property name will be used as the key. + + +```typescript +import { Provide, Get, Plugin } from '@midwayjs/core'; + +@Provide() +export class HomeController { + + @Plugin() + mysql: any; + + @Get('/') + async home() { + this.mysql.query( SQL, values); + } +} +``` +:::info +`@Plugin() mysql` is equivalent to `app.mysql`. The function of `@Plugin` is to take the plug-in corresponding to the attribute name from the app object. Therefore, `@Plugin() xxx` is equal to `app['xxx']`. +::: + + +## Web middleware + + +The middleware sample is as follows: + + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { Context, NextFunction } from '@midwayjs/web'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + const startTime = Date.now(); + await next(); + console.log(Date.now() - startTime); + }; + } + +} +``` + +:::caution +Notice + +1. If you want to continue to use the traditional functional writing method of EggJS, you must put the file under `src/app/middleware` + +2. The built-in middleware that comes with egg has been integrated + +::: + +Application Middleware. + +```typescript +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as egg from '@midwayjs/web'; +import { ReportMiddleware } from './middleware/user.middleware'; + +@Configuration({ + imports: [egg] + // ... +}) +export class MainConfiguration { + + @App() + app: egg.Application; + + async onReady() { + this.app.useMiddleware(ReportMiddleware); + } +} + +``` + +For more information, see [Web middleware](../middleware). + + + +## Middleware sequence + +Since egg also has its own middleware logic, in the new version, we have done a certain processing of the middleware loading sequence, and the execution sequence is as follows: + +- 1. middleware in the egg framework +- 2. the order in which egg plug-ins are added through config.coreMiddleware +- 3. The order in which the business code is configured in config.middleware +- 4. the order in which App. useMiddleware is added + +Because midway's middleware will be post-loaded, we can customize sorting in the onReady. + + + +## BodyParser + +the `bodyParser` feature of the egg. by default, it parses `post` requests and automatically identifies the `json` and `form` types. + +If you need text or xml, you can configure it yourself. + +The default size is limited to `1mb`. You can set the size of each item separately. + +```typescript +// src/config/config.default +export default { + // ... + bodyParser: { + formLimit: '1mb', + jsonLimit: '1mb', + textLimit: '1mb', + xmlLimit: '1mb', + }, +} +``` + +Note that the type selection when using Postman for Post requests: + +![postman](https://img.alicdn.com/imgextra/i4/O1CN01QCdTsN1S347SuzZU5_!!6000000002190-2-tps-1017-690.png) + + + + +## Schedule +For more information about the start of v3, see [bull components](./bull). + +To be compatible with the previous [egg scheduled tasks](https://eggjs.org/zh-cn/basics/schedule.html), follow the following steps. + +First install `midway-schedule` dependencies. + +```bash +$ npm i midway-schedule --save +``` + +Add to the plug-in. + +```typescript +// src/config/plugin.ts +export default { + schedule: true + schedulePlus: { + enable: true + package: 'midway-schedule', + } +} +``` + +Please refer to the previous version of the document. + + + +## Log + +Since v3 cannot use egg-logger, see [Logs](../logger). + + + +## Exception handling + +EggJS framework provides a unified error handling mechanism through [onerror](https://github.com/eggjs/egg-onerror) plug-ins, which will be used as Midway's bottom error logic and will not conflict with [error filters](../error_filter). + +Any exception thrown in all processing methods (Middleware, Controller, Service) for a request will be captured by it and will automatically return different types of errors (based on [Content Negotiation](https://tools.ietf.org/html/rfc7231#section-5.3.2)) according to the type the request wants to obtain. + + + +| Format of request requirements | Environment | errorPageUrl whether to configure | Return content | +| --- | --- | --- | --- | +| HTML & TEXT | local & unittest | - | Onerror comes with an error page that displays detailed error information | +| HTML & TEXT | Other | Yes | Redirect to errorPageUrl | +| HTML & TEXT | Other | No | onerror comes with a simple error page without error information (not recommended) | +| JSON & JSONP | local & unittest | - | JSON object or corresponding JSONP format response with detailed error information | +| JSON & JSONP | Other | - | JSON object or corresponding JSONP format response, without detailed error information | + + + + +errorPageUrl attribute is supported in the configuration of the onerror plug-in. When the errorPageUrl is configured, once the user requests an exception to the HTML page applied online, it will be redirected to this address. + + +In `src/config/config.default.ts` +```typescript +// src/config/config.default.ts +module.exports = { + onerror: { + // When an exception occurs on the online page, redirect to this page + errorPageUrl: '/50x.html', + }, +}; +``` + + + +## Extended Application/Context/Request/Response + + +### Add extension logic + + +Although MidwayJS do not want to mount the attribute directly to koa's Context and App (which will cause uncertainty in management and definition), this function of EggJS is still available. + + +The file location is as follows. +``` +➜ my_midway_app tree +. +├── src +│ ├── app +│ │ └── extend +│ │ ├── application.ts +│ │ ├── context.ts +│ │ ├── request.ts +│ │ └── response.ts +│ ├── config +│ └── interface.ts +├── test +├── package.json +└── tsconfig.json +``` +The content is the same as the original EggJS. +```typescript +// src/app/extend/context.ts +export default { + get hello() { + return 'hello world'; + }, +}; +``` +### Add extended definition + +Context please use Midway to extend, please check the [extended context definition](/docs/context_definition). + + +For the rest, please expand in `src/interface.ts`. +```typescript +// src/interface.ts +declare module 'egg' { + interface Request { + // ... + } + interface Response { + // ... + } + interface Application { + // ... + } +} +``` +:::info +**Do not place the definition of business custom extensions under the root directory** to avoid overwriting by ts-helper tools.`` +::: + + + +## Use egg-scripts deployment + +Since EggJS provides the default multi-process deployment tool `egg-scripts`, Midway also continues to support this method. If the upper layer is EggJS, this deployment method is recommended. + +First, in the dependency, ensure that the `egg-scripts` package is installed. + +```bash +$ npm i egg-scripts --save +``` + + + +Add `npm scripts` to `package.json`: + +After the above code is built, use our `start` and `stop` commands to start and stop. + +```json +"scripts": { + "start": "egg-scripts start --daemon --title=********* --framework=@midwayjs/web", + "stop": "egg-scripts stop --title=*********", +} +``` + + + +:::info + +`*********` is your project name. +::: + +> Note: `egg-scripts` has limited support for Windows systems, see [#22](https://github.com/eggjs/egg-scripts/pull/22). + +#### + +**Start Parameters** + +```bash +$ egg-scripts start --port=7001 --daemon --title=egg-server-showcase +``` + +Copy + +As shown in the above example, the following parameters are supported: + +- ``--port=7001` Port number, the environment variable process.env.PORT will be read by default, if not passed, the framework's built-in port 7001 will be used.` +- Whether `--daemon` is allowed in the background mode without nohup. If Docker is used, it is recommended to run directly at the foreground. +- `--env=prod` running environment of the framework. By default, the environment variable process.env.EGG_SERVER_ENV will be read. If it is not passed, the built-in environment prod of the framework will be used. +- `--workers=2` Number of Worker threads in the framework. By default, the number of app workers equivalent to the number of CPU cores will be created, which can make full use of CPU resources. +- `--title=egg-server-showcase` is used to facilitate grep in ps processes. the default value is `egg-server-${appname}`. +- `--framework=yadan` If the application uses a [custom framework](https://eggjs.org/zh-cn/advanced/framework.html), you can configure the egg.framework of the package.json or specify this parameter. +- `--ignore-stderr`. +- `--https.key` specifies the full path of the key file that is required for HTTPS. +- `--https.cert` specifies the full path of the certificate file required for HTTPS. +- All [egg-cluster](https://github.com/eggjs/egg-cluster) Options support transparent transmission, such as -- port, etc. + +For more parameters, see the [egg-scripts](https://github.com/eggjs/egg-scripts) and [egg-cluster](https://github.com/eggjs/egg-cluster) documents. + +:::info + +Logs deployed using egg-scripts are stored in the **user directory** **,** such as `/home/xxxx/logs`. + +::: + + + +## Startup environment + +The original egg uses `EGG_SERVER_ENV` as an environmental sign, please use `MIDWAY_SERVER_ENV` in Midway. + + + +## State type definition + +There is a special State attribute in the Context of koa at the bottom of egg. The State definition can be extended in a similar way to Context. + +```typescript +// src/interface.ts + +declare module '@midwayjs/web/dist/interface' { + interface Context { + abc: string; + } + + interface State{ + bbb: string; + ccc: number; + } +} +``` + + + + + +## Configuration + +### Default configuration + +```typescript +// src/config/config.default +export default { + // ... + egg: { + port: 7001 + }, +} +``` + +All parameters of `@midwayjs/web` are as follows: + +| Configuration Item | Type | Description | +| -------------- | ---------------- | ---------------------------- | +| port | number | Required, Started Port | +| key | string | Buffer | +| cert | string | Buffer | +| ca | string | Buffer | +| hostname | string | The hostname of the listener, the default 127.1 | +| http2 | boolean | Optional, supported by http2, default false | +| queryParseMode | simple\|extended | The default is extended | +| queryParseOptions | `qs.IParseOptions` | Parse options when 'simple' mode is used | + +The above attributes are valid for applications deployed locally and using `bootstrap.js`. + + + +### Modify port + +:::tip + +Note that this method will only take effect for projects developed locally and deployed using bootstrap.js files. + +::: + +By default, we provide the `7001` default port parameter in `config.default`. by modifying it, we can modify the default port of egg http service. + +For example, we changed it to `6001`: + +```typescript +// src/config/config.default +export default { + // ... + egg: { + port: 6001 + }, +} +``` + +By default, our port configuration is `null` because the single-test environment requires supertest to start the port. + +```typescript +// src/config/config.unittest +export default { + // ... + egg: { + port: null + }, +} +``` + +In addition, you can also temporarily modify the port by `midway-bin dev-ts-port = 6001`, which overwrites the configured port. + + + +### Global prefix + +For more information about this feature, see [Global Prefixes](../controller# Global Routing Prefix). + + + +### Https configuration + +In most cases, please use external agents as much as possible to complete the implementation of Https, such as Nginx. + +In some special scenarios, you can directly turn on Https by configuring SSL certificates (TLS certificates). + +First, you must prepare certificate files in advance, such as `ssl.key` and `ssl.pem`. The key is the private key of the server and the pem is the corresponding certificate. + +Then configure it. + +```typescript +// src/config/config.default +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export default { + // ... + egg: { + key: join(__dirname, '../ssl/ssl.key') + cert: join(__dirname, '../ssl/ssl.pem') + }, +} +``` + + + +### favicon settings + +By default, the browser will initiate a request to `favicon.ico`. + +```typescript +// src/config/config.default +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export default { + // ... + siteFile: { + '/favicon.ico': readFileSync(join(__dirname, 'favicon.png')) + }, +} +``` + + + +If the `@midwayjs/static-file` component is turned on, static file hosting of the component will be preferred. + +### Modify context log + +The context log of the egg framework can be modified individually. + +```typescript +export default { + egg: { + contextLoggerFormat: info => { + const ctx = info.ctx; + return '${info.timestamp} ${info.LEVEL} ${info.pid} [${ctx.userId} - ${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}'; + } + // ... + }, +}; +``` + + + +### Query array parsing + +By default, `ctx.query` parses to ignore arrays, while `ctx.queries` strictly turns all fields into arrays. + +If you adjust the `queryParseMode`, you can make `ctx.query` a structure between the two (the result of the querystring). + +```typescript +// src/config/config.default +export default { + // ... + egg: { + // ... + queryParseMode: 'simple', + queryParseOptions: { + arrayLimit: 100, + }, + }, +} +``` + + + + +## Common problem +### 1. generate ts definition + + +Midway provides the `@midwayjs/egg-ts-Hepler` toolkit for quickly generating the definitions that EggJS depends on when developing. +```bash +$ npm install @midwayjs/egg-ts-helper --save-dev +``` +Add the corresponding `ets` command to `package.json`. Generally speaking, we will add it before the dev command to ensure the correctness of the code. +```json + "scripts": { + "dev": "cross-env ets && cross-env NODE_ENV=local midway-bin dev --ts ", + }, +``` +:::info +Before writing code for the first time, you need to execute this command once to have ts definition generation. +::: + + +EggJS-generated definitions are in the `typings` directory. +``` +➜ my_midway_app tree +. +├── src ## midway project source code +├── typings ## EggJS defines the generation directory. +├── test +├── package.json +└── tsconfig.json +``` + + +### 2. Special Situation of Configuration in EggJS + + +Under EggJS, the life cycle in `configuration.ts` **will only be loaded and executed** under worker. If you have similar requirements on the Agent, use the `agent.ts` of EggJS. + + + +### 3. Asynchronous initialization configuration cannot override plug-in configuration + +`onConfigLoad` the lifecycle is executed after the egg plug-in (if any) is initialized, it cannot be used to override the configuration used by the egg plug-in. + + + +### 4. default csrf error + + +In the post request, especially the first time, the user will find a csrf error. the reason is that [egg-security](https://github.com/eggjs/egg-security) is built into the security plug-in in the framework by default, and csrf verification is enabled by default. + + +We can turn it off in the config, but better to go to [**Learn about it**](https://eggjs.org/en-us/core/security.html#%E5%AE%89%E5%85%A8%E5%A8%81%E8%83%81-csrf-%E7%9A%84%E9%98%B2%E8%8C%83) and then make a selection. + +```typescript +export const security = { + csrf: false +}; +``` + + + + +### 5. There is no definition problem + +Some egg plug-ins do not provide ts definitions, resulting in undeclared methods, such as egg-mysql. +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01mv68zG1zN6nALff8n_!!6000000006701-2-tps-1478-876.png) +You can use any to bypass. + +```typescript +await (this.app as any).mysql.query(sql); +``` + +Or you can add extended definitions by yourself. + +### 6、Get Http Server + +The original HttpServer is sealed inside Eggjs and needs to be accessed through events. + +```typescript +// src/configuration.ts +import { Configuration, App } from '@midwayjs/core'; +import { Application } from '@midwayjs/web'; + +@Configuration(/***/) +export class MainConfiguration { + + @App('egg') + app: Application; + + // ... + async onServerReady() { + this.app.once('server', (server) => { + // ... + }) + } +} +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/etcd.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/etcd.md new file mode 100644 index 000000000000..1b9c6023da86 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/etcd.md @@ -0,0 +1,193 @@ +# etcd + +etcd is an important basic component in the cloud native architecture, hosted by CNCF incubation. etcd can be registered as a service in discovery in microservices and Kubernetes clusters, and can also be used as a middleware for key-value storage. + +Midway provides components packaged based on the [etcd3](https://github.com/microsoft/etcd3) module, which provides etcd client calling capabilities. + +Related Information: + +| Description | | +| ------------------------------- | ---- | +| Available for Standard Items | ✅ | +| Available for Serverless | ✅ | +| Can be used for integration | ✅ | +| contains independent main frame | ❌ | +| Contains standalone logs | ❌ | + + + + +## Install dependencies + +```bash +$ npm i @midwayjs/etcd@3 --save +``` + +Or add the following dependencies in `package.json` and reinstall. + +```json +{ + "dependencies": { + "@midwayjs/etcd": "^3.0.0", + //... + }, +} +``` + + + + +## import component + + +First, import the component, import it in `configuration.ts`: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as etcd from '@midwayjs/etcd'; +import { join } from 'path' + +@Configuration({ + imports: [ + //... + etcd, + ], + //... +}) +export class MainConfiguration { +} +``` + + + +## Configure the default client + +In most cases, we can only use the default client to complete the function. + +```typescript +// src/config/config.default.ts +export default { + //... + etcd: { + client: { + host: [ + '127.0.0.1:2379' + ] + }, + }, +} +``` + + + +## Use the default client + +After the configuration is complete, we can use it in the code. + +```typescript +import { Provide } from '@midwayjs/core'; +import { ETCDService } from '@midwayjs/etcd'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + etcdService: etcdService; + + async invoke() { + + await this.etcdService.put('foo').value('bar'); + + const fooValue = await this.etcdService.get('foo').string(); + console.log('foo was:', fooValue); + + const allFValues = await this.etcdService.getAll().prefix('f').keys(); + console.log('all our keys starting with "f":', allFValues); + + await this.etcdService.delete().all(); + } +} +``` + +For more APIs, please refer to the ts definition or [official document](https://microsoft.github.io/etcd3/classes/etcd3.html). + + + +## Multiple instance configuration + +```typescript +// src/config/config.default.ts +export default { + //... + etcd: { + clients: { + instance1: { + { + host: [ + '127.0.0.1:2379' + ] + }, + }, + instance2: { + { + host: [ + '127.0.0.1:2379' + ] + }, + } + } + }, +} +``` + + + +## Get multiple instances + +```typescript +import { Provide } from '@midwayjs/core'; +import { ETCDServiceFactory } from '@midwayjs/etcd'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + etcdServiceFactory: ETCDServiceFactory; + + async invoke() { + const instance1 = this.etcdServiceFactory.get('instance1'); + //... + + const instance2 = this.etcdServiceFactory.get('instance2'); + //... + } +} +``` + + + +## Create instance dynamically + +```typescript +import { Provide } from '@midwayjs/core'; +import { ETCDServiceFactory } from '@midwayjs/etcd'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + etcdServiceFactory: ETCDServiceFactory; + + async invoke() { + const instance3 = await this.etcdServiceFactory.createInstance({ + host: [ + '127.0.0.1:2379' + ] + }, 'instance3'); + //... + } +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/express.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/express.md new file mode 100644 index 000000000000..434409af784b --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/express.md @@ -0,0 +1,604 @@ +# Express + +This chapter mainly introduces how to use Express as the upper-level framework in Midway and use its own capabilities. + +| Description | | +| -------------- | ---- | +| Contains independent main framework | ✅ | +| Contains independent logs | ✅ | + + + +## Installation dependency + +```bash +$ npm i @midwayjs/express@3 --save +$ npm i @types/body-parser @types/express @types/express-session --save-dev +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/express": "^3.0.0", + // ... + }, + "devDependencies": { + "@types/body-parser": "^1.19.2", + "@types/express": "^4.17.13", + "@types/express-session": "^1.17.4", + // ... + } +} +``` + +Examples can also be created directly using scaffolding. + +```bash +# npm v6 +$ npm init midway --type=express-v3 my_project + +# npm v7 +$ npm init midway -- --type=express-v3 my_project +``` + + +For Express,Midway provides `@midwayjs/express` package for adaptation, in which Midway provides unique dependency injection, section and other capabilities. + +:::info +The Express version we are using is `v4`. +::: + + +## Directory structure +``` +. +├── src +│ ├── controller # controller cdoe +│ ├── service # service code +| └── configuration.ts # Entry, Lifecycle Configuration and Component Management +├── test +├── package.json +└── tsconfig.json +``` + + + +## Open the component + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as express from '@midwayjs/express'; +import { join } from 'path'; + +@Configuration({ + imports: [express], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration { + @App() + app: express.Application; + + async onReady() {} +} +``` + + + + +## Controller + + +The writing of the entire request controller is similar to that of Midway adapts to other frameworks. In order to be consistent with the frame writing of other scenes, Midway maps the `req` of the Express to a `ctx` object when requesting. +```typescript +import { Inject, Controller, Get, Provide, Query } from '@midwayjs/core'; +import { Context, NextFunction } from '@midwayjs/express'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async home(@Query() id) { + console.log(id); // req.query.id === id + return 'hello world'; // Simple return, equivalent to res.send('hello world'); + } +} +``` +You can also add `req` and `res`. +```typescript +import { Inject, Controller, Get, Provide, Query } from '@midwayjs/core'; +import { Context, Response, NextFunction } from '@midwayjs/express'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; // is req + + @Inject() + req: Context; + + @Inject() + res: Response; + + @Get('/') + async home(@Query() id) { + // this.req.query.id === id + } +} +``` + + + +## Web middleware + + +Express middleware is written in a special way, and its parameters are different. + + +```typescript +import { Middleware } from '@midwayjs/core'; +import { Context, Response, NextFunction } from '@midwayjs/express'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async ( + req: Context, + res: Response, + next: NextFunction + ) => { + console.log('Request...'); + next(); + }; + } + +} +``` + +Note that we have exported a `ReportMiddleware` class here. In order to facilitate the docking of asynchronous processes, the `resolve` return can be an async function. + +The next method in the Express is used to call the next middleware, which means that the Express middleware is not an onion model, but a one-way call. + + + + +### Routing middleware + + +We can apply the middleware written above to a single Controller or to a single route. + + +```typescript +import { Controller, Get, Provide } from '@midwayjs/core'; + +@Controller('/', { middleware: [ ReportMiddleware ]}) // controller-level middleware +export class HomeController { + + @Get('/', { middleware: [ ReportMiddleware ]}) // routing-level middleware + async home() { + return 'hello world' + } +} +``` + + +### Global middleware + + +Directly use the `app.generateMiddleware` method provided by Midway to load global middleware at the entrance. +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle } from '@midwayjs/core'; +import * as express from '@midwayjs/express'; +import { ReportMiddleware } from './middleware/report.middleware.ts' + +@Configuration({ + imports: [express], +}) +export class MainConfiguration implements ILifeCycle { + + @App() + app: express.Application; + + async onReady() { + this.app.useMiddleware(ReportMiddleware); + } +} +``` + + +In addition to loading middleware in the form of Class, it also supports loading traditional Express middleware. +```typescript +// src/configuration.ts +import { Configuration, App, ILifeCycle } from '@midwayjs/core'; +import * as express from '@midwayjs/express'; +import { join } from 'path'; + +@Configuration({ + imports: [express] +}) +export class MainConfiguration implements ILifeCycle { + + @App() + app: express.Application; + + async onReady() { + this.app.useMiddleware((req, res, next) => { + // xxx + }); + } +} +``` +You can call methods on all Express by injecting `app` objects. + + + +## Return to unified processing + +Since the Express middleware is a one-way call and cannot be executed on return, we have designed an additional filter decorated by `@Match` to handle the behavior of the return value. + +For example, we can define filters returned globally. + +```typescript +// src/filter/globalMatch.filter.ts +import { Match } from '@midwayjs/core'; +import { Context, Response } from '@midwayjs/express'; + +@Match() +export class GlobalMatchFilter { + match(value, req, res) { + // ... + return { + status: 200 + data: { + value + }, + }; + } +} +``` + +You can also match a specific route for return. + +```typescript +// src/filter/api.filter.ts +import { Match } from '@midwayjs/core'; +import { Context, Response } from '@midwayjs/express'; + +@Match((ctx: Context, res: Response) => { + return ctx.path === '/api'; +}) +export class APIMatchFilter { + match(value, req: Context, res: Response) { + // ... + return { + data: { + message: + data: value + }, + }; + } +} +``` + +It needs to be applied to app. + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as express from '@midwayjs/express'; +import { join } from 'path'; +import { APIMatchFilter } from './filter/api.filter'; +import { GlobalMatchFilter } from 'filter/globalMatch.filter'; + +@Configuration({ + imports: [express], + importConfigs: [join(__dirname, './config')] +}) +export class MainConfiguration { + @App() + app: express.Application; + + async onReady() { + // ... + this.app.useFilter([APIMatchFilter, GlobalMatchFilter]); + } +} +``` + +Note that such filters are matched and executed in the order in which they are added. + + + +## Error handling + +Same as ordinary items, using error filters, but the parameters are slightly different. + +```typescript +import { Catch } from '@midwayjs/core'; +import { Context, Response } from '@midwayjs/express'; + +@Catch() +export class GlobalError { + catch(err: Error, req: Context, res: Response) { + if (err) { + return { + status: err.status ?? 500, + message: err.message + } + } + } +} +``` + +It needs to be applied to app. + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as express from '@midwayjs/express'; +import { join } from 'path'; +import { GlobalError } from './filter/global.filter'; + +@Configuration({ + imports: [express] + importConfigs: [join(__dirname, './config')] +}) +export class MainConfiguration { + @App() + app: express.Application; + + async onReady() { + this.app.useMiddleware((req, res, next) => { + next(); + }); + + this.app.useFilter([GlobalError]); + } +} +``` + +Note that both `@Match` and `@Catch` are filters that are automatically executed internally. . + + + +## Cookie + +`@midwayjs/express` comes with the `cookie parser` function and uses the `cookie-parser` module. + +Use `keys` as the key for cookies. + +```typescript +// src/config/config.default +export default { + keys: ['key1', 'key2'] +} +``` + +Get Cookie. + +```typescript +const cookieValue = req.cookies['cookie-key']; +``` + +Set Cookie. + +```typescript +res.cookie ( + 'cookie-key', + 'cookie-value', + cookieOptions +); +``` + + + +## Session + +`@midwayjs/express` has built-in Session components, providing us with `ctx.session` to access or modify the current user Session. + +By default, `cookie-session` is used. The default configuration is as follows. + +```typescript +// src/config/config.default +export default { + session: { + name: 'MW_SESS', + resave: true + saveUninitialized: true + cookie: { + maxAge: 24*3600 * 1000, // ms + httpOnly: true + // sameSite: null + }, + } +} +``` + +We can set the session through a simple API. + +```typescript +@Controller('/') +export class HomeController { + + @Inject() + req; + + @Get('/') + async get() { + // set all + this.req.session = req.query; + + // set value + this.req.session.key = 'abc'; + + // get + const key = this.req.session.key; + + // remove + this.req.session = null; + + // set max age + this.req.session.maxAge = Number(req.query.maxAge); + + // ... + } +} + +``` + + + +## BodyParser + +`@midwayjs/express` has its own `bodyParser` function. By default, it parses `Post` requests and automatically identifies `json`, `text`, and `urlencoded` types. + +The default size is limited to `1mb`. You can set the size of each item separately. + +```typescript +// src/config/config.default +export default { + // ... + bodyParser: { + json: { + enable: true + limit: '1mb', + strict: true + }, + raw: { + enable: false + limit: '1mb', + }, + text: { + enable: true + limit: '1mb', + }, + urlencoded: { + enable: true + extended: false + limit: '1mb', + parameterLimit: 1000 + }, + }, +} +``` + + + +## Configuration + +### Default configuration + +The configuration sample of `@midwayjs/express` is as follows: + +```typescript +// src/config/config.default +export default { + // ... + express: { + port: 7001 + }, +} +``` + +All attributes are described as follows: + +| Property | Type | Description | +| ------------ |--------------------------------------------| ------------------------------------------------------- | +| port | number | Optional, port to start | +| globalPrefix | string | Optional, global http prefix | +| keys | string[] | Optional, Cookies signature, if the upper layer does not write keys, you can also set it here | +| hostname | string | Optional, hostname to listen to, default 127.1 | +| key | string \| Buffer \| Array\ | Optional, Https key, server private key | +| cert | string \| Buffer \| Array\ | Optional, Https cert, server certificate | +| ca | string \| Buffer \| Array\ | Optional, Https ca | +| http2 | boolean | Optional, http2 support, default false | + + + +### Modify port + +By default, we provide the `7001` default port parameter in `config.default`. by modifying it, we can modify the default port of Express http service. + +For example, we changed it to `6001`: + +```typescript +// src/config/config.default +export default { + // ... + express: { + port: 6001 + }, +} +``` + +By default, our port configuration is `null` because the single-test environment requires supertest to start the port. + +```typescript +// src/config/config.unittest +export default { + // ... + express: { + port: null + }, +} +``` + +In addition, you can also temporarily modify the port by `midway-bin dev-ts-port = 6001`, which overwrites the configured port. + + + +### Global prefix + +For more information about this feature, see [Global Prefixes](../controller# Global Routing Prefix). + + + +### Https configuration + +In most cases, please use external agents as much as possible to complete the implementation of Https, such as Nginx. + +In some special scenarios, you can directly turn on Https by configuring SSL certificates (TLS certificates). + +First, you must prepare certificate files in advance, such as `ssl.key` and `ssl.pem`. The key is the private key of the server and the pem is the corresponding certificate. + +Then configure it. + +```typescript +// src/config/config.default +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export default { + // ... + express: { + key: join(__dirname, '../ssl/ssl.key') + cert: join(__dirname, '../ssl/ssl.pem') + }, +} +``` + + + +### Modify context log + +The context log of the express framework can be modified separately. + +```typescript +export default { + express: { + contextLoggerFormat: info => { + // equivalent req + const req = info.ctx; + const userId = req?.['session']?.['userId'] || '-'; + return '${info.timestamp} ${info.LEVEL} ${info.pid} [${userId} - ${Date.now() - req.startTime}ms ${req.method}] ${info.message}'; + } + // ... + }, +}; +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/grpc.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/grpc.md new file mode 100644 index 000000000000..8f5104d231f2 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/grpc.md @@ -0,0 +1,1113 @@ +# gRPC + +GRPC is a high-performance, universal open source RPC framework, which is mainly developed by Google for mobile applications and designed based on HTTP/2 protocol standards. It is developed based on ProtoBuf(Protocol Buffers) serialization protocol and supports many development languages. + + +This article demonstrates how to provide gRPC services under Midway system and how to call gRPC services. + + +Midway uses the latest gRPC-recommended [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) for development, and provides toolkits for quick service release and service call. + +The module we use is `@midwayjs/grpc`, which can publish services independently and call gRPC services through other frameworks. + +Related information: + +**Provide services** + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ❌ | +| Can be used for integration | ✅ | + +**Call Service** + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | + +**Other** + +| Description | | +| -------------------- | ---- | +| Can be used independently as the main framework | ✅ | +| Middleware can be added independently | ✅ | + + + +## Installation dependency + +```bash +$ npm i @midwayjs/grpc@3 --save +$ npm i @midwayjs/grpc-helper --save-dev +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/grpc": "^3.0.0", + // ... + }, + "devDependencies": { + "@midwayjs/grpc-helper": "^1.0.0 ", + // ... + } +} +``` + + + +## Open the component + +:::tip + +Whether it is providing a service or invoking a service, you need to open the component. + +::: + +`@midwayjs/grpc` can be used as an independent main framework. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as grpc from '@midwayjs/grpc'; + +@Configuration({ + imports: [grpc] + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + +``` + +It can also be attached to other main frameworks, such as `@midwayjs/Koa`. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as grpc from '@midwayjs/grpc'; + +@Configuration({ + imports: [koa, grpc] + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + +``` + + + +## Directory structure + +The general directory structure is as follows. `src/provider` is the directory that provides gRPC services. + +``` +. +├── package.json +├── proto ## proto definition file +│ └── helloworld.proto +├── src +│ ├── configuration.ts ## entry configuration file +│ ├── interface.ts +│ └── provider ## files provider services provided by gRPC +│ └── greeter.ts +├── test +├── bootstrap.js ## service startup portal +└── tsconfig.json +``` + + + +## Define service interface + + +In microservices, defining a service requires a specific interface definition language (IDL) to complete, and Protocol Buffers is used as serialization protocol by default in gRPC. + + +The serialization protocol is independent of the language and platform, and provides implementations in multiple languages, such as Java,C ++,Go, etc. Each implementation contains compilers and library files in the corresponding language. Therefore, gRPC is a service framework that provides and calls across languages. + +The general architecture of a gRPC service can be represented by a diagram on the official website. + +![](https://img.alicdn.com/imgextra/i3/O1CN01kpIyg51k8i5DtcGpZ_!!6000000004639-2-tps-621-445.png) + + +The default suffix is `.proto` for files that Protocol the Buffers protocol. IDL file with the. proto suffix, and generates language-specific data structures, server-side interfaces, and client-side Stub code through its compiler. + + +:::info +Because proto files can be used across languages, in order to facilitate sharing, we usually place proto files outside the src directory to facilitate other tools to copy and distribute. +::: + + +The following is a basic `proto/helloworld.proto` file. +```protobuf +syntax = "proto3"; + +package helloworld; + +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} + +``` + + +Proto3 represents the third version of the protobuf protocol, which is currently recommended by gRPC, "simple syntax and more complete functions". + + +We can define the service body in `service` format, which can include methods. At the same time, we can describe the specific request parameters and response parameters of the service in more detail through `message`. + + +For more information, see [Google's official website documentation](https://developers.google.com/protocol-buffers/docs/overview#simple). + + +:::info +As you will see, this is very similar to Class in Java, and each structure is equivalent to a class in Java. +::: + + +### Write proto file + + +Now let's look at the previous service again, is it easy to understand. +```protobuf +syntax = "proto3"; + +package helloworld; + +// Definition of service +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// Service request parameters +message HelloRequest { + string name = 1; +} + +// Response parameters of the service +message HelloReply { + string message = 1; +} + +``` + + +We define a service called `Greeter`, which contains a requestor of a `HelloRequest` structure and a respondent of a `HelloReply` structure. + + +Next, we will demonstrate this service to you. + + +### Generate code definitions + + +The traditional gRPC framework requires users to manually write proto files, generate js services, and finally rewrite and implement them according to the services generated by js. Under Midway system, we provide a grpc-helper toolkit to speed up this process. + + +If there is no installation, you can install it first. +```bash +$ npm i @midwayjs/grpc-helper --save-dev +``` + + +The function of the grpc-helper tool is to generate the corresponding readable ts interface file from the proto file provided by the user. + + +We can add a script to facilitate this process. +```json +{ + "scripts": { + "generate": "tsproto --path proto --output src/domain" + } +} +``` + + +Then `npm run generate` is executed. + + +After the preceding command is executed, the service interface definition corresponding to the Proto file is generated in the `src/domain` Directory of the code. + + +:::info +Whether it is providing gRPC service or calling gRPC service, it must be defined. +::: + + +The generated code is as follows, including a namespace (namespace) and two TypeScript Interface under the namespace, `Greeter` for writing server-side implementations and `GreeterClient` for writing client-side implementations. +```typescript +/** +* This file is auto-generated by grpc-helper +*/ + +import * as grpc from '@midwayjs/grpc'; + +// Generated namespace +export namespace helloworld { + + // Definition used by the server + export interface Greeter { + // Sends a greeting + sayHello(data: HelloRequest): Promise; + } + + // Definition used by the client + export interface GreeterClient { + // Sends a greeting + sayHello(options?: grpc.IClientOptions): grpc.IClientUnaryService; + } + + // Request body structure + export interface HelloRequest { + name?: string; + } + + // Response body structure + export interface HelloReply { + message?: string; + } +} + +``` + +:::info +Whenever The proto file is modified, the corresponding service definition needs to be regenerated, and then the corresponding method is implemented. +::: + + + +## Provide gRPC service (Provider) + + +### Writing Service Provider (Provider) + + +In the `src/provider` directory, we create `greeter.ts` as follows +```typescript +import { + MSProviderType, + Provider, + GrpcMethod +} from '@midwayjs/core'; +import { helloworld } from '../domain/helloworld'; + +/** + * Implementation of helloworld.Greeter Interface Services + */ +@Provider(MSProviderType.GRPC, { package: 'helloworld' }) +export class Greeter implements helloworld.Greeter { + + @GrpcMethod() + async sayHello(request: helloworld.HelloRequest) { + return { message: 'Hello '+ request.name }; + } +} + +``` +:::info +Note that the @Provider decorator is different from the @Provide decorator, the former is used to provide services, and the latter is used to rely on the class identified by the injection container scan. +::: + + +We use the `@Provider` to expose an RPC service. The first parameter of the `@Provider` is the RPC service type. This parameter is an enumeration. Here, select the GRPC type. + + +The second parameter of the `@Provider` is the metadata of the RPC service, which refers to the metadata of the gRPC service. Here, you need to write the package field of the gRPC, that is, the package field in the proto file (the field here is used to correspond to the field after the proto file is loaded). + + +For ordinary gRPC service interfaces (UnaryCall), we only need to use the `@GrpcMethod()` decorator. The modification method is the service definition itself, the input parameter is the input parameter defined in proto, and the return value is the defined response body. + +:::info +Note that the generated Interface is to better write service code and standardize the structure. Please be sure to write according to the definition. +::: + + +### Configuration service + + +The content is as follows. +```typescript +// src/config/config.default +import { MidwayAppInfo, MidwayConfig } from '@midwayjs/core'; + +export default (appInfo: MidwayAppInfo): MidwayConfig => { + return { + // ... + grpcServer: { + services: [ + { + protoPath: join(appInfo.appDir, 'proto/hero.proto') + package: 'hero', + }, + { + protoPath: join(appInfo.appDir, 'proto/helloworld.proto') + package: 'helloworld', + } + ], + } + }; +} +``` +services fields are arrays, which means that Midway projects can publish multiple gRPC services at the same time. The structure of each service is: + +| Property | Type | Description | +| --- | --- | --- | +| protoPath | String | Required, absolute path of proto file | +| package | String | Required, the package corresponding to the service | + +In addition to the Service configuration, there are some other configurations. + +| Property | Type | Description | +| ------------- | ----------------- | ------------------------------------------------------------ | +| url | String | Optional, gRPC service address, default 6565 port,like 'localhost:6565' | +| loaderOptions | Object | Optional, the options of the proto file loader | +| credentials | ServerCredentials | Optional. credentials parameter options when grpc Server binding | +| serverOptions | ChannelOptions | Optional. [Custom options](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js#supported-channel-options) for grpc Server | + + + +### Provide security certificate + +Security certificates can be passed through `credentials` parameters. + +```typescript +// src/config/config.default +import { MidwayAppInfo, MidwayConfig } from '@midwayjs/core'; +import { ServerCredentials } from '@midwayjs/grpc'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +const cert = readFileSync(join(__dirname, './cert/server.crt')); +const pem = readFileSync(join(__dirname, './cert/server.pem')); +const key = readFileSync(join(__dirname, './cert/server.key')); + +export default (appInfo: MidwayAppInfo): MidwayConfig => { + return { + // ... + grpcServer: { + // ... + credentials: ServerCredentials.createSsl(cert, [{ private_key: key, cert_chain: pem }]); + } + }; +} +``` + + + +### Write unit tests + +The `@midwayjs/grpc` library provides a `createGRPCConsumer` method for calling clients in real time. Generally, we use this method for testing. + +:::caution +This method will be connected in real time every time it is called. It is not recommended to use this method in a production environment. +::: + + +Written in the test as follows. +```typescript +import { createApp, close } from '@midwayjs/mock'; +import { Framework, createGRPCConsumer } from '@midwayjs/grpc'; +import { join } from 'path'; +import { helloworld } from '../src/domain/helloworld'; + +describe('test/index.test.ts', () => { + + it('should create multiple grpc service in one server', async () => { + const baseDir = join(__dirname, '../'); + + // Create Service + const app = await createApp(); + + // Call service + const service = await createGRPCConsumer({ + package: 'helloworld', + protoPath: join(baseDir, 'proto', 'helloworld.proto'), + url: 'localhost:6565' + }); + + const result = await service.sayHello().sendMessage({ + name: 'harry' + }); + + expect(result.message).toEqual('Hello harry'); + await close(app); + }); + +}); + +``` + + + +## Call gRPC service (Consumer) + + +We write a gRPC service to invoke the exposed service above. + +:::info +In fact, you can call it in the Controller of the Web, or Service and other places, here is just an example. +::: + + +### Call configuration + + +You need to add the target service you need to call and its proto file information to `src/config/config.default.ts`. + + +For example, here we fill in the service itself exposed above, as well as the service's Proto, package name and other information (function form). +```typescript +// src/config/config.default +import { MidwayAppInfo, MidwayConfig } from '@midwayjs/core'; + +export default (appInfo: MidwayAppInfo): MidwayConfig => { + return { + // ... + grpc: { + services: [ + { + url: 'localhost:6565', + protoPath: join(appInfo.appDir, 'proto/helloworld.proto'), + package: 'helloworld', + }, + ], + }, + }; +} +``` + + +### Code call + + +After configuration, we can call it in the code. + + +`@midwayjs/grpc` provides `clients` to easily obtain configured services. We just need to inject this object where it needs to be injected. + + +For example: +```typescript +import { + Provide, + Inject, +} from '@midwayjs/core'; +import { helloworld, hero } from '../interface'; +import { Clients } from '@midwayjs/grpc'; + +@Provide() +export class UserService { + @Inject() + grpcClients: Clients; + +} +``` + + +We get the client instance of the other service through the `clients` and call it. + + +```typescript +import { + Provide, + Inject, +} from '@midwayjs/core'; +import { helloworld, hero } from '../interface'; +import { Clients } from '@midwayjs/grpc'; + +@Provide() +export class UserService { + @Inject() + grpcClients: Clients; + + async invoke() { + // Get Services + const greeterService = this.grpcClients.getService ( + 'helloworld. Greeter' + ); + + // Call service + const result = await greeterService.sayHello() + .sendMessage({ + name: 'harry' + }); + + // Return result + return result; + } + +} +``` + + +You can also use the `@Init` decorator to cache the services to be called to properties. This can be reused when other methods are called. + + +An example is as follows. +```typescript +import { + GrpcMethod, + MSProviderType, + Provider, + Inject, + Init, +} from '@midwayjs/core'; +import { helloworld, hero } from '../interface'; +import { Clients } from '@midwayjs/grpc'; + +@Provider(MSProviderType.GRPC, { package: 'hero' }) +export class HeroService implements hero.HeroService { + // Injection client + @Inject() + grpcClients: Clients; + + greeterService: helloworld.GreeterClient; + + @Init() + async init() { + // Assign a service instance + this.greeterService = this.grpcClients.getService ( + 'helloworld. Greeter' + ); + } + + @GrpcMethod() + async findOne(data) { + // Call service + const result = await greeterService.sayHello() + .sendMessage({ + name: 'harry' + }); + + // Return result + return result; + } +} + +``` + + +## Streaming service + + +The streaming service of gRPC is used to reduce connections so that servers or clients can execute tasks without waiting, thus improving execution efficiency. + + +There are three types of gRPC streaming services. From the perspective of the server, + + +- Server receives flow (client push) +- Server response flow (server push) +- Bidirectional flow + + + +We will introduce them one by one. + + +### Streaming proto file + + +The proto file for streaming is written differently. You must mark the `stream` parameter where you want to use the stream. +```protobuf + +syntax = "proto3"; + +package math; + +message AddArgs { + int32 id = 1; + int32 num = 2; +} + +message Num { + int32 id = 1; + int32 num = 2; +} + +service Math { + rpc Add (AddArgs) returns (Num) { + } + + // Bidirectional flow + rpc AddMore (stream AddArgs) returns (stream Num) { + } + + // The server pushes to the client. + rpc SumMany (AddArgs) returns (stream Num) { + } + + // The client pushes to the server. + rpc AddMany (stream AddArgs) returns (Num) { + } +} + +``` +The interface generated by this service is defined: + + +```typescript +import { + IClientDuplexStreamService, + IClientReadableStreamService, + IClientUnaryService, + IClientWritableStreamService, + IClientOptions, +} from '@midwayjs/grpc'; + +export namespace math { + export interface AddArgs { + id?: number; + num?: number; + } + export interface Num { + id?: number; + num?: number; + } + + /** + * server interface + */ + export interface Math { + add(data: AddArgs): Promise; + addMore(data: AddArgs): Promise; + // server push, client read + sumMany(data: AddArgs): Promise + // client push,server read + addMany(num: AddArgs): Promise; + } + + /** + * client interface + */ + export interface MathClient { + add(options?: IClientOptions): IClientUnaryService; + addMore(options?: IClientOptions): IClientDuplexStreamService; + // server push, client read + sumMany(options?: IClientOptions): IClientReadableStreamService; + // Push on the client side and read on the server side + addMany(options?: IClientOptions): IClientWritableStreamService; + } +} + +``` + + +### Server push + + +The client calls once and the server can return multiple times. The streaming type is identified by the parameter of `@GrpcMethod()`. + + +The available types are: + + +- `GrpcStreamTypeEnum.WRITEABLE` the server output stream (single work) +- `GrpcStreamTypeEnum.READABLE` the client output stream (single work), the server accepts multiple times +- `GrpcStreamTypeEnum.DUPLEX` duplex flow + + + +The server example is as follows: +```typescript +import { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core'; +import { Context, Metadata } from '@midwayjs/grpc'; +import { math } from '../interface'; + +/** + */ +@Provider(MSProviderType.GRPC, { package: 'math' }) +export class Math implements math.Math { + + @Inject() + ctx: Context; + + @GrpcMethod({type: GrpcStreamTypeEnum.WRITEABLE }) + async sumMany(args: math.AddArgs) { + this.ctx.write({ + num: 1 + args.num + }); + this.ctx.write({ + num: 2 + args.num + }); + this.ctx.write({ + num: 3 + args.num + }); + + this.ctx.end(); + } + + // ... +} + +``` +The server uses the `ctx.write` method to return data. You can return data multiple times because it is a server stream. + + +After the response is completed, use the `ctx.end()` method to disable the flow. + + +The client, called once, accepts multiple data. + + +For example, the accumulation logic below. + + +Promise writing method, it will wait for the server data to return before processing. +```typescript +// Server push +let total = 0; +let result = await service.sumMany().sendMessage({ + num: 1 +}); + +result.forEach(data => { + total += data.num; +}); + +// total = 9; +``` + + +Event writing, real-time processing. +```typescript +// Server push +let call = service.sumMany().getCall(); + +call.on('data', data => { + // do something +}); + +call.sendMessage({ + num: 1 +}); + +``` + + +### Client push + + +The client calls multiple times, the server receives data multiple times, and returns a result. The streaming type is identified by the parameter of `@GrpcMethod({type: GrpcStreamTypeEnum.READABLE})`. + + +The server example is as follows: +```typescript +import { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core'; +import { Context, Metadata } from '@midwayjs/grpc'; +import { math } from '../interface'; + +/** + */ +@Provider(MSProviderType.GRPC, { package: 'math' }) +export class Math implements math.Math { + + sumDataList: number[] = []; + + @Inject() + ctx: Context; + + @GrpcMethod({type: GrpcStreamTypeEnum.READABLE, onEnd: 'sumEnd' }) + async addMany(data: math.Num) { + this.sumDataList.push(data); + } + + async sumEnd(): Promise { + const total = this.sumDataList.reduce((pre, cur) => { + return { + num: pre.num + cur.num + } + }); + return total; + } + + // ... +} + +``` + + +Each time the client calls, the `addMany` method is triggered. + + +After the client sends the `end` event, the method specified by the `onEnd` parameter on the `@GrpcMethod` decorator is called, and the return value of this method is the value obtained by the last client. + + +The client example is as follows: +```typescript +// Client push +const data = await service.addMany() +.sendMessage({num: 1}) +.sendMessage({num: 2}) +.sendMessage({num: 3}) +.end(); + +// data.num = 6 +``` + + +### Bidirectional flow + + +The client can call multiple times, and the server can also receive multiple data and return multiple results, similar to traditional TCP communication. The duplex streaming type is identified by the parameter of `@GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX})`. + + +The server example is as follows: +```typescript +import { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core'; +import { Context, Metadata } from '@midwayjs/grpc'; +import { math } from '../interface'; + +/** + */ +@Provider(MSProviderType.GRPC, { package: 'math' }) +export class Math implements math.Math { + + @Inject() + ctx: Context; + + @GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX, onEnd: 'duplexEnd' }) + async addMore(message: math.AddArgs) { + this.ctx.write({ + id: message.id + num: message.num +10 + }); + } + + async duplexEnd() { + console.log('got client end message'); + } + // ... +} + +``` +The server can use `ctx.write` to return data at any time, or use `ctx.end` to disable the flow. + + +Client example: + + +For clients of duplex communication, because the order of calling and returning cannot be guaranteed, we need to use the listening mode to consume the results. +```typescript +const clientStream = service.addMore().getCall(); + +let total = 0; +let idx = 0; + +duplexCall.on('data', (data: math.Num) => { + total += data.num; + idx++; + if (idx === 2) { + duplexCall.end(); + // total => 29 + } +}); + +duplexCall.write({ + num: 3, +}); + +duplexCall.write({ + num: 6 +}); +``` + + +If you want to ensure the order of calls, we also provide a two-way flow call method that guarantees the order, but you need to define a fixed ID in the Proto to to ensure the order. + + +For example, our Math.proto adds a fixed id to each entry and exit parameter, so the order can be fixed. +```typescript + +syntax = "proto3"; + +package math; + +message AddArgs { + int32 id = 1; // The id name here is fixed + int32 num = 2; +} + +message Num { + int32 id = 1; // The id name here is fixed + int32 num = 2; +} + +service Math { + rpc Add (AddArgs) returns (Num) { + } + + rpc AddMore (stream AddArgs) returns (stream Num) { + } + + // The server pushes to the client. + rpc SumMany (AddArgs) returns (stream Num) { + } + + // The client pushes to the server. + rpc AddMany (stream AddArgs) returns (Num) { + } +} + +``` +The fixed-order client calls are as follows: +```typescript +// Ensure sequential bidirectional flow +const t = service.addMore(); + +const result4 = await new Promise((resolve, reject) => { + + let total = 0; + + // First call and return + t.sendMessage({ + num: 2 + }) + .then(res => { + expect(res.num).toEqual(12); + total += res.num; + }) + .catch(err => console.error(err)); + + // Second call and return + t.sendMessage({ + num: 5 + }).then(res => { + expect(res.num).toEqual(15); + total += res.num; + resolve(total); + }) + .catch(err => console.error(err)); + + t.end(); +}); + +// result4 => 27 +``` +The default ID is `id`. If the server definition is different, you can change it. +```typescript +// Ensure sequential bidirectional flow +const t = service.addMore({ + messageKey: 'uid' +}); +``` + + +## Metadata (Metadata) + + +The metadata of gRPC is equivalent to the HTTP context. + + +The server returns metadata through the `ctx.sendMetadata` method, and can also obtain the metadata passed by the client through `ctx.metadata`. +```typescript +import { + MSProviderType, + Provider, + GrpcMethod +} from '@midwayjs/core'; +import { helloworld } from '../domain/helloworld'; +import { Context, Metadata } from '@midwayjs/grpc'; + +/** + * Implementation of helloworld.Greeter Interface Services + */ +@Provider(MSProviderType.GRPC, { package: 'helloworld' }) +export class Greeter implements helloworld.Greeter { + + @Inject() + ctx: Context; + + @GrpcMethod() + async sayHello(request: helloworld.HelloRequest) { + + // Metadata passed by the client + console.log(this.ctx.metadata); + + // Create metadata + const meta = new Metadata(); + this.ctx.metadata.add('xxx', 'bbb'); + this.ctx.sendMetadata(meta); + + return { message: 'Hello '+ request.name }; + } +} +``` + + +The client passes metadata through the options parameters of the method. +```typescript +import { Metadata } from '@midwayjs/grpc'; + +const meta = new Metadata(); +meta.add('key', 'value'); + +const result = await service.sayHello({ + metadata: meta +}).sendMessage({ + name: 'harry' +}); +``` + + +Getting metadata is relatively cumbersome. + + +Ordinary unary calls (UnaryCall) require `sendMessageWithCallback` methods to obtain metadata. +```typescript +const call = service.sayHello().sendMessageWithCallback({ + name: 'zhangting' +}, (err) => { + if (err) { + reject(err); + } +}); +call.on('metadata', (meta) => { + // output meta +}); +``` +For other streaming services, you can directly subscribe to the original client stream object by `getCall()` method. +```typescript +// Get the service. Note that there is no await here. +const call = service.addMany().getCall(); +call.on('metadata', (meta) => { + // output meta +}); +``` + + +## Timeout processing + + +We can pass parameters in milliseconds when calling the service. +```typescript +const result = await service.sayHello({ + timeout: 5000 +}).sendMessage({ + name: 'harry' +}); +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/http-proxy.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/http-proxy.md new file mode 100644 index 000000000000..e1856694dd58 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/http-proxy.md @@ -0,0 +1,150 @@ +# HTTP proxy + +The HTTP request proxy component is applicable to multiple frameworks such as `@midwayjs/faas`, `@midwayjs/web`, `@midwayjs/koa`, and `@midwayjs/express`. It supports multiple request methods such as GET and POST. + +Related information: + +| Web support | | +| ----------------- | --- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | 💬 | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + +:::caution + +💬 Some function computing platforms do not support streaming request responses. Please refer to the corresponding platform capabilities. + +::: + +## Installation dependency + +```bash +$ npm i @midwayjs/http-proxy@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/http-proxy": "^3.0.0" + // ... + }, + "devDependencies": { + // ... + } +} +``` + +## Enable components + +Introduce components in `src/configuration.ts` + +```typescript +// ... +import * as proxy from '@midwayjs/http-proxy'; + +@Configuration({ + imports: [ + // ...other components + proxy, + ], +}) +export class MainConfiguration {} +``` + +## Configuration + +The proxy configuration is defined as follows: + +```typescript +// proxy configuration type +export interface HttpProxyConfig { + // Match the URL regular expression to be represented + match: RegExp; + // Replace the host of the matching link and proxy the request to this address. + host?: string; + // Capture group processing proxy addresses through regular expressions + target?: string; + // The timeout time of the forwarding request. The default time is 0. No timeout time is set. + proxyTimeout?: number; + // Ignore the fields in the header forwarded by the proxy request + ignoreHeaders ?: { + [key: string]: boolean; + }; +} +``` + +Agents support a single agent and multiple agents. + +Single proxy configuration + +```typescript +// src/config/config.default.ts + +export default { + httpProxy: { + match: /\/tfs \//, + host: 'https://gw.alicdn.com', + }, +}; +``` + +Multiple agent configurations + +```typescript +// src/config/config.default.ts + +// Proxy configuration type +export default { + httpProxy: { + default: { + // Some multiplexed values for each policy will be merged with the following policies. + }, + strategy: { + gw: { + // https://gw.alicdn.com/tfs/TB1.1EzoBBh1e4jSZFhXXcC9VXa-48-48.png + match: /\/tfs \//, + host: 'https://gw.alicdn.com', + }, + g: { + // https://g.alicdn.com/mtb/lib-mtop/2.6.1/mtop.js + match: /\/bdimg\/(.*)$ /, + target: 'https://sm.bdimg.com/$1', + }, + httpBin: { + // https://httpbin.org/ + match: /\/httpbin\/(.*)$ /, + target: 'https://httpbin.org/$1', + }, + }, + } +}; +``` + +## Example: Configuring Agents Using host + +```typescript +export default { + httpProxy: { + match: /\/tfs \//, + host: 'https://gw.alicdn.com', + }, +}; +``` + +If the request is `https:// yourdomain.com/tfs/test.png`, the regular expression configured in the `match` field is successfully matched, and the `https:// yourdomain.com` part of the `host` in the original request path is replaced with the configured `https:// gw.alicdn.com`, thus initiating a proxy request to `https:// gw.alicdn.com/tfs/test.png` and return the response to the user requesting your site. + +## Example: Configuring Agents Using target + +```typescript +export default { + httpProxy: { + match: /\/httpbin\/(.*)$ /, + target: 'https://httpbin.org/$1', + }, +}; +``` + +When requesting that your site path is: `https:// yourdomain.com/httpbin/get? When name is set to midway`, the regular expression configured in the `match` field is matched, and the regular capture group has the result `['get?name = midway']`, replace the `$1` part of the original request path with the `get? of the first data (index: 0) in the capture group? name = midway` to initiate a proxy request to `https:// httpbin.org/get? Name = midway` and return the response to the user requesting your site. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/i18n.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/i18n.md new file mode 100644 index 000000000000..c72244bb32e7 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/i18n.md @@ -0,0 +1,636 @@ +# I18n + +Midway provides a multi-language component that allows the business to quickly specify different languages and display different texts. It can also be used in HTTP scenarios with request parameters and request first-class methods. + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + +## Installation Components + +```bash +$ npm i @midwayjs/i18n@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/i18n": "^3.0.0", + // ... + }, +} +``` + + + +## Use components + +Configure the i18n component into the code. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as i18n from '@midwayjs/i18n'; + +@Configuration({ + imports: [ + // ... + i18n + ] +}) +export class MainConfiguration { + //... +} +``` + + + +## Use + +The component provides `MidwayI18nService` services for translating multilingual text. + +Using `translate` method, pass in different text keywords and parameters to return text content in different languages. + +```typescript +@Controller('/') +export class UserController { + + @Inject() + i18nService: MidwayI18nService; + + @Get('/') + async index(@Query('username') username: string) { + return this.i18nService.translate('HELLO_MESSAGE', { + args: { + username + }, + }); + } +} +``` + + + +## Configure i18n messages + +You can configure it directly in the configuration file, but in most cases, there will be a lot of copy, and sometimes it may even be on remote services. Direct configuration is not realistic at this time. + +Generally speaking, we will put the copy into a copy configuration directory, such as `src/locales`. + +Take the `src/locale` directory as an example, let's take an example, the structure is as follows: + +```text +. +├── src +│ ├── locales +| │ ├── en_US.json +| │ └── zh_CN.json +│ └── controller +│ └── home.controller.ts +├── package.json +└── tsconfig.json +``` + +Here we have created two multilingual files, `en_US.json` and `zh_CN.json`, representing English and Chinese respectively. + +The contents of the documents are as follows: + +```json +// src/locales/en_US.json +{ + "hello": "Hello {username} ", + "email": "email id ", + "login": "login account ", + "createdAt": "register date" +} +``` + +```json +// src/locales/zh_CN.json +{ + "hello": "你好 {username}", + "email": "邮箱", + "login": "帐号", + "createdAt": "注册时间" +} +``` + +Each line has a string pair, which is a standard JSON format. You can also use js/ts files. The curly brackets are filled with replaceable parameters. + +At the same time, you need to add these two JSON to the configuration, and `default` property is a default group name. + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // Put your translated text here + localeTable: { + en_US: { + default: require('../locale/en_US'), + }, + zh_CN: { + default: require('../locale/zh_CN'), + } + }, + } +} +``` + +In this way, it can be used. The output is as follows. + +```typescript +this.i18nService.translate('hello', { + args: { + username: 'harry', + }, + locale: 'en_US', +}); + +// output: Hello harry. + +this.i18nService.translate('hello', { + args: { + username: 'harry', + }, + locale: 'zh_CN', +}); + +// output: 你好 harry. + +``` + + + +## I18n message group + +In the following configuration, the multi-language message configured by the user is in the `default` group. + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // Put your translated text here + localeTable: { + en_US: { + default: require('../locale/en_US'), + }, + zh_CN: { + default: require('../locale/zh_CN'), + } + }, + } +} +``` + +The advantage of this is that in other components or business codes, we can also use different group names to add other multilingual texts. + +For example: + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // Put your translated text here + localeTable: { + en_US: { + default: require('../locale/en_US'), + user: require('../locale/user_en_US'), + }, + zh_CN: { + default: require('../locale/zh_CN'), + user: require('../locale/user_zh_CN'), + } + }, + } +} +``` + +In the code, if you call a different group, you need to specify the group parameters. + +```typescript +this.i18nService.translate('user.hello', { + args: { + username: 'harry', + }, + group: 'user', // Specify other groups + locale: 'en_US', +}); + +``` + + + +## I18n message format + +Parameters can be added to multilingual text, and parameters can have two forms: `object` and `array`. + +The object form is as follows, using curly braces as placeholders. + +```text +Hello {username} +``` + +When used, passed by configuration, overriding variables by object key. + +```typescript +async index(@Query('username') username: string) { + return this.i18nService.translate('hello', { + args: { + username + }, + }); +} +``` + +The array form is as follows, using numbers as placeholders. + +```text +Hello {0} +``` + +When used, it is passed through configuration, and the format is in the form of an array, overwriting the numeric variables in the order of the array. + +```typescript +async index(@Query('username') username: string) { + return this.i18nService.translate('hello', { + args: [username] + }); +} +``` + + + +## Dynamically add i18n message + +Sometimes, i18n message may be placed remotely, such as databases, etc., and we can add them dynamically through `addLocale` methods. + +For example, after the configuration is loaded, before the code is used. + +```typescript +// configuration.ts + +// ... +@Configuration({ + imports: [ + koa + i18n + ] +}) +export class MainConfiguration { + + @Inject() + i18nService: MidwayI18nService; + + async onReady() { + this.i18nService.addLocale('zh_TW', { + hello: '你好,{username} 美麗的世界' + }); + } + + + // ... +} +``` + +It can be used in code. + +```typescript +async index(@Query('username') username: string) { + return this.i18nService.translate('hello', { + args: [username] + locale: 'zh_TW' + }); +} +``` + + + +## Specify the current language through parameters + +In general, the default language is `en_US`, and the user's browser will usually have an `Accept-Language` header, so the language will be correctly identified. For example, if you use a Chinese browser to access, Chinese can be displayed normally. + +In addition, in HTTP scenarios, you can specify the language through URL Query, Cookie, and Header. + +By default, you can specify URL Query,Cookie, and Header. + +Priority from top to bottom: + +- query: /?locale=en-US +- cookie: locale=zh-TW +- header: Accept-Language: zh-CN,zh;q=0.5 + +After these parameters are passed, the multilingual data will be automatically saved to the current user's Cookie, and the next request will directly use the set language. + + + +## Set language manually + +The current language can be set by calling the `saveRequestLocale`. + +```typescript +async index() { + // ... + this.i18nService.saveRequestLocale('zh_CN'); +} +``` + +If the `writeCookie` configuration is turned on, the settings will be saved to the current user's Cookie and will be used in the next request. + + + +## Language selection priority + +These multiple ways of setting up languages have different priorities, from high to low: + +- 1. The language explicitly specified by the `i18nService.translate` method +- 2. Languages set by other decorators, such as `@Validate` the parameters of the decorator (essentially calling the `i18nService.translate` method) +- 3. The current language directly set through the `saveRequestLocale` API +- 4. Language set by browser Query,Cookie and Header (essentially, `saveRequestLocale` is called) +- 5. default language in i18n component configuration + + + +## About Language Case + +Inside the code, we will replace all multilingual, fallback rules, written text strings, and returned locale results with the following rules + +- 1. Use the middle dash instead of the underscore +- 2. Use lowercase instead of uppercase + +All `en_US` changes to `en-us` and `zh_CN` changes to `zh-cn`. + +This will safely adapt URL and Cookie. + + + +## Used in View + +In the Web type framework, we add locals variable support by default, which can be used in the template engine. + +Assuming that the template engine we use is [Nunjucks](./render), it can be directly referenced to the `i18n` method. + +The multilingual copy is as follows: + +```json +{ + "hello": "Hello {username} ", +} +``` + +The template is as follows: + +```html +{{ i18n('hello', user) }} +``` + +Examples are as follows: + +```typescript +// ... + +@Controller('/') +export class UserController { + + @Inject() + ctx: Context; + + @Get('/') + async index() { + await this.ctx.render('index', { + // Note that this is the entire object passed to the template + user: { + username: 'harry', + } + }); + } +} +``` + +The i18n method is defined as follows: + +```typescript +function i18n(templateName: string, args: Record) { + // ... +} +``` + +The method name can be modified by configuration. + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + localsField: 'i18n', + } +} +``` + + + + + +## Configuration + +### Default configuration + +In most cases, you only need to add your own multilingual translation `localeTable` the configuration. + +The following is the complete configuration, which you can find in the configuration definition. + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // Default language "en_US" + defaultLocale: 'en_US', + + // Put your translated text here + localeTable: { + en_US: { + // group name + default: { + // hello: 'hello' + } + }, + zh_CN: { + // group name + default: { + // hello: '你好' + } + }, + }, + + // Language mapping, you can use * to match + fallbacks: { + // 'en_* ': ' en_US', + // pt: 'pt-BR', + }, + // Whether to write the request parameter to the cookie + writeCookie: true + resolver: { + // url query parameter, default is "locale" + queryField: 'locale', + cookieField: { + // The key in Cookie is "locale" by default" + fieldName: 'locale', + // Cookie domain name, which is empty by default, indicates that the current domain name is valid. + cookieDomain: '', + // The default expiration time of the cookie. Default is one year. + cookieMaxAge: FORMAT.MS.ONE_YEAR + }, + }, + localsField: 'i18n', + } +} +``` + + + +### Write back to Cookie + +By default, the multilingual component will write back the current user's language to the Cookie to avoid searching for the next request to improve performance. We can turn off this behavior by configuration. + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + writeCookie: false + } +} +``` + + + +### Request resolution configuration + +In the HTTP scenario, we provide the ability to specify the current language through parameters. + +By default, components are found through the fields below. + +- `locale` field of query +- `locale` field of cookie +- `Accept-Language` of header + +We can modify the fields of the query by configuration. + +For example, modify the fields of Query. + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + resolver: { + queryField: 'abc' + }, + } +} +``` + +You can use `/?abc = en-US` to request language changes. + +If you do not want to set the language by request, you can turn off the entire `resolver` parsing and write-back to Cookie will stop at the same time. + +```typescript +// src/config/config.default.ts +export default { + // ... + I18n: { + resolver: false, + } +} +``` + + + + + +## Common language + +| Language | Language package name | +| :--------------- | :------- | +| Arabia | ar_EG | +| Armenia | hy_AM | +| Bulgarian | bg_BG | +| Catalan | ca_ES | +| Czech | cs_CZ | +| Danish | da_DK | +| German | de_DE | +| Greek | el_GR | +| English | en_GB | +| English (American) | en_US | +| Spanish | es_ES | +| Estonian | et_EE | +| Persian | Fa_IR | +| Finnish | fi_FI | +| French (Belgium) | fr_BE | +| French | fr_FR | +| Hebrew | He_IL | +| Hindi | Hi_IN | +| Croatian | hr_HR | +| Hungary | Hu_HU | +| Icelandic | is_IS | +| Indonesian | id_ID | +| Italian | it_IT | +| Japanese | ja_JP | +| Georgian | Ka_GE | +| Kannada | Kn_IN | +| Korean/Korean | ko_KR | +| Kurdish | ku_IQ | +| Latvian | lv_LV | +| Malay | Ms_MY | +| Mongolian | mn_MN | +| Norway | nb_NO | +| Nepali | ne_NP | +| Dutch (Belgium) | nl_BE | +| Dutch | nl_NL | +| Polish | pl_PL | +| Portuguese (Brazil) | pt_BR | +| Portuguese | pt_PT | +| Slovak | sk_SK | +| Serbia | sr_RS | +| Slovenia | sl_SI | +| Swedish | sv_SE | +| Tamil | TA_IN | +| Thai | th_TH | +| Turkish | tr_TR | +| Romanian | RO_RO | +| Russian | ru_RU | +| Ukrainian | uk_UA | +| Vietnamese | vi_VN | +| Simplified Chinese | zh_CN | +| Traditional Chinese | zh_TW | + + +## Common problem + +### 1. The test configuration global language does not take effect + +In general scenarios, you don't need to configure the global language, because browser access will automatically bring language information. For example, a Chinese browser will automatically return Chinese, and an English browser will automatically return English. + +If you specifically wish to test the effects of global settings, be sure to do the following: + +* 1. If you are using a browser, please clear the page cookie before visiting again, because the cookie will record the last user's language information. +* 2. If you are using tools such as Postman, please do not bring cookies and language-related Header, Query and other fields. + +### 2. The language returned by the test is unexpected + +Please use a browser to test, not Postman. + +Since the Postman request does not carry a header related to the browser language, the server cannot automatically determine the language. + +If you must use Postman, please refer to the browser request and add the `Accept-Language` Header. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/info.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/info.md new file mode 100644 index 000000000000..14c4dd3af110 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/info.md @@ -0,0 +1,165 @@ +# Information viewing + +Midway provides the info component to display the basic information of the application and facilitate troubleshooting. + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + +## Installation dependency + +```bash +$ npm i @midwayjs/info@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/info": "^3.0.0", + // ... + }, +} +``` + + + +## Use components + +Configure the info component into the code. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as info from '@midwayjs/info'; + +@Configuration({ + imports: [ + // ... + info + ] +}) +export class MainConfiguration { + //... +} +``` + +In some cases, in order not to let the application information out, we specify that it will take effect in a special environment. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as info from '@midwayjs/info'; + +@Configuration({ + imports: [ + koa + { + component: info + enabledEnvironment: ['local'], // enabled locally only + } + ] +}) +export class MainConfiguration { + //... +} +``` + + + +## View information + +By default, the info component automatically adds a middleware to the Http scenario, which can be accessed by using `/_info`. + +By default, key information such as system, process, and configuration is displayed. + +The effect is as follows: + +![info](https://img.alicdn.com/imgextra/i3/O1CN01TCkSvr28x8T7gtnCl_!!6000000007998-2-tps-797-1106.png) + + + +## Modify access route + +For security, we can adjust the route of access. + +```typescript +// src/config/config.default.ts +export default { + // ... + info: { + infoPath: '/_my_info', + } +} +``` + + + +## Hide information + +By default, the info component hides information such as the secret key. null******** + +Keyword can use wildcard characters, such as adding some keywords. + +```typescript +// src/config/config.default.ts +import { DefaultHiddenKey } from '@midwayjs/info'; + +export default { + // ... + info: { + hiddenKey: DefaultHiddenKey.concat(['*abc', '*def', '*bbb*']), + } +} +``` + + + +## API + +The info component provides `InfoService` by default for use in non-Http or custom scenarios. + +For example: + +```typescript +import { Provide } from '@midwayjs/core'; +import { InfoService } from '@midwayjs/info'; + +@Provide() +export class userService { + + @Inject() + inforService: InfoService + + async getInfo() { + // Application information, application name, etc. + this.inforService.projectInfo(); + // System information + this.inforService.systemInfo(); + // Heap memory, cpu, etc. + this.inforService.resourceOccupationInfo(); + // midway framework information + this.inforService.softwareInfo(); + // The current environment configuration + this.inforService.midwayConfig(); + // Depend on the service injected into the container + this.inforService.midwayService(); + // System time, time zone, startup time + this.inforService.timeInfo(); + // Environment variable + this.inforService.envInfo(); + // Dependency information + this.inforService.dependenciesInfo(); + // Network information + this.inforService.networkInfo(); + } +} +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/jwt.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/jwt.md new file mode 100644 index 000000000000..9c21bda10841 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/jwt.md @@ -0,0 +1,211 @@ +# JWT + +`JSON Web Token` (JWT) is an open standard (RFC 7519) that defines a compact, self-contained method for securely transferring information between parties as a `JSON` object. This information can be verified and trusted because it is digitally signed. + +Midway provides jwt components and simply provides some jwt-related API, which can be used for independent authentication and verification. + +Related information: + +| Description | | +| ----------------- | --- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## Installation dependency + +```bash +$ npm i @midwayjs/jwt@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/jwt": "^3.0.0" + // ... + }, +} +``` + +## Use components + +Configure jwt components into the code. + +```typescript +import { Configuration, IMidwayContainer } from '@midwayjs/core'; +import { IMidwayContainer } from '@midwayjs/core'; +import * as jwt from '@midwayjs/jwt'; + +@Configuration({ + imports: [ + // ... + jwt, + ], +}) +export class MainConfiguration { + // ... +} +``` + +## Basic configuration + +Then set in the configuration, the default is not encrypted. + +```typescript +// src/config/config.default.ts +export default { + // ... + jwt: { + secret: 'xxxxxxxxxxxxxx', // fs.readFileSync('xxxxx.key') + sign: { + // signOptions + expiresIn: '2d', // https://github.com/vercel/ms + }, + verify: { + // verifyOptions + }, + decode: { + // decodeOptions + } + }, +}; +``` + +for more configurations, see the ts definition. + +## Common API + +Midway provides jwt common API as synchronous and asynchronous. + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { JwtService } from '@midwayjs/jwt'; + +@Provide() +export class UserService { + @Inject() + jwtService: JwtService; + + async invoke() { + // Synchronization API + this.jwtService.signSync(payload, secretOrPrivateKey, options); + this.jwtService.verifySync(token, secretOrPublicKey, options); + this.jwtService.decodeSync(token, options); + + // Asynchronous API + await this.jwtService.sign(payload, secretOrPrivateKey, options); + await this.jwtService.verify(token, secretOrPublicKey, options); + await this.jwtService.decode(token, options); + } +} +``` + +These APIs are all from the basic [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) library. If you don't know, please read the original document. + +## Middleware example + +In general, jwt will also cooperate with middleware to complete authentication. The following is an example of custom jwt authentication middleware. + +```typescript +// src/middleware/jwt.middleware + +import { Inject, Middleware, httpError } from '@midwayjs/core'; +import { Context, NextFunction } from '@midwayjs/koa'; +import { JwtService } from '@midwayjs/jwt'; + +@Middleware() +export class JwtMiddleware { + @Inject() + jwtService: JwtService; + + public static getName(): string { + return 'jwt'; + } + + resolve() { + return async (ctx: Context, next: NextFunction) => { + // Judge whether there is verification information + if (! ctx.headers['authorization']) { + throw new httpError.UnauthorizedError(); + } + // Get verification information from header + const parts = ctx.get('authorization').trim().split(' '); + + if (parts.length !== 2) { + throw new httpError.UnauthorizedError(); + } + + const [scheme, token] = parts; + + if (/^Bearer$/i.test(scheme)) { + try { + // jwt.verify that token is valid. + await this.jwtService.verify(token, { + complete: true + }); + } catch (error) { + // The token expires and generates a new token. + const newToken = getToken(user); + // Put the new token into the Authorization and return it to the front end. + ctx.set('Authorization', newToken); + } + await next(); + } + }; + } + + // Configure route addresses that ignore authentication + public match(ctx: Context): boolean { + const ignore = ctx.path.indexOf('/api/admin/login') !== -1; + return !ignore; + } +} +``` + +Then enable middleware at the portal. + + +```typescript +// src/configuration.ts + +import { Configuration, App, IMidwayContainer, IMidwayApplication} from '@midwayjs/core'; +import * as jwt from '@midwayjs/jwt'; + +@Configuration({ + imports: [ + // ... + jwt + ], +}) +export class MainConfiguration { + + @App() + app: IMidwayApplication; + + async onReady(applicationContext: IMidwayContainer): Promise { + // Add middleware + this.app.useMiddleware ([ + // ... + JwtMiddleware + ]); + } +} +``` + + + +## Original JWT object + +Objects and methods on the original instance can be referenced through the exported `Jwt` object. + +```typescript +import { Jwt } from '@midwayjs/jwt'; + +// Jwt.TokenExpiredError +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/kafka.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/kafka.md new file mode 100644 index 000000000000..ad567e039c0d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/kafka.md @@ -0,0 +1,573 @@ +# Kafka + +In the architecture of complex systems, event streams are a crucial part, including capturing data in real-time from event sources (databases, sensors, mobile devices, etc.) as event streams, persisting event streams for easy retrieval, and processing and responding to event streams in real-time and retrospectively. + +Applicable to industries such as payment and financial transactions, implementing tracking and monitoring of automotive information flow, capturing and analyzing IoT data, etc. + +In Midway, we provide the ability to subscribe to Kafka to meet such user needs. + +Related Information: + +**Subscription Service** + +| Description | | +| ----------------- | ---- | +| Available for standard projects | ✅ | +| Available for Serverless | ❌ | +| Available for integrated projects | ✅ | +| Includes standalone main framework | ✅ | +| Includes standalone logging | ✅ | + +## Basic Concepts + +Distributed stream processing platform +* Publish-subscribe (stream) information +* Fault-tolerant (failover) storage of information (streams), storing event streams +* Process event streams as they occur + +Understanding Producer + +* Publish messages to one or more topics. + +Understanding Consumer +* Subscribe to one or more topics and process the generated information. + +Understanding Stream API +* Acts as a stream processor, consuming input streams from one or more topics and producing an output stream to one or more output topics, effectively transforming input streams into output streams. + +Understanding Broker +* Published messages are stored in a set of servers called a Kafka cluster. Each server in the cluster is a broker. Consumers can subscribe to one or more topics and pull data from brokers to consume these published messages. + +![image.png](https://kafka.apache.org/images/streams-and-tables-p1_p4.png) + +:::tip +From v3.19, the Kafka component has been refactored, and the configuration and usage methods of the Kafka component have changed significantly from before. The original usage method is compatible, but the documentation is no longer retained. +::: + +## Install Dependencies + +Install the `@midwayjs/kafka` module. + +```bash +$ npm i @midwayjs/kafka --save +``` + +Or add the following dependency to `package.json` and reinstall. + +```json +{ + "dependencies": { + "@midwayjs/kafka": "^3.0.0", + // ... + } +} +``` + +## Enable Component + +`@midwayjs/kafka` can be used as a standalone main framework. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as kafka from '@midwayjs/kafka'; + +@Configuration({ + imports: [ + kafka + ], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} +``` + +It can also be attached to other main frameworks, such as `@midwayjs/koa`. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as kafka from '@midwayjs/kafka'; + +@Configuration({ + imports: [ + koa, + kafka + ], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} +``` + +Since Kafka is divided into **Consumer** and **Producer** parts, both can be used independently, and we will introduce them separately. + +## Consumer + +### Directory Structure + +We usually place consumers in the consumer directory, such as `src/consumer/user.consumer.ts`. +``` +➜ my_midway_app tree +. +├── src +│ ├── consumer +│ │ └── user.consumer.ts +│ ├── interface.ts +│ └── service +│ └── user.service.ts +├── test +├── package.json +└── tsconfig.json +``` + +### Basic Configuration + +We can configure multiple consumers through the `consumer` field and the `@KafkaConsumer` decorator. + +For example, `sub1` and `sub2` below are two different consumers. + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + // ... + }, + sub2: { + // ... + }, + } + } +} +``` + +The simplest consumer configuration requires several fields: Kafka connection configuration, consumer configuration, and subscription configuration. + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + connectionOptions: { + // ... + }, + consumerOptions: { + // ... + }, + subscribeOptions: { + // ... + }, + }, + } + } +} +``` + +For example: + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + connectionOptions: { + clientId: 'my-app', + brokers: ['localhost:9092'], + }, + consumerOptions: { + groupId: 'groupId-test-1', + }, + subscribeOptions: { + topics: ['topic-test-1'], + } + }, + } + } +} +``` + +Complete configurable parameters include: + +- `connectionOptions`: Kafka connection configuration, i.e., parameters for `new Kafka(consumerOptions)` +- `consumerOptions`: Kafka consumer configuration, i.e., parameters for `kafka.consumer(consumerOptions)` +- `subscribeOptions`: Kafka subscription configuration, i.e., parameters for `consumer.subscribe(subscribeOptions)` +- `consumerRunConfig`: Consumer run configuration, i.e., parameters for `consumer.run(consumerRunConfig)` + +For detailed explanations of these parameters, refer to the [KafkaJS Consumer](https://kafka.js.org/docs/consuming) documentation. + +### Reuse Kafka Instance + +If you need to reuse a Kafka instance, you can specify it through the `kafkaInstanceRef` field. + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + connectionOptions: { + clientId: 'my-app', + brokers: ['localhost:9092'], + }, + consumerOptions: { + groupId: 'groupId-test-1', + }, + subscribeOptions: { + topics: ['topic-test-1'], + } + }, + sub2: { + kafkaInstanceRef: 'sub1', + consumerOptions: { + groupId: 'groupId-test-2', + }, + subscribeOptions: { + topics: ['topic-test-2'], + } + } + } + } +} +``` + +Note that `sub1` and `sub2` above are two different consumers, but they share the same Kafka instance, and `sub2`'s `groupId` needs to be different from `sub1`. + +The Kafka SDK writing is similar to the following: + +```typescript +const kafka = new Kafka({ + clientId: 'my-app', + brokers: ['localhost:9092'], +}); + +const consumer1 = kafka.consumer({ groupId: 'groupId-test-1' }); +const consumer2 = kafka.consumer({ groupId: 'groupId-test-2' }); +``` + +### Consumer Implementation + +We can provide a standard consumer implementation in the directory, such as `src/consumer/sub1.consumer.ts`. + +```typescript +// src/consumer/sub1.consumer.ts +import { KafkaConsumer, IKafkaConsumer, EachMessagePayload } from '@midwayjs/kafka'; + +@KafkaConsumer('sub1') +class Sub1Consumer implements IKafkaConsumer { + async eachMessage(payload: EachMessagePayload) { + // ... + } +} +``` + +`sub1` is the consumer name, using the `sub1` consumer in the configuration. + +You can also implement the `eachBatch` method to process batch messages. + +```typescript +// src/consumer/sub1.consumer.ts +import { KafkaConsumer, IKafkaConsumer, EachBatchPayload } from '@midwayjs/kafka'; + +@KafkaConsumer('sub1') +class Sub1Consumer implements IKafkaConsumer { + async eachBatch(payload: EachBatchPayload) { + // ... + } +} +``` + +### Message Context + +Like other message subscription mechanisms, the message itself is passed through the `Context` field. + +```typescript +// src/consumer/sub1.consumer.ts +import { KafkaConsumer, IKafkaConsumer, EachMessagePayload, Context } from '@midwayjs/kafka'; +import { Inject } from '@midwayjs/core'; + +@KafkaConsumer('sub1') +class Sub1Consumer implements IKafkaConsumer { + + @Inject() + ctx: Context; + + async eachMessage(payload: EachMessagePayload) { + // ... + } +} +``` + +The `Context` field includes several properties: + +| Property | Type | Description | +| ------------ | ------------------------------ | ---------------- | +| ctx.payload | EachMessagePayload, EachBatchPayload | Message content | +| ctx.consumer | Consumer | Consumer instance | + +You can call Kafka's API through `ctx.consumer`, such as `ctx.consumer.commitOffsets` to manually commit offsets or `ctx.consumer.pause` to pause consumption. + +## Producer + +### Basic Configuration + +Service producers also need to create instances, and the configuration itself uses the [Service Factory](/docs/service_factory) design pattern. + +The configuration is as follows: + +```typescript +// src/config/config.default +export default { + kafka: { + producer: { + clients: { + pub1: { + // ... + }, + pub2: { + // ... + } + } + } + } +} +``` + +Each Producer instance's configuration also includes `connectionOptions` and `producerOptions`. + +```typescript +// src/config/config.default +export default { + kafka: { + producer: { + clients: { + pub1: { + connectionOptions: { + clientId: 'my-app', + brokers: ['localhost:9092'], + }, + producerOptions: { + // ... + } + } + } + } + } +} +``` + +For specific parameters, refer to the [KafkaJS Producer](https://kafka.js.org/docs/producing) documentation. + +Additionally, since Kafka Consumer and Producer can both be created from the same Kafka instance, they can reuse the same Kafka instance. + +If the Producer is created after the Consumer, it can also reuse the Kafka instance using the `kafkaInstanceRef` field. + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + connectionOptions: { + clientId: 'my-app', + brokers: ['localhost:9092'], + }, + } + }, + producer: { + clients: { + pub1: { + kafkaInstanceRef: 'sub1', + } + } + } + } +} +``` + +### Using Producer + +There is no default instance for Producer. Since the service factory design pattern is used, it can be injected through `@InjectClient()`. + +```typescript +// src/service/user.service.ts +import { Provide, InjectClient } from '@midwayjs/core'; +import { KafkaProducerFactory, Producer } from '@midwayjs/kafka'; + +@Provide() +export class UserService { + + @InjectClient(KafkaProducerFactory, 'pub1') + producer: Producer; + + async invoke() { + await this.producer.send({ + topic: 'topic-test-1', + messages: [{ key: 'message-key1', value: 'hello consumer 11 !' }], + }); + } +} +``` + +## Admin + +Kafka's Admin functionality can be used to create, delete, view topics, view configurations, and ACLs, etc. + +### Basic Configuration + +Like Producer, Admin also uses the service factory design pattern. + +```typescript +// src/config/config.default +export default { + kafka: { + admin: { + clients: { + admin1: { + // ... + } + } + } + } +} +``` + +Similarly, Admin can also reuse the Kafka instance. + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + connectionOptions: { + clientId: 'my-app', + brokers: ['localhost:9092'], + }, + } + }, + admin: { + clients: { + admin1: { + kafkaInstanceRef: 'sub1', + } + } + } + } +} +``` + +### Using Admin + +There is no default instance for Admin. Since the service factory design pattern is used, it can be injected through `@InjectClient()`. + +```typescript +// src/service/admin.service.ts +import { Provide, InjectClient } from '@midwayjs/core'; +import { KafkaAdminFactory, Admin } from '@midwayjs/kafka'; + +@Provide() +export class AdminService { + + @InjectClient(KafkaAdminFactory, 'admin1') + admin: Admin; +} +``` + +For more Admin usage methods, refer to the [KafkaJS Admin](https://kafka.js.org/docs/admin) documentation. + +## Component Logging + +The Kafka component uses the `kafkaLogger` log by default, which will record `ctx.logger` in `midway-kafka.log`. + +You can modify it through configuration. + +```typescript +// src/config/config.default +export default { + midwayLogger: { + clients: { + kafkaLogger: { + fileLogName: 'midway-kafka.log', + }, + }, + }, +} +``` + +The output format of this log can also be configured separately. + +```typescript +export default { + kafka: { + // ... + contextLoggerFormat: info => { + const { jobId, from } = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.message}`; + }, + } +} +``` + +## Access KafkaJS Module + +The KafkaJS module can be accessed through the `KafkaJS` field of `@midwayjs/kafka`. + +```typescript +import { KafkaJS } from '@midwayjs/kafka'; + +const { ConfigResourceTypes } = KafkaJS; +// ... +``` + +## Warning About Partitions + +If you are using KafkaJS version v2.0.0, you may see the following warning: + +``` +2024-11-04 23:47:28.228 WARN 31729 KafkaJS v2.0.0 switched default partitioner. To retain the same partitioning behavior as in previous versions, create the producer with the option "createPartitioner: Partitioners.LegacyPartitioner". See the migration guide at https://kafka.js.org/docs/migration-guide-v2.0.0#producer-new-default-partitioner for details. Silence this warning by setting the environment variable "KAFKAJS_NO_PARTITIONER_WARNING=1" { timestamp: '2024-11-04T15:47:28.228Z', logger: 'kafkajs' } +``` + +This warning is due to KafkaJS version v2.0.0 using a new partitioner by default. If you accept the new partitioner behavior but want to turn off this warning message, you can eliminate this warning by setting the environment variable `KAFKAJS_NO_PARTITIONER_WARNING=1`. + +Or explicitly declare the partitioner. + +```typescript +// src/config/config.default +import { KafkaJS } from '@midwayjs/kafka'; +const { Partitioners } = KafkaJS; + +export default { + kafka: { + producer: { + clients: { + pub1: { + // ... + producerOptions: { + createPartitioner: Partitioners.DefaultPartitioner, + // ... + createPartitioner: Partitioners.LegacyPartitioner, + }, + }, + }, + }, + } +} +``` + +It is recommended to check the KafkaJS v2.0.0 [migration guide](https://kafka.js.org/docs/migration-guide-v2.0.0#producer-new-default-partitioner) for more details. + +## Reference Documentation + +- [KafkaJS](https://kafka.js.org/docs/introduction) +- [Apache Kafka Official Website](https://kafka.apache.org/intro) diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/koa.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/koa.md new file mode 100644 index 000000000000..67e683597d18 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/koa.md @@ -0,0 +1,494 @@ +# Koa + +Koa is a very lightweight and easy-to-use Web framework. The content of this chapter mainly introduces how to use Koa as the upper-level framework in Midway and use its own capabilities. + +Midway's default examples are based on this package. + +The `@midwayjs/koa` package uses `koa @2` and integrates `@koa/router` as the basic routing capability by default, and has built-in `session` and `body-parser` functions by default. + +| Description | | +| -------------- | ---- | +| Contains independent main framework | ✅ | +| Contains independent logs | ✅ | + + + +## Installation dependency + +```bash +$ npm i @midwayjs/koa@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/koa": "^3.0.0", + // ... + }, +} +``` + +Examples can also be created directly using scaffolding. + +```bash +# npm v6 +$ npm init midway --type=koa-v3 my_project + +# npm v7 +$ npm init midway -- --type=koa-v3 my_project +``` + + + +## Enable component + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { join } from 'path'; + +@Configuration({ + imports: [koa] + importConfigs: [join(__dirname, './config')] +}) +export class MainConfiguration { + @App() + app: koa.Application; + + async onReady() { + // ... + } +} + +``` + + + +## BodyParser + +`@midwayjs/koa` has its own `bodyParser` function, which will parse `Post` requests by default and automatically recognize `json` and `form` types. + +If you need text or xml, you can configure it yourself. + +The default size is limited to `1mb`. You can set the size of each item separately. + +```typescript +// src/config/config.default +export default { + // ... + bodyParser: { + enableTypes: ['json', 'form', 'text', 'xml'] + formLimit: '1mb', + jsonLimit: '1mb', + textLimit: '1mb', + xmlLimit: '1mb', + }, +} +``` + +Note that the type selection when using Postman for Post requests: + +![postman](https://img.alicdn.com/imgextra/i4/O1CN01QCdTsN1S347SuzZU5_!!6000000002190-2-tps-1017-690.png) + +Disable bodyParser middleware。 + +```typescript +// src/config/config.default +export default { + // ... + bodyParser: { + enable: false, + // ... + }, +} +``` + +## Cookie and Session + +`@midwayjs/koa` encapsulates `cookies` parsing and `Session` support by default. You can view [Cookies and Session](../cookie_session). + + + +## Extended Context + +In some scenarios, the Context needs to be expanded. + +If you want to hang some temporary request-related object data, you can use the `ctx.setAttr(key, value)` API to implement it, such as the data used by the component. + +If you really want to extend the Context, you can use koa's own API. + +For example, we extend in `configuration.ts` to provide a `render()` method. + +```typescript +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App() + app: koa.Application; + + async onReady(container) { + Object.defineProperties(app.context, { + render: { + value: async function (...args) { + // ... + }, + }, + }); + } +} +``` + +However, this cannot directly allow the Context to include Typescript Definitions. Additional definitions are required. Please refer to [extended context definitions](../context_definition). + + + +## Get Http Server + +In some special cases, you need to get the original Http Server, we can get it after the server starts. + +```typescript +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + framework: koa.Framework; + + async onServerReady(container) { + const server = this.framework.getServer(); + // ... + } +} +``` + + + +## State type definition + +There is a special State attribute in koa's Context, and the State definition can be extended in a similar way to Context. + +```typescript +// src/interface.ts + +declare module '@midwayjs/koa/dist/interface' { + interface Context { + abc: string; + } + + interface State{ + bbb: string; + ccc: number; + } +} +``` + + + + + +## Configuration + + + +### Default configuration + +The configuration sample of `@midwayjs/Koa` is as follows: + +```typescript +// src/config/config.default +export default { + // ... + koa: { + port: 7001 + }, +} +``` + +All attributes are described as follows: + +| Property | Type | Description | +| ------------ | ----------------------------------------- | ------------------------------------------------------- | +| port | `number` | Optional, port to start | +| globalPrefix | `string` | Optional, the global http prefix | +| keys | `string[]` | Optional, Cookies signature, if the upper layer does not write keys, you can also set it here | +| hostname | `string` | Optional. The hostname to listen to. Default 127.1 | +| key | `string \| Buffer \| Array` | Optional, Https key, server private key | +| cert | `string \| Buffer \| Array` | Optional, Https cert, server certificate | +| ca | `string \| Buffer \| Array` | Optional, Https ca | +| http2 | `boolean` | Optional, supported by http2, default false | +| proxy | `boolean` | Optional, whether to enable the proxy. If it is true, the IP in the request request will be obtained first from the X-Forwarded-For in the Header field. The default is false. | +| subdomainOffset | `number` | Optional, the offset of the subdomain name, default 2. | +| proxyIpHeader | `string` | Optional. obtains the field name of the proxy ip address. The default value is X-Forwarded-For | +| maxIpsCount | `number` | Optional. the maximum number of ips obtained, which is 0 by default. | +| serverTimeout | `number` | Optional, server timeout configuration, the default is 2 * 60 * 1000 (2 minutes), in milliseconds | +| serverOptions | `Record` | Optional,http Server [Options](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener) | + + + +### Modify port + +By default, we provide the `7001` default port parameter in `config.default`. by modifying it, we can modify the default port of koa http service. + +For example, we changed it to `6001`: + +```typescript +// src/config/config.default +export default { + // ... + koa: { + port: 6001 + }, +} +``` + +By default, our port configuration is `null` because the single-test environment requires supertest to start the port. + +```typescript +// src/config/config.unittest +export default { + // ... + koa: { + port: null + }, +} +``` + +In addition, you can also temporarily modify the port by `midway-bin dev-ts-port = 6001`, which overwrites the configured port. + + + +### Global prefix + +For more information about this feature, see [Global Prefixes](../controller# Global Routing Prefix). + + + +### Reverse proxy configuration + +If you use a reverse proxy such as Nginx, please enable the `proxy` configuration. + +```typescript +// src/config/config.default +export default { + // ... + koa: { + proxy: true, + }, +} +``` + +The `X-Forwarded-For` Header is used by default. If the proxy configuration is different, please configure a different Header yourself. + +```typescript +// src/config/config.default +export default { + // ... + koa: { + proxy: true, + proxyIpHeader: 'X-Forwarded-Host' + }, +} +``` + + + +### Https configuration + +In most cases, please use external agents as much as possible to complete the implementation of Https, such as Nginx. + +In some special scenarios, you can directly turn on Https by configuring SSL certificates (TLS certificates). + +First, you must prepare certificate files in advance, such as `ssl.key` and `ssl.pem`. The key is the private key of the server and the pem is the corresponding certificate. + +Then configure it. + +```typescript +// src/config/config.default +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export default { + // ... + koa: { + key: join(__dirname, '../ssl/ssl.key') + cert: join(__dirname, '../ssl/ssl.pem') + }, +} +``` + + + +### favicon settings + +By default, the browser will initiate a request to `favicon.ico`. + +The framework provides a default middleware to handle the request, and you can specify a `favicon` Buffer. + +```typescript +// src/config/config.default +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export default { + // ... + siteFile: { + favicon: readFileSync(join(__dirname, '../static/fav.ico')) + }, +} +``` + +If the `@midwayjs/static-file` component is turned on, static file hosting of the component will be preferred. + +Disable middleware。 + +```typescript +// src/config/config.default +export default { + // ... + siteFile: { + enable: false, + // ... + }, +} +``` + +### Modify context log + +The context log of the koa framework can be modified separately. + +```typescript +export default { + koa: { + contextLoggerFormat: info => { + const ctx = info.ctx; + return '${info.timestamp} ${info.LEVEL} ${info.pid} [${ctx.userId} - ${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}'; + } + // ... + }, +}; +``` + + + +### Query array parsing + +By default, koa uses `querystring` to parse query parameters, and when it encounters an array, it will split the data in the array. + +for example: + +``` +GET /query?a[0]=1&a[1]=2 +``` + +The result obtained is: + +```json +{ + "a[0]": 1, + "a[1]": 2, +} +``` + +The framework provides some parameters to handle this situation. + +```typescript +// src/config/config.default +export default { + // ... + koa: { + queryParseMode: 'extended', + // ... + }, +} +``` + +The `queryParseMode` parameter can choose from three values: `extended`, `strict`, and `first`. + + When `queryParseMode` has a value, the `qs` module will be used to process the query, and the effect is the same as the `koa-qs` module. + +When the request parameter is `/query?a=1&b=2&a=3&c[0]=1&c[1]=2'`. + +Default effect (using `querystring`) + +```JSON +{ + "a": ["1", "3" ], + "b": "2", + "c[0]": "1", + "c[1]": "2" +} +``` + + `extended` effect + +```JSON +{ + "a": ["1", "3" ], + "b": ["2"], + "c": ["1", "2"] +} +``` + + `strict` effect + +```JSON +{ + "a": ["1", "3" ], + "b": "2", + "c": ["1", "2"] +} +``` + + `first` effect + +```JSON +{ + "a": "1", + "b": "2", + "c": "1" +} + +### Timeout Configuration + +RequestTimeout and ServerTimeout are two different timeout scenarios. + +- `serverTimeout`: Used to set the timeout for the server to wait for the client to send data after receiving a request. If the client does not send any data within this time, the server will close the connection. This timeout applies to the entire request-response cycle, including request headers, request body, and response. +- `requestTimeout`: Used to set the timeout for the server to wait for the client to send a complete request. This timeout applies specifically to request headers and request body. If the complete request is not received within this time, the server will abort the request. + +By default, `serverTimeout` is set to 0 and does not trigger a timeout. + +If needed, you can modify the configuration by specifying the timeout in milliseconds. + + +```typescript +// src/config/config.default +export default { + // ... + koa: { + serverTimeout: 100_000 + }, +} +``` + +If you encounter the `ERR_HTTP_REQUEST_TIMEOUT` error, it means that the `requestTimeout` has been triggered, which defaults to `300_000` (5 minutes) in milliseconds. You can modify this timeout by configuring as follows. + +```typescript +// src/config/config.default +export default { + // ... + koa: { + serverOptions: { + requestTimeout: 600_000 + } + }, +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/mikro.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/mikro.md new file mode 100644 index 000000000000..96c2362c57b9 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/mikro.md @@ -0,0 +1,502 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# MikroORM + +This section describes how users use MikroORM in midway. MikroORM is the TypeScript ORM of Node.js based on data mapper, working unit and identity mapping mode. + +The MikroORM official website document is [here](https://mikro-orm.io/docs). + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## About upgrade + +* Starting from the `v3.14.0` version of the component, mikro v5/v6 versions are supported. Since there are major changes from mikro v5 to v6, if you want to upgrade from an old version of mikro, please read [Upgrading from v5 to v6](https:/ /mikro-orm.io/docs/upgrading-v5-to-v6) +* Component examples updated to v6 + + + +## Installation Components + + +Install mikro components to provide access to mikro-orm. + + +```bash +$ npm i @midwayjs/mikro@3 @mikro-orm/core --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/mikro": "^3.0.0", + "@mikro-orm/core": "^6.0.2", + // ... + }, + "devDependencies": { + // ... + } +} +``` + +At the same time, it is also necessary to introduce the adaptation package of the corresponding database. + +For example: + +```typescript +{ + "dependencies": { + // sqlite + "@mikro-orm/sqlite": "^6.0.2", + + // mysql + "@mikro-orm/mysql": "^6.0.2", + }, + "devDependencies": { + // ... + } +} +``` + +For more information about drivers, see [Official documentation](https://mikro-orm.io/docs/usage-with-sql/). + + + +## Introducing components + + +The mikro component is introduced in `src/configuration.ts`, as an example. + +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as mikro from '@midwayjs/mikro'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + Mikro // load mikro components + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + + + +## Basic use + +Similar to other orm frameworks, they are divided into several steps: + +- 1. Define Entity +- 2. Configure the data source +- 3. Get the EntityModel to call + +For more information about Entity code, see [Example](https://github.com/midwayjs/midway/tree/main/packages/mikro/test/fixtures/base-fn-origin). + + +### Directory structure + +A basic reference directory structure is as follows. + + +``` +MyProject +├── src +│ ├── config +│ │ └── config.default.ts +│ ├── entity +│ │ ├── book.entity.ts +│ │ ├── index.ts +│ │ └── base.ts +│ ├── configuration.ts +│ └── service +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + +### Define Entity + +Entity that defines the basis. + +```typescript +// src/entity/BaseEntity.ts +import { PrimaryKey, Property } from '@mikro-orm/core'; + +export abstract class BaseEntity { + + @PrimaryKey() + id! : number; + + @Property() + createdAt: Date = new Date(); + + @Property({ onUpdate: () => new Date() }) + updatedAt: Date = new Date(); + +} +``` + +Define the actual Entity, including one-to-many, many-to-many relationships. + +```typescript +// src/entity/book.entity.ts +import { Cascade, Collection, Entity, ManyToMany, ManyToOne, Property } from '@mikro-orm/core'; +import { Author, BookTag, Publisher } from './index'; +import { BaseEntity } from './base'; + +@Entity() +export class Book extends BaseEntity { + + @Property() + title: string; + + @ManyToOne(() => Author) + author: Author; + + @ManyToOne(() => Publisher, { cascade: [Cascade.PERSIST, Cascade.REMOVE], nullable: true }) + publisher?: Publisher; + + @ManyToMany(() => BookTag) + tags = new Collection(this); + + @Property({ nullable: true }) + metaObject?: object; + + @Property({ nullable: true }) + metaArray?: any[]; + + @Property({ nullable: true }) + metaArrayOfStrings?: string[]; + + constructor(title: string, author: Author) { + super(); + this.title = title; + this.author = author; + } + +} +``` + + + +### Configure the data source + +mikro v5 and v6 are slightly different. + + + + +```typescript +// src/config/config.default +import { Author, BaseEntity, Book, BookTag, Publisher } from '../entity'; +import { join } from 'path'; +import { SqliteDriver } from '@mikro-orm/sqlite'; + +export default (appInfo) => { + return { + mikro: { + dataSource: { + default: { + dbName: join(__dirname, '../../test.sqlite'), + driver: SqliteDriver, // SQLite is used as an example here. + allowGlobalContext: true, + // Object format + entities: [Author, Book, BookTag, Publisher, BaseEntity], + // The following scanning form is supported. For compatibility, we can match both .js and .ts files at the same time + entities: [ + 'entity', // Specify the directory + '**/entity/*.entity.{j,t}s', // Wildcard with suffix matching + ], + } + } + } + } +} +``` + + + + + +```typescript + +// src/config/config.default +import { Author, BaseEntity, Book, BookTag, Publisher } from '../entity'; +import { join } from 'path'; + +export default (appInfo) => { + return { + mikro: { + dataSource: { + default: { + dbName: join(__dirname, '../../test.sqlite') + Type: 'sqlite', // SQLite is used as an example here. + allowGlobalContext: true, + // Object format + entities: [Author, Book, BookTag, Publisher, BaseEntity], + // The following scanning form is supported. For compatibility, we can match both .js and .ts files at the same time + entities: [ + 'entity', // Specify the directory + '**/entity/*.entity.{j,t}s', // Wildcard with suffix matching + ], + } + } + } + } +} + +``` + + + + +:::tip + +The `entities` field configuration of mikro has been processed by the framework, please do not refer to the original document. + +::: + + + +### CRUD Operations + +Use `InjectRepository` to inject `Repository` to perform simple query operations. And use `InjectEntityManager` to get the instance of `EntityManager`, to perform creating, updating and deleting operations. +You can also get `EntityManager` by calling `repository.getEntityManger()`. + +:::caution + +* 1. Since v5.7, `persist` and `flush` etc. on `Repository` (shortcuts to methods on `EntityManager`) were marked as *deprecated*, and [planned to remove them in v6](https://github.com/mikro-orm/mikro-orm/discussions/3989). Please use those APIs on `EntityManger` directly instead of on `Repository`. +* 2. v6 has been completely [deprecated](https://mikro-orm.io/docs/upgrading-v5-to-v6#removed-methods-from-entityrepository) the above interface + +::: + +```typescript +// src/service/book.service.ts +import { Book } from './entity/book.entity'; +import { Provide } from '@midwayjs/core'; +import { InjectEntityManager, InjectRepository } from '@midwayjs/mikro'; +import { QueryOrder } from '@mikro-orm/core'; +import { EntityManager, EntityRepository } from '@mikro-orm/mysql'; // should be imported from driver specific packages + +@Provide() +export class BookService { + + @InjectRepository(Book) + bookRepository: EntityRepository; + + @InjectEntityManager() + em: EntityManager; + + async queryByRepo() { + // query with Repository + const books = await this.bookRepository.findAll({ + populate: ['author'], + orderBy: { title: QueryOrder.DESC }, + limit: 20, + }); + return books; + } + + async createBook() { + const book = new Book({ title: 'b1', author: { name: 'a1', email: 'e1' } }); + // mark book as persisted + this.em.persist(book); + // persist all changes to database + await this.em.flush(); + return book; + } +} +``` + +## Advanced Features + +### Get data source + +The data source is the created data source object, which we can obtain by injecting the built-in data source manager. + +```typescript +import { Configuration } from '@midwayjs/core'; +import { MikroDataSourceManager } from '@midwayjs/mikro'; + +@Configuration({ + //... +}) +export class MainConfiguration { + + async onReady(container: IMidwayContainer) { + const dataSourceManager = await container. getAsync(MikroDataSourceManager); + const orm = dataSourceManager.getDataSource('default'); + const connection = orm.em.getConnection(); + //... + } +} +``` + +Starting with v3.8.0, it is also possible to inject via a decorator. + +```typescript +import { Configuration } from '@midwayjs/core'; +import { InjectDataSource } from '@midwayjs/mikro'; +import { MikroORM, IDatabaseDriver, Connection } from '@mikro-orm/core'; + +@Configuration({ + //... +}) +export class MainConfiguration { + + // Inject the default data source + @InjectDataSource() + defaultDataSource: MikroORM>; + + // inject custom data source + @InjectDataSource('default1') + customDataSource: MikroORM>; + + async onReady(container: IMidwayContainer) { + //... + } +} +``` + + + +### Logging + +Midway's logger can be added to mikro through configuration to record SQL and other information. + +```typescript +// src/config/config.default.ts +exporg default { + midwayLogger: { + clients: { + mikroLogger: { + // ... + } + } + }, + mikro: { + dataSource: { + default: { + entities: [Author, Book, BookTag, Publisher, BaseEntity], + // ... + logger: 'mikroLogger', + } + }, + } +} +``` + +By default mikro comes with colors and also writes them to files, which can be turned off through configuration. + +```typescript +// src/config/config.default.ts +exporg default { + midwayLogger: { + clients: { + mikroLogger: { + transports: { + console: { + autoColors: false, + }, + file: { + fileLogName: 'mikro.log', + }, + }, + } + } + }, + mikro: { + dataSource: { + default: { + entities: [Author, Book, BookTag, Publisher, BaseEntity], + // ... + logger: 'mikroLogger', + colors: false, + } + }, + } +} +``` + + + +## Frequently Asked Questions + + + +### 1. Node version + +Mikro-orm there are some restrictions on Node version, it must be `>= 14.0.0`, so do the usage rules of `@midwayjs/mikro` components. + + + +### 2. Identity Map + +Mikro-orm internal query has a concept of [Identity Map](https://mikro-orm.io/docs/identity-map). Midway has added this function to all built-in Framework middleware. If it is used in non-requesting link call scenarios, such as `src/configuration`, the `allowGlobalContext` option can be turned on. + + + +### 3. Multi-dataSource support + +Like other databases, Midway supports the configuration of multiple data sources. + +```typescript +// src/config/config.default +import { Author, BaseEntity, Book, BookTag, Publisher } from '../entity'; +import { SqlHighlighter } from '@mikro-orm/sql-highlighter'; +import { join } from 'path'; + +export default (appInfo) => { + return { + mikro: { + dataSource: { + custom1: { + // ... + }, + custom2: { + // ... + } + } + } + } +} +``` + +Note which data source you need to pass from when using. + +```typescript +// ... + +@Provide() +export class BookController { + + @InjectRepository(Book, 'custom1') + bookRepository: EntityRepository; + + async findBookAndQuery() { + // ... + } +} +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/mongodb.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/mongodb.md new file mode 100644 index 000000000000..cf592586c4ae --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/mongodb.md @@ -0,0 +1,611 @@ +# MongoDB + +In this chapter, we choose [Typegoose](https://github.com/typegoose/typegoose) as the base MongoDB ORM library. As he describes "Define Mongoose models using TypeScript classes", it works well with TypeScript. + +Simply put, Typegoose using TypeScript "wrappers" to write Mongoose models, most of its capabilities are provided by [mongoose](https://www.npmjs.com/package/mongoose) libraries. + +You can also directly select the [mongoose](https://www.npmjs.com/package/mongoose) library to use, and we will describe it separately. + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + +:::tip + +- 1. The current module has been reconfigured since v3.4.0, and the historical writing method is compatible. For more information about how to query historical documents, see [here](../legacy/mongodb). +- 2. If there is a read configuration in the code, note that the `mongoose.clients` may not be read, please use the `mongoose.dataSource`. + +::: + + + +## The difference with the old writing + +If you want to use the new version of the usage, please refer to the following process to modify the old code. Do not mix the new and old codes. + +Upgrade method: + +- 1. No need to use `EntityModel` decorator +- 3. configure the adjustment in the `mongoose` section of `src/config.default`. refer to the following data source configuration section + - The 3.1 is modified to the form of a data source to `mongoose.dataSource` + - 3.2 declare the entity model in the `entities` field of the data source + + + + +## Mongoose version dependency + + +The mongoose is also related to the version of MongoDB Server used by your server, as follows, please note. + + +- MongoDB Server 2.4.x: mongoose ^3.8 or 4.x +- MongoDB Server 2.6.x: mongoose ^3.8.8 or 4.x +- MongoDB Server 3.0.x: mongoose ^3.8.22, 4.x, or 5.x +- MongoDB Server 3.2.x: mongoose ^4.3.0 or 5.x +- MongoDB Server 3.4.x: mongoose ^4.7.3 or 5.x +- MongoDB Server 3.6.x: mongoose 5.x +- MongoDB Server 4.0.x: mongoose ^5.2.0 +- MongoDB Server 4.2.x: mongoose ^5.7.0 +- MongoDB Server 4.4.x: mongoose ^5.10.0 +- MongoDB Server 5.x: mongoose ^6.0.0 + + +**mongoose related dependencies are complex and correspond to different versions. At this stage, we mainly use mongoose v5 and v6.** + + +:::info +From mongoose@v5.11.0 on, mongoose the definition is officially supported, there is no need to install the @types/mongoose dependency package. +::: + +The installation package depends on the following version: + +**Support MongoDB Server 6.x** + +```json + "dependencies": { + "mongoose": "^7.0.0", + "@typegoose/typegoose": "^10.0.0", // This dependency needs to be installed using typegoose + }, +``` + +**Support MongoDB Server 5.x** + +```json + "dependencies": { + "mongoose": "^6.0.7 ", + "@typegoose/typegoose": "9.0.0", // This dependency needs to be installed using typegoose + }, +``` + + +**Support MongoDB Server 4.4.x** + + +The following versions do not require additional definition packages to be installed. +```json + "dependencies": { + "mongoose": "^5.13.3 ", + "@typegoose/typegoose": "8.0.0", // This dependency needs to be installed using typegoose + }, +``` + + +The following versions require additional definition packages to be installed (not recommended). +```json + "dependencies": { + "mongodb": "3.6.3", // The version is written inside the mongoose + "mongoose": "~5.10.18 ", + "@typegoose/typegoose": "7.0.0", // This dependency needs to be installed using typegoose + }, + "devDependencies": { + "@types/mongodb": "3.6.3", // this version can only be used + "@types/mongoose": "~5.10.3 ", + } +``` + + +The rest of the MongoDB installation modules are similar and not tested. + + + +## Use Typegoose + + +### 1. Install components + + +Install Typegoose components to provide access to MongoDB. + + +**Please note that please check the first section to write/install mongoose and other related dependency packages in advance.** +```bash +$ npm i @midwayjs/typegoose@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + // Components + "@midwayjs/typegoose": "^3.0.0", + // mongoose dependency in the previous section + }, + "devDependencies": { + // mongoose dependency in the previous section + // ... + } +} +``` + + + +After installation, you need to manually configure it in `src/configuration.ts`, the code is as follows. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as typegoose from '@midwayjs/typegoose'; + +@Configuration({ + imports: [ + typegoose // Load typegoose Components + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + +:::info +In this component, midway just makes a simple configuration regularization and injects it into the initialization process. +::: + +### 2. Simple directory structure + + +We take a simple project as an example, please refer to other structures. + + +```text +MyProject +├── src // TS root directory +│ ├── config +│ │ └── config.default.ts // Application Profile +│ ├── entity // entity (database Model) directory +│ │ └── user.ts // entity file +│ ├── configuration.ts // Midway configuration file +│ └── service // Other service directory +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + +Here, our database entities are mainly located in the `entity` directory (non-mandatory). This is a simple convention. + + + +### 3. Create entity files + +For example, in `src/entity/user.ts`. + + +```typescript +import { prop } from '@typegoose/typegoose'; + +export class User { + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} +``` + +Equivalent to the following code that uses the mongoose + +```typescript +const userSchema = new mongoose.Schema({ + name: String + jobs: [{ type: String }] +}); + +const User = mongoose.model('User', userSchema); +``` + +:::info +Therefore, typegoose just simplify the process of creating model. +::: + + + + +### 4. Configure connection information + + +Add the configuration of the connection to `src/config/config.default.ts`. + +```typescript +import { User } from '../entity/user'; + +export default { + // ... + mongoose: { + dataSource: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '***********' + }, + // Associated Entities + entities: [ User] + } + } + }, +} +``` + +For more information, see [Data source management](../data_source). + + + +### 5, reference the entity, call the database. + + +The sample code is as follows: + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typegoose'; +import { ReturnModelType } from '@typegoose/typegoose'; +import { User } from '../entity/user'; + +@Provide() +export class TestService { + + @InjectEntityModel(User) + userModel: ReturnModelType; + + async getTest() { + // create data + const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an "as" assertion, to have types for all properties + + // find data + const user = await this.userModel.findById(id).exec(); + console.log(user) + } +} +``` + + +### 6, Multi-dataSource situation + +First define multiple entities. + +```typescript +class User { + + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} + +class User2 { + + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} +``` + + +Configure entities to multiple data sources. + + +Add the configuration of the data source to `src/config/config.default.ts`. +```typescript +import { User, User2 } from '../entity/user'; + +export default { + // ... + mongoose: { + dataSource: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '***********' + }, + entities: [ User] + }, + db1: { + uri: 'mongodb://localhost:27017/test1', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '***********' + }, + entities: [ User2] + } + } + }, +} +``` + + +Use a fixed connection when defining an instance, and configure the Model to automatically associate the mongoose connection when scanning the dataSource (`getModelForClass(Model, { existingConnection: conn })`). +```typescript +@Provide() +export class TestService { + + @InjectEntityModel(User) + userModel: ReturnModelType; + + @InjectEntityModel(User2) + user2Model: ReturnModelType; + + async getTest() { + const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an "as" assertion, to have types for all properties + const user = await this.userModel.findById(id).exec(); + console.log(user) + + const { _id: id2 } = await this.user2Model.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User2); // an "as" assertion, to have types for all properties + const user2 = await this.user2Model.findById(id2).exec(); + console.log(user2) + } +} + +``` + + + +### 7. About schemaOptions + +Typegoose reserved a `setGlobalOptions` method to set up [schemaOptions](https://typegoose.github.io/typegoose/docs/api/decorators/model-options#schemaoptions) and some other global [configurations](https://typegoose.github.io/typegoose/docs/api/decorators/model-options#options-1). + +We can set it up when config loaded. + +```typescript +// srcconfiguration.ts +import { Configuration } from '@midwayjs/core'; +import * as typegoose from '@midwayjs/typegoose'; +import * as Typegoose from '@typegoose/typegoose'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + async onConfigLoad() { + + Typegoose.setGlobalOptions({ + schemaOptions: { + // ... + }, + options: { allowMixed: Severity.ERROR} + }); + // ... + } +} +``` + + + + + +## Direct use of mongoose + +mongoose component is the basic component of typegoose, sometimes we can use it directly. + + +### 1. Install components + + +**Please note that please check the first section to write/install mongoose and other related dependency packages in advance.** + +```bash +$ npm i @midwayjs/mongoose@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + // Components + "@midwayjs/mongoose": "^3.0.0", + // mongoose dependency in the previous section + }, + "devDependencies": { + // mongoose dependency in the previous section + // ... + } +} +``` + + + +### 2. Open the components + + +After installation, you need to manually configure `src/configuration.ts`. The code is as follows. +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as mongoose from '@midwayjs/mongoose'; + +@Configuration({ + imports: [ + mongoose // enable mongoose component + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + + + + +### 2. Configuration + +Same as typegoose, or typegoose use mongoose configuration. + +Whether it is a single database or a multi-database, the data source configuration is similar. + + +Single library: +```typescript +export default { + // ... + mongoose: { + dataSource: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '**********' + } + } + } + }, +} +``` +Multi-library: +```typescript +export default { + // ... + mongoose: { + dataSource: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '***********' + } + }, + db1: { + uri: 'mongodb://localhost:27017/test1', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '***********' + } + } + } + }, +} +``` + + + +### 3. How to use + + +When we want to get the original connection object, we can directly use the encapsulated `MongooseConnectionService` object. +```typescript +import { Provide, Inject, Init } from '@midwayjs/core'; +import { MongooseDataSourceManager } from '@midwayjs/mongoose'; +import { Schema, Document } from 'mongoose'; + +interface User extends Document { + name: string; + email: string; + avatar: string; +} + +@Provide() +export class TestService { + + @Inject() + dataSourceManager: MongooseDataSourceManager;9 + + @Init() + async init() { + // get default connection + this.conn = this.dataSourceManager.getDataSource('default'); + } + + async invoke() { + const schema = new Schema({ + name: { type: String, required: true} + email: { type: String, required: true} + avatar: String + }); + const UserModel = this.conn.model('User', schema); + const doc = new UserModel({ + name: 'Bill', + email: 'bill@initech.com', + avatar: 'https:// I .imgur.com/dM7Thhn.png' + }); + await doc.save(); + } +} + +``` + + + + + + +## Frequently Asked Questions + + +### 1. E002: You are using a NodeJS Version below 12.22.0 + + +Node version verification has been added to the new version @typegoose/typegoose (v8, v9). if your Node.js version is lower than v12.22.0, this prompt will appear. + + +Under normal circumstances, please upgrade Node.js to this version or above to solve the problem. + + +In special scenarios, such as when the Serverless cannot modify the Node.js version and the version is lower than v12.22, the V12 version can actually be subversions, which can be bypassed by temporarily modifying the process.version. + + +```typescript +// src/configuration.ts + +Object.defineProperty(process, 'version', { + value: 'v12.22.0', + } +}); + +// other code + +export class MainConfiguration {} +``` + + + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/mqtt.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/mqtt.md new file mode 100644 index 000000000000..ce9c07d15c6f --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/mqtt.md @@ -0,0 +1,295 @@ +# MQTT + +MQTT is an OASIS standard messaging protocol for the Internet of Things (IoT). It is designed as an extremely lightweight publish/subscribe messaging transport that is ideal for connecting remote devices with a small code footprint and minimal network bandwidth. MQTT today is used in a wide variety of industries, such as automotive, manufacturing, telecommunications, oil and gas, etc. + +Related Information: + +| Description | | +| ------------------------------- | -------------------- | +| Available for standard projects | ✅ | +| Can be used for Serverless | Can publish messages | +| Available for integration | ✅ | +| Contains independent main frame | ✅ | +| Contains independent log | ✅ | + + + +## Version requirements + +Due to the requirements of the [mqtt](https://github.com/mqttjs/MQTT.js) library itself, the required version is **Node.js >= 16** + + + +## Prerequisites + +Since MQTT requires Broker as a transit transport, you need to deploy the MQTT Broker service yourself. This document does not provide deployment guidance for the MQTT service itself. + + + +## Install components + + +Install the mqtt component. + + +```bash +$ npm i @midwayjs/mqtt@3 --save +``` + +Or add the following dependencies in `package.json` and reinstall. + +```json +{ + "dependencies": { + "@midwayjs/mqtt": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## Enable component + +Introduce components in `src/configuration.ts` + +```typescript +// ... +import * as mqtt from '@midwayjs/mqtt'; + +@Configuration({ + imports: [ + // ...other components + mqtt, + ], +}) +export class MainConfiguration {} +``` + + + +Since MQTT is divided into two parts: **subscriber** and **publisher**, the two can be used independently, and we will introduce them separately. + + + +## Subscription service + +### Basic configuration + +Through the `sub` field and the `@MqttSubscriber` decorator, we can configure multiple subscribers. + +For example, `sub1` and `sub2` below are two different subscribers. + +```typescript +// src/config/config.default + +export default { + mqtt: { + sub: { + sub1: { + // ... + }, + sub2: { + // ... + } + } + } +} +``` + +The simplest subscriber configuration requires several fields, the subscribed address and the subscribed Topic. + +```typescript +// src/config/config.default + +export default { + mqtt: { + sub: { + sub1: { + connectOptions: { + host: 'test.mosquitto.org', + port: 1883, + }, + subscribeOptions: { + topicObject: 'test', + }, + }, + sub2: { + // ... + } + } + } +} +``` + + The `sub1` subscriber is configured with `connectOptions` and `subscribeOptions`, which represent connection configuration and subscription configuration respectively. + +### Subscription implementation + +We can provide a standard subscriber implementation in a directory, such as `src/consumer/sub1.subscriber.ts`. + +```typescript +// src/consumer/sub1.subscriber.ts + +import { ILogger, Inject } from '@midwayjs/core'; +import { Context, IMqttSubscriber, MqttSubscriber } from '@midwayjs/mqtt'; + +@MqttSubscriber('test') +export class Sub1Subscriber implements IMqttSubscriber { + + @Inject() + ctx: Context; + + async subscribe() { + // ... + } +} +``` + +The `@MqttSubscriber` decorator declares a subscription class implementation, and its parameter is the name of the subscriber, such as `sub1` in our configuration file. + +The `IMqttSubscriber` interface specifies a `subscribe` method, which will be executed whenever a new message is received. + +Like other message subscription mechanisms, the message itself is passed through the `Context` field. + +```typescript +// ... +export class Sub1Subscriber implements IMqttSubscriber { + @Inject() + ctx: Context; + + async subscribe() { + const payload = this.ctx.message.toString(); + // ... + } +} +``` + +The `Context` field includes several mqtt properties. + +| Properties | Type | Description | +| ----------- | ---------------------------------- | --------------------------- | +| ctx.topic | string | Subscribe to Topic | +| ctx.message | Buffer | Message content | +| ctx.packet | IPublishPacket (from mqtt library) | publish package information | + + + +## Message publish + +### Basic configuration + +Message publishing also requires the creation of instances, and the configuration itself uses the [Service Factory](/docs/service_factory) design pattern. + +For example, the multi-instance configuration is as follows: + +```typescript +// src/config/config.default + +export default { + mqtt: { + pub: { + clients: { + default: { + host: 'test.mosquitto.org', + port: 1883, + }, + pub2: { + // ... + } + } + } + } +} +``` + +The above configuration creates two instances named `default` and `pub2`. + + + +### Use publisher + +If the instance name is `default`, the default message publishing class can be used. + +for example: + +```typescript +// src/service/user.service.ts +import { Provide, Inject } from '@midwayjs/core'; +import { DefaultMqttProducer } from '@midwayjs/mqtt'; + +@Provide() +export class UserService { + + @Inject() + producer: DefaultMqttProducer; + + async invoke() { + // Publish messages synchronously + this.producer.publish('test', 'hello world'); + + //Asynchronous release + await this.producer.publishAsync('test', 'hello world'); + + //Add configuration + await this.producer.publishAsync('test', 'hello world', { + qos: 2 + }); + } +} +``` + +You can also use the built-in factory class `MqttProducerFactory` to inject different instances. + +```typescript +// src/service/user.service.ts +import { Provide, Inject } from '@midwayjs/core'; +import { MqttProducerFactory, DefaultMqttProducer } from '@midwayjs/mqtt'; + +@Provide() +export class UserService { + + @InjectClient(MqttProducerFactory, 'pub2') + producer: DefaultMqttProducer; + + async invoke() { + // ... + } +} +``` + + + +## Component log + +The component has its own log, and `ctx.logger` will be recorded in `midway-mqtt.log` by default. + +We can configure this logger object separately. + +```typescript +export default { + midwayLogger: { + // ... + mqttLogger: { + fileLogName: 'midway-mqtt.log', + }, + } +} +``` + +We can also configure the output format of this log separately. + +```typescript +export default { + mqtt: { + // ... + contextLoggerFormat: info => { + const { jobId, from } = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.message}`; + }, + } +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/orm.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/orm.md new file mode 100644 index 000000000000..0c9059c11856 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/orm.md @@ -0,0 +1,1688 @@ +# TypeORM + +[TypeORM](https://github.com/typeorm/typeorm) is the most mature object relation mapper (`ORM`) in the existing community of `node.js`. This article describes how to use TypeORM in Midway. + +:::tip + +This module is a new version from v3.4.0. The module name has changed and the history is partially compatible. For more information about how to query historical documents, see [here](../legacy/orm). + +::: + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## The difference with the old writing + +The old module is `@midwayjs/orm` and the new module is `@midwayjs/typeorm`. The differences are as follows: + +- 1. Different package names +- 2. Adjust some configurations in `src/config.default` + - The key in the 2.1 configuration file is different (orm => typeorm) + - The 2.2 is modified to the form of a data source to `typeorm.dataSource` + - The path to 2.3 an entity model class or an entity model class needs to be declared in the `entities` field of the data source. + - 2.4 Subscriber need to be declared in the `subscribers` field of the data source +- 3, no longer use the `EntityModel` decorator, directly use the ability provided by the typeorm + + + +## Installation Components + + +Install typeorm components to provide database ORM capability. + + +```bash +$ npm i @midwayjs/typeorm@3 typeorm --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/typeorm": "^3.0.0", + "typeorm": "~0.3.0 ", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## Introducing components + + +Introducing orm components in `src/configuration.ts`, an example is as follows. + +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as orm from '@midwayjs/typeorm'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + orm, // enable typeorm component + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + + +## Install database Driver + + +The commonly used database drivers are as follows. Select the database type to install the corresponding connection: +```bash +# for MySQL or MariaDB, you can also use mysql2 instead +npm install mysql --save +npm install mysql2 --save + +# for PostgreSQL or CockroachDB +npm install pg --save + +# for SQLite +npm install sqlite3 --save + +# for Microsoft SQL Server +npm install mssql --save + +# for SQL .js +npm install SQL .js --save + +# for Oracle +npm install oracledb --save + +# for MongoDB(experimental) +npm install mongodb --save +``` + +:::info + +- Oracle driver is special, you need to view the [documentation](https://github.com/oracle/node-oracledb) +- typeorm link mongodb is not recommended, please use mongoose components + +::: + + + + +## Simple directory structure + + +We take a simple project as an example, please refer to other structures. + + +``` +MyProject +├── src // TS root directory +│ ├── config +│ │ └── config.default.ts // Application Profile +│ ├── entity // entity (database Model) directory +│ │ └── photo.entity.ts // entity file +│ │ └── photoMetadata.ts +│ ├── configuration.ts // Midway configuration file +│ └── service // Other service directory +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + + + +Here, our database entities are mainly located in the `entity` directory (non-mandatory). This is a simple convention. + + + +## Getting Started + +Next, we will take mysql as an example. + + + + +### 1. Create Model + + +We associate with the database through the model. The model in the application is the database table. In the TypeORM, the model is bound to the entity. Each Entity file is a Model and an Entity. + + +In the example, you need an entity. Let's take `photo` as an example. Create an entity directory and add the entity file `photo.entity.ts` to the entity directory. A simple entity is as follows. +```typescript +// entity/photo.entity.ts +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` +Note that each attribute of the entity file here is actually one-to-one corresponding to the database table. Based on the existing database table, we add content up. + + +### 2. Define the entity model + + +`Entity` is used to define an entity model class. +```typescript +// entity/photo.entity.ts +import { Entity } from 'typeorm'; + +@Entity('photo') +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + +If the table name is different from the current entity name, you can specify it in the parameter. +```typescript +// entity/photo.entity.ts +import { Entity } from 'typeorm'; + +@Entity('photo_table_name') +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + + +These entity columns can also be generated using [typeorm_generator](/docs/tool/typeorm_generator) tools. + + +### 3. Add database columns + + +The properties are modified by the `@Column` decorator provided by the typeorm, each corresponding to a column. + + +```typescript +// entity/photo.entity.ts +import { Entity, Column } from 'typeorm'; + +@Entity() +export class Photo { + + @Column() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` + +The `id`, `name`, `description`, `filename`, `views`, `isPublished` columns are added to the `photo` table. The column types in the database are inferred according to the attribute types you use, for example, number will be converted to integers, strings will be converted to varchar, boolean values will be converted to bool, and so on. However, you can use any column type supported by the database by explicitly specifying the column type in the `@Column` decorator. + + +We generated a database table with columns, but there is one thing left. Each database table must have a column with a primary key. + + +Database columns include more column options (ColumnOptions), such as modifying column names, specifying column types, and column lengths. For more options, see the [official documentation](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/entities.md#%E5%88% 97% E9%80% 89% E9%A1%B9). + + + + +### 4. Create a primary key column + + +Each entity must have at least one primary key column. To make a column a primary key, you need to use the `@PrimaryColumn` decorator. + + +```typescript +// entity/photo.entity.ts +import { Entity, Column, PrimaryColumn } from 'typeorm'; + +@Entity() +export class Photo { + + @PrimaryColumn() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` +### 5. Create an auto-incrementing primary key column + + +Now, if you want to set the self-increasing id column, you need to change the `@PrimaryColumn` decorator to the `@PrimaryGeneratedColumn` decorator: +```typescript +// entity/photo.entity.ts +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class Photo { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` + + +### 6. Column data type + + +Next, let's adjust the data type. By default, strings map to types similar to `varchar(255)` (depending on the database type). Number is mapped to an integer-like type (depending on the database type). However, we do not want all columns to be limited to varchars or integers. Some changes can be made at this time. + + +```typescript +// entity/photo.entity.ts +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class Photo { + + @PrimaryGeneratedColumn() + id: number; + + @Column({ + length: 100 + }) + name: string; + + @Column('text') + description: string; + + @Column() + filename: string; + + @Column("double") + views: number; + + @Column() + isPublished: boolean; +} +``` + + +Example, different column names +```typescript +@Column({ + length: 100, + name: 'custom_name' +}) +name: string; +``` + + +Example, different column names + + +- `@CreateDateColumn` is a special column that automatically inserts dates for entities. +- The `@UpdateDateColumn` is a special column that automatically updates the entity date each time the entity manager or save of the repository is called. +- The `@VersionColumn` is a special column that automatically increases the entity version (increment number) each time the entity manager or save of the repository is called. +- `@DeleteDateColumn` is a special column that automatically sets the deletion time of the entity when soft-delete is called. + +For example: + +```typescript + @CreateDateColumn({ + type: 'timestamp', + }) + createdDate: Date; +``` + + + +The column type is database-specific. You can set any column type supported by the database. For more information about supported column types, see [here](https://github.com/typeorm/typeorm/blob/master/docs/entities.md#column-types). + +:::tip + +`CreateDateColumn` and `UpdateDateColumn` rely on the insertion date function of creating the default data on the column when the table structure is synchronized for the first time. If the table is created by yourself, you need to add the default data to the column. + +::: + + + + +### 7. Configure connection information and entity model + + +For more information, see [Configuration](/docs/env_config). + + +Then configure the database connection information in `config.default.ts`. +```typescript +// src/config/config.default.ts +import { Photo } from '../entity/photo.entity'; + +export default { + // ... + typeorm: { + dataSource: { + default: { + /** + * Single database instance + */ + type: 'mysql', + host: '*******', + port: 3306, + username: '*******', + password: '*******', + database: undefined, + synchronize: false, // If it is used for the first time, there is no table, and there is a need for synchronization, you can write true + logging: false, + + // Configure the entity model + entities: [Photo], + + // or scan format,For compatibility we can match .js and .ts at the same time + entities: [ + 'entity', // Specific directory + '**/*.entity.{j,t}s', // wildcard path + suffix matching + ] + } + } + }, +} +``` +:::tip + +- 1. If the database you are using already has the function of table structure synchronization, such as cloud database, it is better not to open it. If it must be used, it is best to use the synchronize configuration only in the development phase or for the first time to avoid consistency problems. +- 2. The `entities` field configuration has been processed by the framework, please do not refer to the original document for this field configuration. +::: + + + +You can use other database types for the `type` field, including `mysql`, `mariadb`, `postgres`, `cockroachdb`, `sqlite`, `mssql`, `oracle`, `cordova`, `nativescript`, `react-native`, `expo`, or `mongodb` + + +For example, sqlite requires the following information. + + +```typescript +// src/config/config.default.ts +export default { + // ... + typeorm: { + dataSource: { + default: { + type: 'sqlite', + database: path.join(__dirname, '../../test.sqlite') + synchronize: true, + logging: true + // ... + } + } + }, +} +``` + + +:::info +Note: synchronize fields are used to synchronize table structures. It is not safe to use `synchronize: true` for production mode synchronization. Please set this field to false after going online. +::: + + +### 8. Use Model to insert database data + + +In common Midway files, use the `@InjectEntityModel` decorator to inject our configured Model. All we need to do is: + + +- 1. Create entity objects +- 2. Execute the `save()` + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from '../entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // save + async savePhoto() { + // create a entity object + let photo = new Photo(); + photo.name = 'Me and Bears'; + photo.description = 'I am near polar bears'; + photo.filename = 'photo-with-bears.jpg'; + photo.views = 1; + photo.isPublished = true; + + // save entity + const photoResult = await this.photoModel.save(photo); + + // save success + console.log('photo id =', photoResult.id); + } +} +``` + + +### 9. Query Data + +For more information, see [find documentation](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/find-options.md). + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from '../entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhotos() { + + // find All + let allPhotos = await this.photoModel.find({}); + console.log("All photos from the db: ", allPhotos); + + // find first + let firstPhoto = await this.photoModel.findOne({ + where: { + id: 1 + } + }); + console.log("First photo from the db: ", firstPhoto); + + // find one by name + let meAndBearsPhoto = await this.photoModel.findOne({ + where: { name: "Me and Bears"} + }); + console.log("Me and Bears photo from the db: ", meAndBearsPhoto); + + // find by views + let allViewedPhotos = await this.photoModel.find({ + where: { views: 1} + }); + console.log("All viewed photos: ", allViewedPhotos); + + let allPublishedPhotos = await this.photoModel.find({ + where: { isPublished: true} + }); + console.log("All published photos: ", allPublishedPhotos); + + // find and get count + let [allPhotos, photosCount] = await this.photoModel.findAndCount({}); + console.log("All photos: ", allPhotos); + console.log("Photos count: ", photosCount); + + } +} + +``` + + +### 10. Update the database + + +Now, let's load a photo from the database, update it and save it. + + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from '../entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + + let photoToUpdate = await this.photoModel.findOne({ + where: { + id: 1, + }, + }); + photoToUpdate.name = "Me, my friends and polar bears"; + + await this.photoModel.save(photoToUpdate); + } +} +``` + +### 11. Delete data + +`remove` is used to remove the given entity or array of entities. `delete` is used to delete by a given ID or condition. + + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from '../entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + /*...*/ + const photo = await this.photoModel.findOne({ + where: { + id: 1, + }, + }); + + // Remove by entity + await this.photoModel.remove(photo) + // Delete multiple entities + await this.photoModel.remove([photo1, photo2, photo3]); + + // Delete by ID + await this.photoModel.delete(1); + await this.photoModel.delete([1, 2, 3]); + await this.photoModel.delete({ name: "Timber" }); + } +} +``` +Now, Photo with ID = 1 will be deleted from the database. + + +There is also a soft deletion method. +```typescript +await this.photoModel.softDelete(1); +// And You can restore it using restore; +await this.photoModel.restore(1); +``` + + +### 12. Create a one-to-one association + + +Let's create a one-to-one relationship with another class. Let's create a new class in `entity/photoMetadata.ts`. This class contains additional meta-information for photo. + + +```typescript +import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm'; +import { Photo } from './photo'; + +@Entity() +export class PhotoMetadata { + + @PrimaryGeneratedColumn() + id: number; + + @Column("int") + height: number; + + @Column("int") + width: number; + + @Column() + orientation: string; + + @Column() + compressed: boolean; + + @Column() + comment: string; + + @OneToOne(type => Photo) + @JoinColumn() + photo: Photo; + +} +``` + + +Here, we use a new fitting called `@OneToOne`. It allows us to create a one-to-one relationship between two entities. `type => photo` is a function that returns the class of the entity with which we want to establish a relationship. + + +Due to the particularity of the language, we are forced to use a function that returns the class instead of using the class directly. You can also write it as `() => Photo`, but we use `type => Photo` as a convention to improve the readability of the code. The type variable itself contains nothing. + + +We also added an `@JoinColumn` decorator, which indicates that this side of the relationship will have the relationship. Relationships can be one-way or two-way. The relationship can only be owned by one party. The owner side of the relationship needs to use the @JoinColumn decorator. If you run the application, you will see a newly generated table that will contain a column containing foreign keys for the Photo relationship. + + +``` ++-------------+--------------+----------------------------+ +| photo_metadata | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| height | int(11) | | +| width | int(11) | | +| comment | varchar(255) | | +| compressed | boolean | | +| orientation | varchar(255) | | +| photoId | int(11) | FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +Next we will associate them in the code. + + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from './entity/photo.entity'; +import { PhotoMetadata } from './entity/photoMetadata.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + @InjectEntityModel(PhotoMetadata) + photoMetadataModel: Repository; + + async updatePhoto() { + + // create a photo + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.isPublished = true; + + // create a photo metadata + let metadata = new PhotoMetadata(); + metadata.height = 640; + metadata.width = 480; + metadata.compressed = true; + metadata.comment = "cybershoot"; + metadata.orientation = "portrait"; + metadata.photo = photo; // this way we connect them + + + // first we should save a photo + await this.photoModel.save(photo); + + // photo is saved. Now we need to save a photo metadata + await this.photoMetadataModel.save(metadata); + + // done + console.log("Metadata is saved, and relation between metadata and photo is created in the database too"); + } +} +``` + + +### 13. Reverse relation mapping + + +Relational mapping can be one-way or two-way. When the relationship between PhotoMetadata and Photo is one-way. The owner of the relationship is PhotoMetadata, and Photo knows nothing about PhotoMetadata. This complicates accessing PhotoMetadata from the Photo side. To solve this problem, we add a reverse relational mapping to make the PhotoMetadata and Photo a two-way association. Let's modify our entity. + + +```typescript +import { Entity } from 'typeorm'; +import { Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm'; +import { Photo } from './photo.entity'; + +@Entity() +export class PhotoMetadata { + + /* ... other columns */ + + @OneToOne(type => Photo, photo => photo.metadata) + @JoinColumn() + photo: Photo; +} +``` +```typescript +import { Entity } from 'typeorm'; +import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from 'typeorm'; +import { PhotoMetadata } from './photoMetadata.entity'; + +@Entity() +export class Photo { + + /* ... other columns */ + + @OneToOne(type => PhotoMetadata, photoMetadata => photoMetadata.photo) + metadata: PhotoMetadata; +} +``` +`Photo => photo.metadata` is a function that returns a reverse mapping relationship. Here, we explicitly declare that metadata properties of the Photo class are used to associate PhotoMetadata. In addition to passing functions that return photo properties, you can also directly pass strings to `@OneToOne` decorators, such as `"metadata"`. But we used this function callback method to make our code writing simpler. + + +Note that the `@JoinColumn` decorator will only be used on one side of the relationship map. No matter which side of this decorator you place, you are the owner of the relationship. The owner of the relationship contains columns with foreign keys in the database. + + +### 14. Load objects and their dependencies + + +Now, let's try to load Photo and PhotoMetadata together in a single query. There are two ways to do this, using the `find *` method or using the `QueryBuilder` function. Let's first use the `find *` method. The `find *` method allows you to specify objects using the `FindOneOptions`/`FindManyOptions` interface. + + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from './entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhoto() { + /*...*/ + let photos = await this.photoModel.find({ relations: [ 'metadata' ] }); // typeorm@0.2.x + } +} + +``` +Here, the value of photos is an array that contains the query results of the entire database, and each photo object contains its associated metadata attribute. Learn more about the `Find Options` in [this document](https://github.com/typeorm/typeorm/blob/master/docs/find-options.md). + + +`Find Options` is simple, but if you need more complex queries, you should use `QueryBuilder` instead. `QueryBuilder` allows more complex queries to be used in an elegant way. + + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from './entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhoto() { + /*...*/ + let photos = await this.photoModel + .createQueryBuilder('photo') + .innerJoinAndSelect('photo.metadata', 'metadata') + .getMany(); + } +} +``` +`QueryBuilder` allows the creation and execution of almost any complex SQL query. When using `QueryBuilder`, think like creating SQL queries. In this example, "photo" and "metadata" are aliases applied to the selected photos. You can use aliases to access the columns and properties of the selected data. + + +### 15. Use cascade operations to automatically save associated objects + + +Cascade can be set in the relationship when we want to automatically save the associated object every time we save another object. Let's slightly change the `@OneToOne` decorator of the photo. + + +```typescript +export class Photo { + /// ... other columns + + @OneToOne(type => PhotoMetadata, metadata => metadata.photo, { + cascade: true + }) + metadata: PhotoMetadata; +} +``` +Using `cascade` allows us to no longer save Photo and PhotoMetadata separately now. Due to the cascade option, metadata objects will be saved automatically. + + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from './entity/photo.entity'; +import { PhotoMetadata } from './entity/photoMetadata.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + + // create photo object + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.isPublished = true; + + // create photo metadata object + let metadata = new PhotoMetadata(); + metadata.height = 640; + metadata.width = 480; + metadata.compressed = true; + metadata.comment = "cybershoot"; + metadata.orientation = "portrait"; + + photo.metadata = metadata; // this way we connect them + + // save a photo also save the metadata + await this.photoModel.save(photo); + + // done + console.log("Photo is saved, photo metadata is saved too"); + } +} +``` + + +Note that we now set the metadata of Photo instead of setting the Photo attribute of metadata as before. This is only valid when you connect Photo to the PhotoMetadata from the Photo side. If set on the PhotoMetadata side, it will not be saved automatically. + + +### 16. Create many-to-one/one-to-many associations + + +Let's create a many-to-one/one-to-many relationship. Suppose a photo has an author, and each author can have many photos. First, let's create an Author class: +```typescript +import { Entity } from 'typeorm'; +import { Column, PrimaryGeneratedColumn, OneToMany, JoinColumn } from 'typeorm'; +import { Photo } from './entity/photo.entity'; + +@Entity() +export class Author { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @OneToMany(type => Photo, photo => photo.author) // note: we will create author property in the Photo class below + photos: Photo[]; +} +``` +`Author` contains a reverse relationship. `OneToMany` and `ManyToOne` need to appear in pairs. + + +Now, add the owner of the relationship to the Photo entity: +```typescript +import { Entity } from 'typeorm'; +import { Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; +import { PhotoMetadata } from './photoMetadata.entity'; +import { Author } from './author.entity'; + +@Entity() +export class Photo { + + /* ... other columns */ + + @ManyToOne(type => Author, author => author.photos) + author: Author; +} +``` + + +In a many-to-one/one-to-many relationship, the owner is always many-to-one. This means that the class using the `@ManyToOne` will store the ID of the related object. + + +After the application is run, ORM creates the `author` table: + + +``` ++-------------+--------------+----------------------------+ +| author | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| name | varchar(255) | | ++-------------+--------------+----------------------------+ +``` +It also modifies the `photo` table, adds a new `author` column, and creates a foreign key for it: +``` ++-------------+--------------+----------------------------+ +| photo | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| name | varchar(255) | | +| description | varchar(255) | | +| filename | varchar(255) | | +| isPublished | boolean | | +| authorId | int(11) | FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +### 17. Create many-to-many associations + + +Let's create a many-to-one/many-to-many relationship. Suppose a photo can be in many albums, and each album can contain many photos. Let's create an `Album` class. + + +```typescript +import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm'; + +@Entity() +export class Album { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @ManyToMany(type => Photo, photo => photo.albums) + @JoinTable() + photos: Photo[]; +} +``` + + +`@JoinTable` is used to indicate that this is the owner of the relationship. + + +Now, add the reverse association to `Photo`. + + +```typescript +export class Photo { + /// ... other columns + + @ManyToMany(type => Album, album => album.photos) + albums: Album[]; +} +``` +After running the application, ORM will create a album_photos_photo_albums join table: + + +``` ++-------------+--------------+----------------------------+ +| album_photos_photo_albums | ++-------------+--------------+----------------------------+ +| album_id | int(11) | PRIMARY KEY FOREIGN KEY | +| photo_id | int(11) | PRIMARY KEY FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +Now, let's insert albums and photos into the database: + + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from './entity/photo.entity'; +import { Album } from './entity/album.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + @InjectEntityModel(Album) + albumModel: Repository + + async updatePhoto() { + + // create a few albums + let album1 = new Album(); + album1.name = "Bears"; + await this.albumModel.save(album1); + + let album2 = new Album(); + album2.name = "Me"; + await this.albumModel.save(album2); + + // create a few photos + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.albums = [album1, album2]; + await this.photoModel.save(photo); + + + // now our photo is saved and albums are attached to it + // now lets load them: + const loadedPhoto = await this.photoModel.findOne(1, { relations: ["albums"] }); // typeorm@0.2.x + } +} +``` +The `loadedPhoto` value is: +```json +{ + id: 1, + name: "Me and Bears ", + description: "I am near polar bears ", + filename: "photo-with-bears.jpg ", + albums: [{ + id: 1, + name: "Bears" + }, { + id: 2, + name: "Me" + }] +} +``` + +### 18. Use QueryBuilder + + +You can use QueryBuilder to build almost any complex SQL query. For example, you can do this: + + +```typescript +let photos = await this.photoModel + .createQueryBuilder("photo") // first argument is an alias. Alias is what you are selecting - photos. You must specify it. + .innerJoinAndSelect("photo.metadata", "metadata") + .leftJoinAndSelect("photo.albums", "album") + .where("photo.isPublished = true") + .andWhere("(photo.name = :photoName OR photo.name = :bearName)") + .orderBy("photo.id", "DESC") + .skip(5) + .take(10) + .setParameters({ photoName: "My", bearName: "Mishka" }) + .getMany(); +``` +The query selects all published photos with "My" or "Mishka" names. It will return results (paging offset) from position 5, and only 10 results (paging limit) will be selected. The selection results will be sorted in descending order of ID. The photo album will be left-Joined and metadata will be automatically associated. + + +You will use query generators extensively in your application. Learn more about QueryBuilder [here](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/select-query-builder.md). + + +### 19. Event Subscriber + + +typeorm provides an event subscription mechanism to facilitate log output when doing some database operations. For this reason, midway provides a `EventSubscriberModel` decorator to label event subscription classes with the following code. + + +```typescript +import { EventSubscriberModel } from '@midwayjs/typeorm'; +import { EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm'; + +@EventSubscriberModel() +export class EverythingSubscriber implements EntitySubscriberInterface { + + /** + * Called before entity insertion. + */ + beforeInsert(event: InsertEvent) { + console.log('BEFORE ENTITY INSERTED:', event.entity); + } + + /** + * Called before entity insertion. + */ + beforeUpdate(event: UpdateEvent) { + console.log('BEFORE ENTITY UPDATED:', event.entity); + } + + /** + * Called before entity insertion. + */ + beforeRemove(event: RemoveEvent) { + console.log('BEFORE ENTITY WITH ID ${event.entityId} REMOVED:', event.entity); + } + + /** + * Called after entity insertion. + */ + afterInsert(event: InsertEvent) { + console.log('AFTER ENTITY INSERTED:', event.entity); + } + + /** + * Called after entity insertion. + */ + afterUpdate(event: UpdateEvent) { + console.log('AFTER ENTITY UPDATED:', event.entity); + } + + /** + * Called after entity insertion. + */ + afterRemove(event: RemoveEvent) { + console.log('AFTER ENTITY WITH ID ${event.entityId} REMOVED:', event.entity); + } + + /** + * Called after entity is loaded. + */ + afterLoad(entity: any) { + console.log('AFTER ENTITY LOADED:', entity); + } + +} +``` + +This subscription class provides some common interfaces to perform some things during database operations. + +At the same time, we need to add subscription classes to the configuration. + +```typescript +// src/config/config.default.ts +import { EverythingSubscriber } from '../event/subscriber'; + +export default { + // ... + typeorm: { + dataSource: { + default: { + // ... + entities: [Photo], + // Incoming subscription class + subscribers: [EverythingSubscriber] + } + } + }, +} +``` + + + +## Repository API + +For more APIs, please check [official website documentation](https://github.com/typeorm/typeorm/blob/master/docs/repository-api.md). + + + + +## Advanced features +### Multi-dataSource support + + +Sometimes, we have multiple database connections (Connection) in an application, and there will be multiple configurations at this time. We use the DataSource standard form of **object** to define the configuration. + + +For example, the following defines two database connections (Connection), `default` and `test`. + + +```typescript +import { join } from 'path'; + +export default { + typeorm: { + dataSource: { + default: { + type: 'sqlite', + database: join(__dirname, '../../default.sqlite') + // ... + }, + test: { + type: 'mysql', + host: '127.0.0.1', + port: 3306 + // ... + } + } + } +} +``` + + +In use, you need to specify which connection (Connection) the model belongs. +```typescript +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { User } from './entity/user.entity'; + +export class XXX { + + @InjectEntityModel(User, 'test') + testUserModel: Repository; + + //... +} +``` + + + +### Column value conversion + +We can handle column value conversions in the entity definition. + +The `transformer` parameters of the column decorator can be used to process entry and exit parameters, such as formatting time. + +```typescript +import { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import * as dayjs from 'dayjs'; + +const dateTransformer = { + from: (value: Date | number) => { + return dayjs(typeof value === 'number '? value: value.getTime()).format('YYYY-MM-DD HH:mm:ss'); + }, + to: () => new Date() +}; + +@Entity() +export class Photo { + // ... + + @CreateDateColumn({ + type: 'timestamp', + transformer: dateTransformer + }) + createdAt: Date; +} + +``` + + + +### Specify the default data source + +When including multiple data sources, you can specify a default data source. + +```typescript +export default { + // ... + typeorm: { + dataSource: { + default1: { + // ... + }, + default2: { + // ... + }, + }, + // 多个数据源时可以用这个指定默认的数据源 + defaultDataSourceName: 'default1', + }, +}; +``` + + + +### Get data source + +The data source is the DataSource object of TypeORM created, which we can obtain by injecting the built-in data source manager. + +```typescript +import { Configuration } from '@midwayjs/core'; +import { TypeORMDataSourceManager } from '@midwayjs/typeorm'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + + async onReady(container: IMidwayContainer) { + const dataSourceManager = await container.getAsync(TypeORMDataSourceManager); + const conn = dataSourceManager.getDataSource('default'); + console.log(dataSourceManager.isConnected(conn)); + } +} +``` + +Starting with v3.8.0, it is also possible to inject via a decorator. + +```typescript +import { Configuration } from '@midwayjs/core'; +import { InjectDataSource } from '@midwayjs/typeorm'; +import { DataSource } from 'typeorm'; + +@Configuration({ + //... +}) +export class MainConfiguration { + + // Inject the default data source + @InjectDataSource() + defaultDataSource: DataSource; + + // inject custom data source + @InjectDataSource('default1') + customDataSource: DataSource; + + async onReady(container: IMidwayContainer) { + //... + } +} +``` + + + +### Logging + +When the data source does not configure a log object, the component will automatically create a `typeormLogger` to save the executed SQL information, which is convenient for troubleshooting and SQL audit. + +The default configuration is: + +```typescript +export default { + midwayLogger: { + clients: { + typeormLogger: { + fileLogName: 'midway-typeorm.log', + enableError: false, + level: 'info', + }, + }, + } +} +``` + +We can use the normal log configuration method to make adjustments. If you do not want to generate logs, you can configure them to close. + +```typescript +export default { + // ... + typeorm: { + default: { + // All data sources closed + logging: false, + }, + dataSource: { + default: { + // Single data source closed + logging: false, + }, + }, + }, +}; +``` + + + +### Transaction + +The typeorm transaction needs to get the data source first and then open the transaction. + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { TypeORMDataSourceManager } from '@midwayjs/typeorm'; +import { UserDTO } from '../entity/user'; + +@Provide() +export class UserService { + + @Inject() + dataSourceManager: TypeORMDataSourceManager; + + async updateUser(user: UserDTO) { + + // get dataSource + const dataSource = this.dataSourceManager.getDataSource('default'); + + // start transaction + await dataSource.transaction(async (transactionalEntityManager) => { + // run code + await transactionalEntityManager.save(UserDTO, user); + }); + } + +} +``` + +For more information, see [Documentation](https://github.com/typeorm/typeorm/blob/master/docs/transactions.md). + + + +### CLI + +TypeORM provides a CLI by default to create entity, migration, etc. For more documents, please see [here](https://github.com/typeorm/typeorm/blob/master/docs/using-cli.md) . + +Since the default configuration of TypeORM is different from Midway, we provide a simple modified version to adapt to Midway's data source configuration. + +Check the installation: + +```bash +$ npx mwtypeorm -h +``` + +Commonly used commands are + + **Create Empty Entity** + +A `src/entity/User.ts` file will be created. + +```bash +$ npx mwtypeorm entity:create src/entity/User +``` + +**Create Migration** + +A `src/migration/******-photo.entity.ts` file will be generated based on the existing data source. + +For example, the configuration is as follows: + +```typescript +export default { + typeorm: { + dataSource: { + 'default': { + //... + entities: [ + '**/entity/*.entity{.ts,.js}' + ], + migrations: [ + '**/migration/*.ts' + ], + }, + }, +} +``` + +You can execute the following command to generate a migration file for the modified Entity. + +```bash +$ npx mwtypeorm migration:generate -d ./src/config/config.default.ts src/migration/photo +``` + +:::caution + +Note: Since the above entities configuration needs to be reused between CLI and Midway, the scanning method supported by both is adopted. + +::: + + + +### About Table Structure Synchronization + + +- If you already have a table structure, you want to automatically create an Entity and use the [Generator](https://www.npmjs.com/package/typeorm-model-generator) +- If you already have Entity code, if you want to create a table structure, please use `synchronize: true` in the configuration, be aware that data may be lost +- If it is already online, but the table structure has been modified, you can use `migration:generate` in the CLI + + + +## Frequently Asked Questions + + +### Handshake inactivity timeout + + +Generally, it is due to network reasons. If it appears locally, you can ping but telnet is not available. You can try to execute the following command: +```bash +$sudo sysctl -w net.inet.tcp.sack=0 +``` + + + +### Time Zone Display of mysql Time Column + +In general, UTC time is stored in the database. If you want to return the time in the current time zone, you can use the following method + +**1. Check the environment where the mysql database is located.** + +For example, the default time zone is the system UTC time, which can be adjusted to `+08:00`. + +```text +mysql> show global variables like '%time_zone%'; ++------------------+--------+ +| Variable_name | Value | ++------------------+--------+ +| system_time_zone | UTC | +| time_zone | SYSTEM | ++------------------+--------+ +2 rows in set (0.05 sec) +``` + +**2. Check the environment where the service code is deployed.** + +Try to be consistent with the environment where the database is located. If not, set the `timezone` in the configuration (set to be consistent with mysql). + +```typescript +export default { + typeorm: { + dataSource: { + default: { + type: 'mysql', + // ... + timezone: '+08:00', + }, + }, + }, +} +``` + + + +### Time column returns string + +Configuring dateStrings can make mysql return time in DATETIME format, which is only valid for mysql. + +```typescript +// src/config/config.default.ts +export default { + // ... + typeorm: { + dataSource: { + default: { + //... + dateStrings: true + } + } + }, +} +``` + +Entity return types can be adjusted if `@CreateDateColumn` and `@UpdateDateColumn` are used. + +```typescript +@UpdateDateColumn({ + name: "gmt_modified", + type: 'timestamp' +}) +gmtModified: string; + +@CreateDateColumn({ + name: "gmt_create", + type: 'timestamp', +}) +gmtCreate: string; +``` + + + +The effect is as follows: + +**Before configuration:** + +```typescript +gmtModified: 2021-12-13T03:49:43.000Z +gmtCreate: 2021-12-13T03:49:43.000Z +``` +**After configuration:** +```typescript +gmtModified: '2021-12-13 11:49:43', +gmtCreate: '2021-12-13 11:49:43' +``` + + + +### Install mysql and mysql2 at the same time + +When you have both mysql and mysql2 in node_modules, typeorm will automatically load mysql instead of mysql2. + +If you need to use mysql2 at this time, please specify the driver. + +```typescript +// src/config/config.default.ts +export default { + // ... + typeorm: { + dataSource: { + default: { + //... + type: 'mysql', + driver: require('mysql2') + } + } + }, +} +``` + + + + +### Cannot read properties of undefined (reading 'getRepository') + +Generally, the configuration is incorrect, and two configurations can be considered. + +- 1. Check whether the `entities` configuration in `config.default.ts` is correct +- 2. Check the `configuration.ts` file to confirm whether orm is imported + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/oss.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/oss.md new file mode 100644 index 000000000000..1c6a86495a3e --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/oss.md @@ -0,0 +1,262 @@ +# Alibaba Cloud Object Storage (OSS) + +Object Storage Service (OSS) is a massive, secure, low-cost, and highly reliable cloud storage service provided by Alibaba Cloud. Its data design durability is not less than 99.999999999% and service design availability is not less than 99.99%. With platform-independent RESTful API interfaces, you can store and access any type of data in any application, at any time, and at any place. + +The `@midwayjs/oss` component is the sdk used to interface with OSS services under the midway system. + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## Antecedents + + +To use OSS components, you need to apply for an OSS bucket in advance. Bucket is the concept of OSS repository in which all your files are stored. + + +- OSS Object Storage Service (OSS): [https:// www.aliyun.com/product/oss](https://www.aliyun.com/product/oss) +- What is Object Storage: [https:// www.alibabacloud.com/help/zh/doc-detail/31817 htm](https://www.alibabacloud.com/help/zh/doc-detail/31817.htm) + + +## Installation dependency + +`@midwayjs/oss` is the main function package of oss. + +```bash +$ npm i @midwayjs/oss@3 --save +``` +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/oss": "^3.0.0", + // ... + } +} +``` + + + + +## Introducing components + + +First, introduce components and import them in `configuration.ts`: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as oss from '@midwayjs/oss'; +import { join } from 'path' + +@Configuration({ + imports: [ + // ... + oss // import oss components + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + +## Configure OSS + + +OSS components need to be configured before they can be used. You need to enter OSS bucket, accessKeyId, accessKeySecret and other necessary information. + + +Supports common oss clients and oss cluster clients based on the [ali-oss](https://github.com/ali-sdk/ali-oss/) package. + + +For example: + +**Common OSS bucket configuration** +```typescript +// src/config/config.default +export default { + // ... + oss: { + // normal oss bucket + client: { + accessKeyId: 'your access key', + accessKeySecret: 'your access secret', + bucket: 'your bucket name', + endpoint: 'oss-cn-hongkong.aliyuncs.com', + timeout: '60s', + }, + }, +} +``` + + +**OSS bucket configuration in cluster (cluster) mode, you need to configure multiple** + +```typescript +// src/config/config.default +export default { + // ... + oss: { + // need to config all bucket information under cluster + client: { + clusters: [{ + endpoint: 'host1', + accessKeyId: 'id1', + accessKeySecret: 'secret1', + }, { + endpoint: 'host2', + accessKeyId: 'id2', + accessKeySecret: 'secret2', + }], + schedule: 'masterSlave', //default is 'roundRobin' + timeout: '60s', + } + }, +} +``` + +**STS** +```typescript +// src/config/config.default +export default { + // ... + oss: { + // if config.sts == true, oss will create STS client + client: { + sts: true + accessKeyId: 'your access key', + accessKeySecret: 'your access secret', + }, + }, +} +``` + +## Use components + + +You can directly get the `OSSService` and then call the interface, for example, to save the file. +```typescript +import { OSSService } from '@midwayjs/oss'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + ossService: OSSService; + + async saveFile() { + + + const localFile = join(__dirname, 'test.log'); + const result = await this.ossService.put('/test/test.log', localFile); + + // => result.url + } +} +``` + + +If STS mode is configured, the client can use `OSSSTSService`. +```typescript +import { OSSSTSService } from '@midwayjs/oss'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + stsService: OSSSTSService; + + async saveFile() { + + const roleArn = '******'; // This is arn of ariyun role + const result = await this.stsService.assumeRole(roleArn); + + // result.credentials.AccessKeyId + // result.credentials.AccessKeySecret; + // result.credentials.SecurityToken; + } +} +``` + +For more information about OSS client APIs, see [OSS documentation](https://github.com/ali-sdk/ali-oss). + + +## Use multiple OSS Buckets + + +Some applications need to access multiple oss buckets, so you need to configure `oss.clients`. +```typescript +// src/config/config.default +export default { + // ... + oss: { + clients: { + bucket1: { + bucket: 'bucket1', + // ... + }, + bucket2: { + bucket: 'bucket2', + // ... + }, + }, + // client, clients, configuration shared by createInstance methods + default: { + endpoint: '', + accessKeyId: '', + accessKeySecret: '', + }, + }, + // other custom config + bucket3: { + bucket: 'bucket3', + // ... + }, +} +``` + +You can use `OSSServiceFactory` to get different instances. + +```typescript +import { OSSServiceFactory } from '@midwayjs/oss'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + ossServiceFactory: OSSServiceFactory; + + @Config('bucket3') + bucket3Config; + + async saveFile() { + + // The default type is OSSService + const bucket1 = this.ossServiceFactory.get('bucket1'); + const bucket2 = this.ossServiceFactory.get('bucket2'); + + // If it is STS, you need to set a generic contact. + // const bucket1 = this.ossServiceFactory.get('bucket1'); + + // config.bucket3 and config.oss.default will be merged + const bucket3 = await this.ossServiceFactory.createInstance(this.bucket3Config, 'bucket3'); + // After passing the name, you can also get it from the factory. + bucket3 = this.ossServiceFactory.get('bucket3'); + + } +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/otel.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/otel.md new file mode 100644 index 000000000000..01fe5f8a6d97 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/otel.md @@ -0,0 +1,335 @@ +# Tracer + +Midway adopts the latest [open-telemetry](https://opentelemetry.io/) scheme in the community. Its predecessor is a well-known OpenTracing and OpenCensus specification. At this stage, Midway is also an incubation project of CNCF. Many well-known large companies in the community such as Amazon,Dynatrace,Microsoft,Google,Datadog,Splunk, etc. have used it. + +[Open-telemetry](https://opentelemetry.io/) provides a general Node.js access solution, which receives, processes, and exports data in a vendor-independent manner, and supports sending observable data to one or more open source or commercial collection terminals (such as Alibaba Cloud SLS,Jaeger,Prometheus,Fluent Bit, etc.). + +Midway provides a Node.js scheme to access [open-telemetry](https://opentelemetry.io/) and some simple API. + +:::info + +The Tracing part of [open-telemetry](https://opentelemetry.io/) is currently Release 1.0.0 for Node.js SDK, which can be used in production. The Metrics part has not been officially released, and we are still following up (encoding). + +::: + + + +## Instructions for Use + +[Open-telemetry](https://opentelemetry.io/) the Async_Hooks stable API implementation based on Node.js. after our tests, the performance impact of the latest Node.js v14/v16 has been very small and can be used in production. although it can be used in the case of v12, there is still a big loss in performance. please use it in the version of Node.js >= v14 as much as possible. + + + +## Installation base dependency + +```bash +# Node.js api abstraction +$ npm install --save @opentelemetry/api + +Api implementation of# Node.js +$ npm install --save @opentelemetry/sdk-node + +# Common Node.js Module Buried Point Implementation +$ npm install --save @opentelemetry/auto-instrumentations-node + +# jaeger output +$ npm install --save @opentelemetry/exporter-jaeger +``` + +The above packages are all official packages of [open-telemetry](https://opentelemetry.io/). + + + +## Enable open-telemetry + +Please add the [open-telemetry](https://opentelemetry.io/) module to the beginning of the code as much as possible (earlier than the framework), so we have different ways to add it in different scenarios. + + + +### Use bootstrap deployment + +If you use `bootstrap.js` deployment, you can add it to the top of the `bootstrap.js`. The sample code is as follows. + +```typescript +const process = require('process'); +const { NodeSDK, node, resources } = require('@opentelemetry/sdk-node'); +const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); +const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions') +const { JaegerExporter } = require('@opentelemetry/exporter-jaeger') + +// Midway startup file +const { Bootstrap } = require('@midwayjs/bootstrap'); + +// https://www.npmjs.com/package/@opentelemetry/exporter-jaeger +const tracerAgentHost = process.env['TRACER_AGENT_HOST'] || '127.0.0.1' +const jaegerExporter = new JaegerExporter({ + host: tracerAgentHost +}); + +// Initialize an open-telemetry SDK +const sdk = new NodeSDK({ + // Set the tracking service name + resource: new resources.Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'my-app', + }), + // Configure the current export method. For example, one output to the console is configured here, or other Exporter can be configured, such as Jaeger. + traceExporter: new node.ConsoleSpanExporter(), + // configure the current export as jaeger + // traceExporter: jaegerExporter + + // Some monitoring modules provided by default are configured here, such as http module, etc. + // If the initialization time is very long, you can log off this line and configure the required instrumentation entries separately. + instrumentations: [getNodeAutoInstrumentations()] +}); + +// Initialize the SDK and start the Midway framework after successful startup. +sdk.start() + +// When the process is closed, data collection is closed at the same time +process.on('SIGTERM', () => { + sdk.shutdown() + .then(() => console.log('Tracing terminated')) + .catch((error) => console.log('Error terminating tracing', error)) + .finally(() => process.exit(0)); +}); + +Bootstrap + .configure(/**/) + .run(); +``` + + + +### Use egg-scripts deployment + +Egg-scripts Since no portal deployment is provided, additional files must be loaded in the form of `-require`. + +Add a `tel.js` file to the root directory. The content is as follows. + +```javascript +const process = require('process'); +const { NodeSDK, node, resources } = require('@opentelemetry/sdk-node'); +const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); + +// Initialize an open-telemetry SDK +const sdk = new NodeSDK({ + // Configure the current export method. For example, one output to the console is configured here, or other Exporter can be configured, such as Jaeger. + traceExporter: new node.ConsoleSpanExporter(), + // Some monitoring modules provided by default are configured here, such as http module, etc. + instrumentations: [getNodeAutoInstrumentations()] +}); + +// Initialize SDK +sdk.start() + + +// When the process is closed, data collection is closed at the same time +process.on('SIGTERM', () => { + sdk.shutdown() + .then(() => console.log('Tracing terminated')) + .catch((error) => console.log('Error terminating tracing', error)) + .finally(() => process.exit(0)); +}); +``` + +Modify the startup command in the `package.json`. + +```json +{ + // ... + "scripts": { + "start": "egg-scripts start --daemon --title=**** --framework=@midwayjs/web --require=./otel.js ", + }, +} +``` + +### Development and debugging portal + +`midway-bin` uses the `-- entryFile` parameter to specify the entry file + +For example, the `package.json` file +```json +{ + "scripts": { + "start": "cross-env NODE_ENV=local midway-bin dev --ts --entryFile=bootstrap.js" + } +} +``` + +## Common concepts + +[Open-telemetry](https://opentelemetry.io/) provides some abstract packaging, packaging the whole process of monitoring into several steps, each step can be customized configuration, and there are also some terms that users do not understand. Let's explain them below. + +Please refer to the [Concepts](https://opentelemetry.io/docs/concepts/) for complete English concepts. + + + +### API + +A set of API abstractions used to generate and associate data types and operations of Tracing, Metrics, and Logs record data. The specific expression is the package `@opentelemetry/api`, which contains some interfaces and empty implementations. + +### SDK + +The language-specific implementation of the API, such as the implementation of Node.js (`@opentelemetry/sdk-node` ), and the implementation of the collection SDK of other monitoring platforms. + +### Instrumentations + +[Open-telemetry](https://opentelemetry.io/) provides shim codes of some common libraries. It uses hooks or monkey-patching methods to intercept methods, automatically saves link data when specific methods are called, and supports http,gRPC , redis,mysql and other modules. Users can use them directly by configuring them. + +For example, the `@opentelemetry/auto-instrumentations-node` introduced in the above example is a instrumentations collection package that has already encapsulated common libraries by default, including most of the libraries that will be used. For specific dependencies, please refer to [Github](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/metapackages/auto-instrumentations-node/package.json). + +### Exporter + +Sends the received link data to a specific implementation, such as Jaeger,zipkin, etc. + + + +## Example + + + +### Add tripartite instrumentation + +When the SDK is initialized, add it to the `instrumentations` array. + +```typescript +const { RedisInstrumentation } = require('@opentelemetry/instrumentation-redis'); +// ... + +// Initialize an open-telemetry SDK +const sdk = new NodeSDK({ + // ... + + // This is only an added example. If auto-instrumentations-node is used, the following instrumentation are already included + instrumentations: [ + new RedisInstrumentation() + ] +}); +``` + + + +### Add Jaeger Exporter + +Here, Jaeger Exporter is taken as an example, and other Exporter are similar. + +Add dependencies first. + +```bash +$ npm install --save @opentelemetry/exporter-jaeger @opentelemetry/propagator-jaeger +``` + +Configure in the SDK. + +```typescript +const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); +const { JaegerPropagator } = require('@opentelemetry/propagator-jaeger'); +// ... + +const exporter = new JaegerExporter({ + tags: [], // optional + // You can use the default UDPSender + host: 'localhost', // optional + port: 6832, // optional + // OR you can use the HTTPSender as follows + // endpoint: 'http://localhost:14268/api/traces', + maxPacketSize: 65000 // optional +}); + +// Initialize an open-telemetry SDK +const sdk = new NodeSDK({ + traceExporter: exporter + textMapPropagator: new JaegerPropagator() + // ... +}); +``` + +For specific parameters, please refer: + +- [opentelemetry-exporter-jaeger](https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-exporter-jaeger/README.md) +- [opentelemetry-propagator-jaeger](https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-propagator-jaeger/README.md) + + + +### Alibaba Cloud ARMS + +Alibaba Cloud Application Real-Time Monitoring Service ([ARMS](https://www.aliyun.com/product/arms/)) already supports indicators in open-telemetry format, and provides an sdk for access. + +First, `opentelemetry-arms` is installed. + +```bash +# arms sdk +$ npm install --save opentelemetry-arms +``` + +Then, add the environment variable and `-R` parameters at startup. + +```bash +$SERVICE_NAME=nodejs-opentelemetry-express AUTHENTICATION=**** ENDPOINT=grpc://**** node -r opentelemetry-arms bootstrap.js +``` + +:::tip + +- 1. There is no need to add code to `bootstrap.js` for access in this way. +- 2. The default sdk only provides link support for http/express/koa modules and does not include other instrumentations. If necessary, you can copy the source code to `bootstrap.js` for customization. + +::: + +## Framework capability support + +Note that the component only wraps the interface of otel, if you don’t need the following interface to use, you don’t need to install this component + +Install dependencies first. + +```bash +$ npm i @midwayjs/otel@3 --save +``` + +Enable the `otel` component. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as otel from '@midwayjs/otel'; + +@Configuration({ + imports: [ + //... + otel + ] +}) +export class MainConfiguration { +} +``` + + + +### ctx.traceId + +The component provides `ctx.traceId` field. + +You can get it under supported components (egg/koa). + +```typescript +ctx.traceId => ***** +``` + +### Decorator support + +Midway adds a decorator to add link nodes to the needs of the user side. + +The Otel component provides an @Trace decorator that can be added to the method. + +```typescript +export class UserService { + + @Trace('user.get') + async getUser() { + // ... + } +} +``` + +The decorator needs to pass in a node name, so that the link will automatically add a link node of the method and record the execution time. The method execution succeeded or failed. + + + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/passport.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/passport.md new file mode 100644 index 000000000000..dcb148588786 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/passport.md @@ -0,0 +1,516 @@ +# Authentication + +Authentication is an important part of most Web applications. Therefore, Midway encapsulates the most popular Passport library in Nodejs. + + +Related information: + +| Web support | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + +Starting from v3.4.0, Midway maintains its own passport and will no longer need to introduce community packages and type packages. + + + +## Some concepts + +The passport is that the community uses more authentication libraries to make authentication requests through extensible plug-ins called policies. + +It itself contains several parts: + +- 1. Verification strategies, such as jwt verification, github verification, oauth verification, etc. The most abundant passport is this one. +- 2. After the policy is executed, the logic processing and configuration of middleware, such as jump after success or failure, error reporting, etc. + + + + +## Installation dependency + +`npm I @midwayjs/passport` installation and related policy dependencies. + +```bash +## Required +$ npm i @midwayjs/passport@3 --save + +## Optional +## Install the local policy below +$ npm i passport-local --save +$ npm i @types/passport-local --save-dev +## Install Github policy below +$ npm i passport-github --save +## Install Jwt policy below +$ npm i passport-jwt --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/passport": "^3.0.0", + // Local policy + "passport-local": "^1.0.0" + // Jwt strategy + "passport-jwt": "4.0.0", + // Github policy + "passport-github": "1.1.0", + // ... + }, + "devDependencies": { + // Local policy + "@types/passport-local": "^1.0.34 ", + // Jwt strategy + "@types/passport-jwt": "^3.0.6 ", + // Github policy + "@types/passport-github": "^1.1.7 ", + // ... + } +} +``` + + + +## Enable components + + +Enable the component first. + +```typescript +// src/configuration.ts + +import { join } from 'path'; +import { ILifeCycle,} from '@midwayjs/core'; +import { Configuration } from '@midwayjs/core'; +import * as passport from '@midwayjs/passport'; + +@Configuration({ + imports: [ + // ... + passport + ], + importConfigs: [join(__dirname, './config')] +}) +export class MainConfiguration implements ILifeCycle {} + +``` + + + +## Policy example + +Here we use the local authentication strategy and the Jwt strategy as a demonstration. + +### Example: Local Policy + +We use `passport-local` to introduce how to use the Passport policy in Midway. The official document example of `passport-local` is as follows. Load a policy through `passport.use`. The verification logic of the policy is a `verify` method, including callback parameter, the rest of the strategy parameters are in the constructor. + +```typescript +passport.use( + //Initialize a strategy + new LocalStrategy({ + usernameField: 'username', + passwordField: 'password', + passReqToCallback: true, + session: false + }, + function verify(username, password, done) { + User.findOne({ username: username }, function (err, user) { + if (err) { return done(err); } + if (!user) { return done(null, false); } + if (!user.verifyPassword(password)) { return done(null, false); } + return done(null, user); + }); + }) +); +``` + +Midway adapts this by inheriting a Passport existing strategy through the `@CustomStrategy` and `PassportStrategy` classes. + +The asynchronous `validate` method replaces the original `verify` method. The `validate` method returns the verified user result. The parameters of the method are consistent with the original corresponding policy. + +The effect written in Midway is as follows: + +```typescript +// src/strategy/local.strategy.ts + +import { CustomStrategy, PassportStrategy } from '@midwayjs/passport'; +import { Strategy, IStrategyOptions } from 'passport-local'; +import { Repository } from 'typeorm'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { UserEntity } from './user'; +import * as bcrypt from 'bcrypt'; + +@CustomStrategy() +export class LocalStrategy extends PassportStrategy(Strategy) { + @InjectEntityModel(UserEntity) + userModel: Repository; + + //Verification of strategy + async validate(username, password) { + const user = await this.userModel.findOneBy({ username }); + if (!user) { + throw new Error('User does not exist ' + username); + } + if (!(await bcrypt.compare(password, user.password))) { + throw new Error('Password is incorrect ' + username); + } + + return user; + } + + // Constructor parameters of the current strategy + getStrategyOptions(): IStrategyOptions { + return { + usernameField: 'username', + passwordField: 'password', + passReqToCallback: true, + session: false + }; + } +} +``` + +:::tip + +Note: The validate method is a Promise alternative to the community policy verify. You do not need to pass the callback parameter at the end. + +::: + +In the official documentation of `passport-local`, after the strategy is implemented, it needs to be loaded into the business as middleware, such as: + +```typescript +app.post('/login/password', passport.authenticate('local', { + successRedirect: '/', + failureRedirect: '/login' +})); +``` + +:::tip + +Here `local` is the internal name of `passport-local`. + +::: + +In Midway, the `LocalStrategy` implemented above also needs to be loaded through middleware. + +Customize a middleware that inherits the basic middleware extended by `PassportMiddleware`. The example is as follows. + +```typescript +// src/middleware/local.middleware.ts + +import { Middleware } from '@midwayjs/core'; +import { PassportMiddleware, AuthenticateOptions } from '@midwayjs/passport'; +import { LocalStrategy } from '../strategy/local.strategy'; + +@Middleware() +export class LocalPassportMiddleware extends PassportMiddleware(LocalStrategy) { + //Set AuthenticateOptions + getAuthenticateOptions(): Promise | AuthenticateOptions { + return { + failureRedirect: '/login', + }; + } +} +``` + +Load middleware into the global or route. + +```typescript +// src/controller.ts +import { Post, Inject, Controller } from '@midwayjs/core'; +import { LocalPassportMiddleware } from '../middleware/local.middleware'; + +@Controller('/') +export class LocalController { + @Post('/passport/local', { middleware: [LocalPassportMiddleware] }) + async localPassport() { + console.log('local user: ', this.ctx.state.user); + return this.ctx.state.user; + } +} +``` + +Use curl to simulate a request. + +```bash +curl -X POST http://localhost:7001/passport/local -d '{"username": "demo", "password": "1234"}' -H "Content-Type: application/json" + +Result {"username": "demo", "password": "1234"} +``` + +:::caution + +Note: If you place middleware globally, remember to ignore routes that require login, otherwise the request will loop endlessly. + +::: + + + +### Example: Jwt strategy + +**Additional installation** of dependencies and policies is required first: + +```bash +$ npm i @midwayjs/jwt passport-jwt --save +``` + +Additional jwt components are enabled. + +```typescript +// configuration.ts + +import { join } from 'path'; +import * as jwt from '@midwayjs/jwt'; +import { Configuration, ILifeCycle,} from '@midwayjs/core'; +import * as passport from '@midwayjs/passport'; + +@Configuration({ + imports: [ + // ... + jwt, + passport + ], + importConfigs: [join(__dirname, './config')] +}) +export class MainConfiguration implements ILifeCycle {} + +``` + +Then set in the configuration, the default is not encrypted, please do not store sensitive information in the payload. + +```typescript +// src/config/config.default.ts +export default { + // ... + jwt: { + secret: 'xxxxxxxxxxxxxx', // fs.readFileSync('xxxxx.key') + expiresIn: '2d' // https://github.com/vercel/ms + }, +} +``` + +```typescript +// src/strategy/jwt.strategy.ts + +import { CustomStrategy, PassportStrategy } from '@midwayjs/passport'; +import { Strategy, ExtractJwt } from 'passport-jwt'; +import { Config } from '@midwayjs/core'; + +@CustomStrategy() +export class JwtStrategy extends PassportStrategy ( + Strategy, + 'jwt' +) { + @Config('jwt') + jwtConfig; + + async validate(payload) { + return payload; + } + + getStrategyOptions(): any { + return { + secretOrKey: this.jwtConfig.secret + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken() + }; + } +} +``` +:::tip + +Note: validate method is an Promise alternative to community policy verify. You don't need to pass callback parameters at the end. + +::: + +```typescript +// src/middleware/jwt.middleware.ts + +import { Middleware } from '@midwayjs/core'; +import { PassportMiddleware, AuthenticateOptions } from '@midwayjs/passport'; +import { JwtStrategy } from '../strategy/jwt.strategy'; + +@Middleware() +export class JwtPassportMiddleware extends PassportMiddleware(JwtStrategy) { + getAuthenticateOptions(): Promise | AuthenticateOptions { + return {}; + } +} +``` + +```typescript +import { Post, Inject, Controller } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { JwtService } from '@midwayjs/jwt'; +import { JwtPassportMiddleware } from '../middleware/jwt.middleware'; + +@Controller('/') +export class JwtController { + + @Inject() + jwt: JwtService; + + @Inject() + ctx: Context; + + @Post('/passport/jwt', { middleware: [JwtPassportMiddleware] }) + async jwtPassport() { + console.log('jwt user:', this.ctx.state.user); + return this.ctx.state.user; + } + + @Post('/jwt') + async genJwt() { + return { + t: await this.jwt.sign({ msg: 'Hello Midway' }) + }; + } +} +``` + +Use curl to simulate requests + +```bash +curl -X POST http://127.0.0.1:7001/jwt + +Result {"t": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} + +curl http://127.0.0.1:7001/passport/jwt -H "Authorization: Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +Results {"msg": "Hello Midway","iat": 1635468727,"exp": 1635468827} + +``` + +## Customize other policies + +`@midwayjs/passport` supports customizing [other policies](http://www.passportjs.org/packages/). Here, take Github OAuth as an example. +`npm I passport-github` first, and then write the following code: + +```typescript +// github-strategy.ts + +import { CustomStrategy, PassportStrategy } from '@midwayjs/passport'; +import { Strategy, StrategyOptions } from 'passport-github'; + +const GITHUB_CLIENT_ID = 'xxxxxx', GITHUB_CLIENT_SECRET = 'xxxxxxxx'; + +@CustomStrategy() +export class GithubStrategy extends PassportStrategy(Strategy, 'github') { + async validate(...payload) { + return payload; + } + getStrategyOptions() { + return { + clientID: GITHUB_CLIENT_ID + clientSecret: GITHUB_CLIENT_SECRET + callbackURL: 'https://127.0.0.1:7001/auth/github/cb' + }; + } +} + +``` +```typescript +// src/middleware/github.middleware.ts + +import { PassportMiddleware } from '@midwayjs/passport'; +import { Middleware } from '@midwayjs/core'; +import { GithubStrategy } from './github-strategy.ts'; + +@Middleware() +export class GithubPassportMiddleware extends PassportMiddleware(GithubStrategy) { + getAuthenticateOptions(): AuthenticateOptions | Promise { + return {}; + } +} +``` +```typescript +// src/controoer/auth.controller.ts + +import { Controller, Get, Inject } from '@midwayjs/core'; +import { GithubPassportMiddleware } from './github.middleware'; + +@Controller('/oauth') +export class AuthController { + @Inject() + ctx; + + @Get('/github', { middleware: [GithubPassportMiddleware] }) + async githubOAuth() {} + + @Get('/github/cb', { middleware: [GithubPassportMiddleware] }) + async githubOAuthCallback() { + return this.ctx.state.user; + } +} + +``` + + + +## Policy options + +| Options | Type | Description | +| ------------------- | ------- | ------------------------------------------------- | +| failureRedirect | string | The url of the failed jump. | +| session | boolean | The default is true. When it is turned on, the user information will be automatically set to the session | +| sessionUserProperty | string | set to the key on the session, the default user | +| userProperty | string | The key set to ctx.state or req, the default user | +| successRedirect | string | The address that jumps after user authentication is successful. | + + + +## Frequently Asked Questions + + + +### 1. Failed to serialize user into session + +Since the passport will try to write user data to the session by default, if you do not need to save the user to the session, you can turn off session support. + +```typescript +// src/config/config.default +export default { + // ... + passport: { + session: false + } +} +``` + +If you explicitly need to save data to the Session, you need to rewrite the serialization method of the `PassportStrategy` User. Please do not save particularly large data. + +For example, the local strategy implemented by oneself. + +```typescript +// src/strategy/local.strategy.ts + +import { CustomStrategy, PassportStrategy } from '@midwayjs/passport'; +import { Repository } from 'typeorm'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { UserEntity } from './user'; +import * as bcrypt from 'bcrypt'; + +@CustomStrategy() +export class LocalStrategy extends PassportStrategy(Strategy) { + + // ... + serializeUser(user, done) { + // You can save only the user name + done(null, user.username); + } + + deserializeUser(id, done) { + + // This is not an asynchronous method. You can check the user data from other places according to the user name. + const user = getUserFromDataBase(id); + + done(null, user); + } +} +``` + + + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/pm2.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/pm2.md new file mode 100644 index 000000000000..ec163912ac5c --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/pm2.md @@ -0,0 +1,81 @@ +# pm2 + +[PM2](https://github.com/Unitech/pm2) is the production process manager for Node.js applications with built-in load balancers. It can be used to simplify many tedious tasks of Node application management, such as performance monitoring, automatic restart, load balancing, etc. + +## Installation + +We usually install pm2 to the global. + +```bash +$ npm install pm2 -g# command line installation pm2 +``` + +## Common commands + +```bash +$ pm2 start# Start a service +$ pm2 list# lists the current services +$ pm2 stop# stop a service +$ pm2 restart# Restart a service +$ pm2 delete# delete a service +$ pm2 logs# view the output log of the service +``` + +For example, `pm2 list` is displayed in a table. + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1616560437389-b193a0d0-b463-49f1-a347-8dec20e7504d.png) + +All services of pm2 have an array id, and you can quickly operate it with id. + +For example: + +```bash +$ pm2 stop 1 # stop service number 1 +$ pm2 delete 1 # delete service number 1 +``` + +Use the `-- name` parameter to add an application name. + +```bash +$ pm2 start ./bootstrap.js --name test_app +``` + +Then you can use this application name to operate start and stop. + +```bash +$ pm2 stop test_app +$ pm2 restart test_app +``` + +## Start application + +Midway applications generally use `npm run start` for online deployment. The corresponding command is `NODE_ENV = production node bootstrap.js`. + +:::info +Compile npm run build is required before deployment +::: + +The corresponding pm2 command is + +```bash +$ NODE_ENV=production pm2 start ./bootstrap.js --name midway_app -i 4 +``` + +- -- name is used to specify the application name +- -I is used to specify the number of instances (processes) to be started and will be started in cluster mode + +The effect is as follows: + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1616562075255-088155ee-7c4f-4eae-b5c5-db826f78b519.png) + +## Docker container startup + +In the Docker container, the code started in the background will be exited, which will not achieve the expected effect. Pm2 uses another command to support container startup. + +Please change the command to pm2-runtime start. + +```bash +$ NODE_ENV=production pm2-runtime start ./bootstrap.js --name midway_app -i 4 +``` + +For more information about the pm2 behavior, see [Pm2 container deployment](https://www.npmjs.com/package/pm2#container-support). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/process_agent.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/process_agent.md new file mode 100644 index 000000000000..c3c477202ee8 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/process_agent.md @@ -0,0 +1,132 @@ +# Process Agent + +midway encapsulates `@midwayjs/process-agent` to solve data inconsistencies between processes in some multi-process scenarios in node scenarios, or it is impossible to specify the master process to execute a method. + + +Examples: + +- If pm2, cluster, and multi-process deployment methods are used, and memory cache is used, then the cache is in its own process. +- prometheus, when acquiring `/metrics`, it is necessary to collect data from all processes, not from one process. +- Health check, if there are 4 processes, if one process is abnormal, the health check should fail. + + + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ❌ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## Installation method + +Usage: + +```bash +$ npm install @midwayjs/process-agent@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/process-agent": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## Introducing components + +Usage of `configuration.ts`: + +```typescript +import * as processAgent from '@midwayjs/process-agent'; + +@Configuration({ + imports: [ + // ... + processAgent + ], +}) +export class MainConfiguration { +} + +``` +## Usage + +Business code UserService: + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { TestService } from './test'; + +@Provide() +export class UserService { + + @Inject() + testService: TestService; + + async getUser() { + let result = await this.testService.setData(1); + return result; + } +} + +``` +Then when calling the testService, it is hoped that it will only be executed in the main process: + +```typescript +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; +import { RunInPrimary } from '@midwayjs/process-agent'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class TestService { + + data: any = 0; + + @RunInPrimary() + async setData( B) { + this.data = B; + return this.data; + } + + @RunInPrimary() + async getData() { + return this.data; + } +} + +``` +Note that the data returned by the execution is limited to serializable data, such as ordinary JSON, and does not support data that cannot be serialized such as including methods. + + +## Effect description +Assume that it is started in a multi-process manner such as pm2 or egg-script. Assume that this is a request. + +First: + +- 1. Set setData +- 2. Then get the getData + + +If this decorator is not RunInPrimary, the request may fall on process 2 or process 3, and the updated data may not be obtained. + +So RunInPrimary can ensure that the execution of this function falls to the main process. + + +## Function Solicitation +If you have other related functions that can be placed in this package, please mention them in the comment area or [issue](https://github.com/midwayjs/midway/issues). We will discuss and implement them with you. + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/prometheus.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/prometheus.md new file mode 100644 index 000000000000..34477aa22867 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/prometheus.md @@ -0,0 +1,302 @@ +# Prometheus + +Prometheus (Prometheus) is a monitoring system originally built on the SoundCloud. Since 2012, it has become a community open source project with a very active developer and user community. In order to emphasize open source and independent maintenance, Prometheus joined the Cloud Native Cloud Computing Foundation (CNCF) in 2016, becoming the second hosting project after Kubernetes. + +Grafana is an open source measurement analysis and visualization suite. A front-end tool developed purely Javascript displays custom reports and charts by accessing libraries (such as InfluxDB). Grafana supports many different data sources. Each data source has a specific query editor, and the features and functions customized by the editor are the specific data sources that are exposed. The Prometheus is exactly one of its supported data sources. + +This article introduces how Midway accesses Grafana + Prometheus. + +The access effect is as follows: + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617259935548-a2df4339-3229-4391-bd3d-4ba8e6979d4d.png) + +## Installation dependency + +First install the indicator monitoring component provided by Midway: + +```bash +$ npm install @midwayjs/prometheus@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/prometheus": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## Introducing components + +In `configuration.ts`, introduce this component: + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as prometheus from '@midwayjs/prometheus'; // Import module +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + prometheus + ], + importConfigs: [join(__dirname, 'config')] +}) +export class MainConfiguration {} +``` + +When we started our application, there was an extra `${host }:${ port}/metrics` when we visited it. + +:::info +Prometheus the monitoring data is obtained based on HTTP, please include one of web/koa/express component.. +::: + +Access interface, return as follows, the contents are the current indicators. + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260048533-4f725824-9471-40c9-be8b-6dcbf27d9cca.png) + +## Other configurations + +The indicator component also provides relevant configurations to facilitate developers to configure. + +You can modify the configuration of the prometheus in `config.default.ts`. + +```typescript +// src/config/config.default +export default { + // ... + prometheus: { + labels: { + APP_NAME: 'demo_project', + }, + }, +} +``` + +For more configurations, we can look at the definitions for configuration. + +Through configuration, for example, we can classify which nodes are the same application, because when we deploy, the node program is distributed. For example, we added APP_NAME above to distinguish different applications, so that we can distinguish different applications in the monitoring index. + +## Data acquisition + +The components we introduced earlier in Midway are mainly to add indicator modules to Node. Next, we need Prometheus to collect our index data. + +If the developer's department already has Prometheus + grafana, it only needs to report the application's indicator address to PE or through the interface. Here we assume that everyone has no Prometheus + grafana, and then follow the following description. + +## Deploy Prometheus + +Here we use docker-compose to build Prometheus. The docker-compose.yml file is as follows: + +```yaml +version: '2.2' +services: + tapi: + logging: + driver: 'json-file' + options: + max-size: '50m' + image: prom/prometheus + restart: always + volumes: + - ./prometheus_data:/prometheus_data:rw + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - ./targets.json:/etc/prometheus/targets.json + command: + - '--storage.tsdb.path=/prometheus_data' + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.retention=10d' + - '--web.enable-lifecycle' + ports: + - '9090:9090' +``` + +The `prometheus.yml` file is as follows: + +```yaml +global: + scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. +scrape_configs: + - job_name: 'node' + file_sd_configs: + - refresh_interval: 1m + files: + - '/etc/prometheus/targets.json' + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] +``` + +Then, the collected `targets.json` is as follows: `${ip}` in the following file is replaced with the ip address of the server where the Node.js application is located. + +```json +[ + { + "targets": ["${ip}:7001"] + "labels": { + "env": "prod ", + "job": "api" + } + } +] +``` + + + +Then we start the `docker-compose.yml` file, + +```bash +$ docker-compose up +``` + +At this point, Prometheus will already pull the indicator data of our Node application. + +What to do if you want to update target: +After modifying this targets.json file, it is hot loaded by the prometheus reload method. +The method is as follows: + +```bash +$ curl-X POST http://${prometheus ip}:9090/-/reload +``` + +Then we can check the prometheus page or confirm whether it takes effect. The interface address is: + +```text +http://${prometheus ip}:9090/classic/targets +``` + +The next step is how to show the collected data. + + + +## Data presentation + +We can use Grafana to show our data. + +Here we simply use Docker to build a Grafana: + +```bash +$ docker run -d --name=grafana -p 3000:3000 grafana/grafana +``` + +You can also put grafana and prometheus together and use docker-compose for unified management. + +Add grafana to `docker-compose.yml`, example: + +```yaml +version: '2.2' +services: + tapi: + logging: + driver: 'json-file' + options: + max-size: '50m' + image: prom/prometheus + restart: always + volumes: + - ./prometheus_data:/prometheus_data:rw # prometheus Data mapping directory + - ./prometheus.yml:/etc/prometheus/prometheus.yml # prometheus Configuration mapping file + - ./targets.json:/etc/prometheus/targets.json + command: + - '--storage.tsdb.path=/prometheus_data' + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.retention=10d' + - '--web.enable-lifecycle' + ports: + - '9090:9090' + // highlight-start + grafana: + image: grafana/grafana + container_name: "grafana0" + ports: + - "3000:3000" + restart: always + volumes: + - "./grafana_data:/var/lib/grafana" # grafana data mapping directory + - "./grafana_log:/var/log/grafana" # grafana log mapping directory + // highlight-end +``` + +Restart the `docker-compose.yml` file + +```bash +docker-compose restart +``` +![](https://cdn.nlark.com/yuque/0/2022/png/525744/1667300763153-5ee476a7-00ff-4899-92ba-5985995b4862.png) + +Complete any of the above, and then we access 127.0.0.1:3000, the default account password: admin:admin. + +After the visit, the effect is as follows: + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260561047-c2643a69-6258-491b-937d-9bfc4558252f.png) + +Then we let Grafana access our Prometheus data sources: + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260581029-1e2e06a8-3054-4ad8-96b5-d50ab9bb1612.png) + +Then we click Grafana to add the chart: + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725466020-28793a78-c03b-48fa-bf16-0c9c8ecc1a94.png) + +Select 14403 ID here, then click load, then click Next, and then click import to see the effect we have just accessed. + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725497338-a32a8982-d51f-4e74-b511-dc10a7c66d80.png) + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725514630-4f654f10-ef3a-41f7-b403-02832d3ef7d8.png) + +In this way, developers can operate and operate their own Node programs, for example, whether an NPM package has recently been introduced to cause any memory leakage, and whether there has been an application restart recently. + +Of course, it can also support other custom operations. + + +## Socket-io scene + +Usage: + +```bash +$ npm install @midwayjs/prometheus-socket-io@3 --save +``` + +Usage: + +```typescript +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; +import * as prometheus from '@midwayjs/prometheus'; +import * as prometheusSocketIo from '@midwayjs/prometheus-socket-io'; + +@Configuration({ + imports: [prometheus, prometheusSocketIo], + importConfigs: [join(__dirname, './config')] +}) +export class MainConfiguration {} +``` + +Then you can see the socket-io data on the/metrics side. + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1631090438583-d925c13c-371a-4037-9f53-edaa34580aab.png) + +A total of 8 new indicators have been added. +The Grafana template ID will be provided for everyone to use later. + + +## Function introduction + +- [x] Sort by appName +- [x] View the qps situation of different paths +- [x] View the distribution of different statuses +- [x] Query the rt situation of different paths +- [x] CPU usage of the process +- [x] memory usage of the process +- [x] stack situation +- [x] Event Loop +- [ ] Wait diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/rabbitmq.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/rabbitmq.md new file mode 100644 index 000000000000..f2aa5885c6d5 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/rabbitmq.md @@ -0,0 +1,601 @@ +# RabbitMQ + +In the architecture of a complex system, there will be microservices responsible for processing message queues, as shown in the following figure: service A is responsible for generating messages to the message queue, while service B is responsible for consuming tasks in the message queue. + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01SYMbCz1moVSVLl7S2_!!6000000005001-2-tps-646-251.png) + +In Midway, we provide the ability to subscribe to rabbitMQ specifically to meet such needs of users. + +Related information: + +**Subscribe to service** + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ❌ | +| Can be used for integration | ✅ | +| Contains independent main framework | ✅ | +| Contains independent logs | ❌ | + + + +## Basic concept + + +The concept of RabbitMQ is more complicated. It is based on the Advanced Message Queuing Protocol, that is, the Advanced Message Queuing Protocol(AMQP). Please read the relevant reference documents for the first time. + + +AMQP has some concepts. Queue, Exchange and Binding form the core of AMQP protocol, including: + +- Producer: message producer, that is, the program that delivers messages. +- Broker: Message Queuing Server Entity. + - Exchange: Message Switch, which specifies which rules the message is routed to and to which queue. + - Binding: Binding, its function is to bind Exchange and Queue according to routing rules. + - Queue: Message queue carrier, where each message is put into one or more queues. +- Consumer: Message consumer, that is, the program that accepts messages. + + + +Simply understand, messages are published to Exchange (switches) through Publisher, Consumer receive messages by subscribing to Queue, and Exchange and Queue are connected through routing. + + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01fLrucw1FVNbCx4NqG_!!6000000000492-2-tps-700-328.png) + + + +## Consumer (Consumer) Usage + + +### Installation dependency + + +Midway provides the ability to subscribe to rabbitMQ and can be deployed and used independently. Install the `@midwayjs/rabbitmq` module and its definition. +```bash +$ npm i @midwayjs/rabbitmq@3 --save +$ npm i amqplib --save +$ npm i @types/amqplib --save-dev +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/rabbitmq": "^3.0.0", + "amqplib": "^0.10.1 ", + // ... + }, + "devDependencies": { + "@types/amqplib": "^0.8.2 ", + // ... + } +} +``` + +## Open the component + +`@midwayjs/rabbmitmq` can be used as a separate main framework. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as rabbitmq from '@midwayjs/rabbitmq'; + +@Configuration({ + imports: [ + rabbitmq + ], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} +``` + +It can also be attached to other mainframes, such as `@midwayjs/koa` . + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as rabbitmq from '@midwayjs/rabbitmq'; + +@Configuration({ + imports: [ + koa, + rabbitmq + ], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} +``` + +### Directory structure + + +We generally divide capabilities into producers and consumers, and subscriptions are the capabilities of consumers. + + +We usually put consumers in consumer catalogues. For example, `src/consumer/userConsumer.ts`. +``` +➜ my_midway_app tree +. +├── src +│ ├── consumer +│ │ └── user.consumer.ts +│ ├── interface.ts +│ └── service +│ └── user.service.ts +├── test +├── package.json +└── tsconfig.json +``` +The code example is as follows. + +```typescript +import { Consumer, MSListenerType, RabbitMQListener, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/rabbitmq'; +import { ConsumeMessage } from 'amqplib'; + +@Consumer(MSListenerType.RABBITMQ) +export class UserConsumer { + + @Inject() + ctx: Context; + + @RabbitMQListener('tasks') + async gotData(msg: ConsumeMessage) { + this.ctx.channel.ack(msg); + } + +} + +``` +`@Consumer` the decorator, which provides the consumer identity, and its parameters specify the type of a certain consumption framework. For example, we specify the type of `MSListenerType.RABBITMQ` here, which refers to the rabbitMQ type. + + +The class that identifies the `@Consumer` can bind a RabbitMQ queue after using the `@RabbitMQListener` decorator for the method. + + +The parameter of the method is the received message of type `ConsumeMessage`. If you need to confirm the returned value, you must perform the `ack` operation on the server to specify the received data. + + +If you need to subscribe to multiple queues, you can use multiple methods or multiple files. + + +### RabbitMQ message context + + +The context of the subscription `RabbitMQ` data is the same as that of the Web, which contains a `requestContext` and a data binding for each message received. + + +`channel` can be taken from ctx. The entire ctx is defined: +```typescript +export type Context = { + channel: amqp.Channel; + requestContext: IMidwayContainer; +}; +``` + + +You can get the definition from the framework +```typescript +import { Context } from '@midwayjs/rabbitmq'; +``` + + +### Configure consumers + +We need to specify the address of rabbitmq in the configuration. + +```typescript +// src/config/config.default +import { MidwayConfig } from '@midwayjs/core'; + +export default { + // ... + rabbitmq: { + url: 'amqp://localhost' + } +} as MidwayConfig; +``` + +More configurations: + +| Property | Description | +| --- | --- | +| url | rabbitMQ connection information | +| socketOptions | amqplib. The second parameter of the connect | +| reconnectTime | Retry time after queue disconnection, default 10 seconds | + + + +### Fanout Exchange + + +Fanout is a specific switch that sends a message to the Queue to which the Exchange is bound if a match (binding) is met. Fanout Exchange ignores the RoutingKey settings and broadcasts the Message directly to all bound Queue. + +That is, all Queues subscribing to the switch will receive messages. + +For example, we have added two Queue and subscribed to the same switch. +```typescript +import { Consumer, MSListenerType, RabbitMQListener, Inject, App } from '@midwayjs/core'; +import { Context, Application } from '@midwayjs/rabbitmq'; +import { ConsumeMessage } from 'amqplib'; + +@Consumer(MSListenerType.RABBITMQ) +export class UserConsumer { + + @App() + app: Application; + + @Inject() + ctx: Context; + + @Inject() + logger; + + @RabbitMQListener('abc', { + exchange: 'logs', + exchangeOptions: { + type: 'fanout', + durable: false + }, + exclusive: true, + consumeOptions: { + noAck: true, + } + }) + async gotData(msg: ConsumeMessage) { + this.logger.info('test output1 =>', msg.content.toString('utf8')); + // TODO + } + + @RabbitMQListener('bcd', { + exchange: 'logs', + exchangeOptions: { + type: 'fanout', + durable: false + }, + exclusive: true + consumeOptions: { + noAck: true + } + }) + async gotData2(msg: ConsumeMessage) { + this.logger.info('test output2 =>', msg.content.toString('utf8')); + // TODO + } + +} + +``` + + +The subscribed ABC and BCD queues are bound to the same switch logs. As a result, both methods will be called. + + +### Direct Exchange + + +Direct Exchange is the RabbitMQ default Exchange that routes messages based entirely on RoutingKey. When setting the Binding between Exchange and Queue, you need to specify the RoutingKey (usually Queue Name). When sending a message, you also specify the same RoutingKey, and the message will be routed to the corresponding Queue. + + +In the following sample code, we do not fill in Queue Name, only add a routingKey, and the switch type is direct. +```typescript +import { Consumer, MSListenerType, RabbitMQListener, Inject, App } from '@midwayjs/core'; +import { Context, Application } from '../../../../../src'; +import { ConsumeMessage } from 'amqplib'; + +@Consumer(MSListenerType.RABBITMQ) +export class UserConsumer { + + @App() + app: Application; + + @Inject() + ctx: Context; + + @Inject() + logger; + + @RabbitMQListener ('', { + exchange: 'direct_logs', + exchangeOptions: { + type: 'direct', + durable: false + }, + routingKey: 'direct_key', + exclusive: true, + consumeOptions: { + noAck: true + } + }) + async gotData(msg: ConsumeMessage) { + // TODO + } +} + +``` + + +Direct messages are filtered according to routerKey, so only specific subscriptions can receive messages. + + + + +### Decorator parameters + + +The first parameter of the `@RabbitMQListener` decorator is queueName, which represents the queue to be listened. + + +The second parameter is an object, including queue, switch and other parameters. The detailed definition is as follows: +```typescript +export interface RabbitMQListenerOptions { + exchange?: string; + /** + * queue options + */ + exclusive?: boolean; + durable?: boolean; + autoDelete?: boolean; + messageTtl?: number; + expires?: number; + deadLetterExchange?: string; + deadLetterRoutingKey?: string; + maxLength?: number; + maxPriority?: number; + pattern?: string; + /** + * prefetch + */ + prefetch?: number; + /** + * router + */ + routingKey?: string; + /** + * exchange options + */ + exchangeOptions ?: { + type?: 'direct' | 'topic' | 'headers' | 'fanout' | 'match' | string; + durable?: boolean; + internal?: boolean; + autoDelete?: boolean; + alternateExchange?: string; + arguments?: any; + }; + /** + * consumeOptions + */ + consumeOptions ?: { + consumerTag?: string; + noLocal?: boolean; + noAck?: boolean; + exclusive?: boolean; + priority?: number; + arguments?: any; + } +} +``` + + + + +### Local test + + +Midway provides a simple test method for testing subscriptions to a certain data. The `@midwayjs/mock` tool provides a `createRabbitMQProducer` method for creating a producer through which you can create a queue and send messages to the queue. + + +Then, we start an app to automatically listen to the data in this queue and execute subsequent logic. + +```typescript +import { createRabbitMQProducer, close, creatApp } from '@midwayjs/mock'; + +describe('/test/index.test.ts', () => { + it('should test create message and get from app', async () => { + // create a queue and channel + const channel = await createRabbitMQProducer('tasks', { + isConfirmChannel: true + mock: false + url: 'amqp://localhost', + }); + + // send data to queue + channel.sendToQueue('tasks', Buffer.from('something to do')) + + // create app and got data + const app = await creatApp(); + + // wait a moment + + await close(app); + }); +}); + +``` + + +**Example 1** + + +Create a fanout exchange. +```typescript +const manager = await createRabbitMQProducer('tasks-fanout', { + isConfirmChannel: false + mock: false + url: 'amqp://localhost', +}); + +// Name of the exchange +const ex = 'logs'; +// Write a message +const msg = "Hello World!"; + +// Declare Switch +manager.assertExchange(ex, 'fanout', { durable: false }) // 'fanout' will broadcast all messages to all the queues it knows + +// Start the service +const app = await creatApp('base-app-fanout', { + url: 'amqp://localhost', + reconnectTime: 2000 +}); + +// Sent to the switch, because it is not persistent, you need to wait until the subscription service is up before sending it. +manager.sendToExchange(ex, '', Buffer.from(msg)) + +// Wait for a while +await sleep(5000); + +// Check result + +// Close producer +await manager.close(); + +// Close app +await close(app); +``` + + +**Example 2** + + +Create a direct exchange. +```typescript +/** + * direct type messages, targeted filtering according to routerKey + */ +const manager = await createRabbitMQProducer('tasks-direct', { + isConfirmChannel: false + mock: false + url: 'amqp://localhost', +}); + +// Name of the exchange +const ex = 'direct_logs'; +// Write a message +const msg = "Hello World!"; + +// Declare Switch +manager.assertExchange(ex, 'direct', { durable: false }) // 'fanout' will broadcast all messages to all the queues it knows + +const app = await creatApp('base-app-direct', { + url: 'amqp://localhost', + reconnectTime: 2000 +}); + +// Specify the routerKey here and send it to the switch +manager.sendToExchange(ex, 'direct_key', Buffer.from(msg)) + +// Check result + +await manager.close(); +await close(app); +``` + + +## Producer Usage Method + + +The producer (Producer), that is, the message producer in the first section, simply creates a client to send the message to the RabbitMQ service. + + +Note: Currently Midway does not use components to support message sending, the examples shown here are just written in Midway using pure SDK. + + +### Installation dependency + + +```bash +$ npm i amqplib amqp-connection-manager --save +$ npm i @types/amqplib --save-dev +``` + + +### Call the service to send a message + + +For example, we add a `rabbitmq.ts` file under the service file. +```typescript +import { Provide, Scope, ScopeEnum, Init, Autoload, Destroy } from '@midwayjs/core'; +import * as amqp from 'amqp-connection-manager' + +@Autoload() +@Provide() +@Scope(ScopeEnum.Singleton) // Singleton singleton, globally unique (process level) +export class RabbitmqService { + + private connection: amqp.AmqpConnectionManager; + + private channelWrapper; + + @Init() + async connect() { + // To create a connection, you can put the configuration in Config and inject it into it. + this.connection = await amqp.connect('amqp://localhost'); + + // Create channel + this.channelWrapper = this.connection.createChannel({ + json: true, + setup: function(channel) { + return Promise.all ([ + // Binding queue + channel.assertQueue("tasks", { durable: true }) + ]); + } + }); + } + + // Send a message + public async sendToQueue(queueName: string, data: any) { + return this.channelWrapper.sendToQueue(queueName, data); + } + + @Destroy() + async close() { + await this.channelWrapper.close(); + await this.connection.close(); + } +} + +``` +Probably created a service to encapsulate message communication, and it is the only Singleton singleton in the world. Due to the addition of `@AutoLoad` decorator, initialization can be self-executed. + + +In this way, the basic calling service is abstract. We only need to call `sendToQueue` method where it is used. + + +For example: + + +```typescript +@Provide() +export class UserService { + + @Inject() + rabbitmqService: RabbitmqService; + + async invoke() { + // TODO + + // Send a message + await this.rabbitmqService.sendToQueue('tasks', {hello: 'world'}); + } +} +``` + + +## Reference document + + +- [Understanding RabbitMQ Exchange](https://zhuanlan.zhihu.com/p/37198933) +- [RabbitMQ for Node.js in 30 steps](https://github.com/Gurenax/node-rabbitmq) diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/redis.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/redis.md new file mode 100644 index 000000000000..ab50743d5ca0 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/redis.md @@ -0,0 +1,224 @@ +# Redis + +Here is how to quickly use Redis in Midway. + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## Installation dependency + +`@midwayjs/redis` is the main function package. + +```bash +$ npm i @midwayjs/redis@3 --save +``` +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/redis": "^3.0.0", + // ... + } +} +``` + + + + +## Introducing components + + +First, introduce components and import them in `src/configuration.ts`: +```typescript +import { Configuration } from '@midwayjs/core'; +import * as redis from '@midwayjs/redis'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + redis // import redis components + ], + importConfigs: [ + join(__dirname, 'config') + ], +}) +export class MainConfiguration { +} +``` + + +## Configure Redis + + +**Single-client configuration** +```typescript +// src/config/config.default.ts +export default { + // ... + redis: { + client: { + port: 6379, // Redis port + host: "127.0.0.1", // Redis host + password: "auth ", + db: 0 + }, + }, +} +``` +**Sentinel configuration** +```typescript +// src/config/config.default.ts +export default { + // ... + redis: { + client: { + sentinels: [{ // Sentinel instances + port: 26379, // Sentinel port + host: '127.0.0.1', // Sentinel host + }], + name: 'mymaster', // Master name + password: 'auth', + db: 0 + }, + }, +} +``` + + +**Cluster mode configuration, you need to configure multiple** +```typescript +// src/config/config.default.ts +export default { + // ... + redis: { + // Cluster Redis + client: { + cluster: true + nodes: [{ + host: 'host', + port: 'port', + },{ + host: 'host', + port: 'port', + }], + redisOptions: { + family: '', + password: 'xxxx', + db: 'xxx' + } + } + }, +} +``` + +**Configure multiple clients.** +```typescript +// src/config/config.default.ts +export default { + // ... + redis: { + // Multi Redis + clients: { + instance1: { + host: 'host', + port: 'port', + password: 'password', + db: 'db', + }, + instance2: { + host: 'host', + port: 'port', + password: 'password', + db: 'db', + }, + }, + }, +} +``` +The [ioredis document](https://github.com/luin/ioredis/blob/master/API.md#new_Redis_new) can be viewed for more parameters. + + +## Use Redis service + + +We can inject it into any code. +```typescript +import { Provide, Controller, Inject, Get } from '@midwayjs/core'; +import { RedisService } from '@midwayjs/redis'; + +@Provide() +export class UserService { + + @Inject() + redisService: RedisService; + + async invoke() { + + // Simple setup + await this.redisService.set('foo', 'bar'); + + // Set the expiration time in seconds. + await this.redisService.set('foo', 'bar', 'EX', 10); + + // get data + const result = await this.redisService.get('foo'); + + // result => bar + } +} +``` + + +You can use `RedisServiceFactory` to get different instances. +```typescript +import { RedisServiceFactory } from '@midwayjs/redis'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + redisServiceFactory: RedisServiceFactory; + + async save() { + const redis1 = this.redisServiceFactory.get('instance1'); + const redis2 = this.redisServiceFactory.get('instance3'); + + //... + + } +} +``` + +It can also be obtained through decorators. + +```typescript +import { RedisServiceFactory, RedisService } from '@midwayjs/redis'; +import { InjectClient } from '@midwayjs/core'; + +@Provide() +export class UserService { + + @InjectClient(RedisServiceFactory, 'instance1') + redis1: RedisService; + + @InjectClient(RedisServiceFactory, 'instance3') + redis2: RedisService; + + async save() { + //... + } +} +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/render.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/render.md new file mode 100644 index 000000000000..e307802b5730 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/render.md @@ -0,0 +1,453 @@ +# Template rendering + +This component is used to render ejs and nunjucks templates using the server in midway system. + +Related information: + +| Web support | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ❌ | + + + + +## Use ejs + + +### Installation dependency + + +Select the corresponding template installation dependency. +```bash +$ npm i @midwayjs/view-ejs@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/view-ejs": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +### Introducing components + + +First, introduce components and import them in `configuration.ts`: +```typescript +import { Configuration } from '@midwayjs/core'; +import * as view from '@midwayjs/view-ejs'; +import { join } from 'path' + +@Configuration({ + imports: [ + View // import ejs components + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` +### Configuration + +Configure suffixes to map to the specified engine. + +```typescript +// src/config/config.default.ts +export default { + // ... + view: { + mapping: { + '.ejs': 'ejs', + }, + }, + // ejs config + ejs: {} +} +``` +### Use + + +Note that the default view directory is `${appDir}/view`. Create a `hello.ejs` file in the view directory. + + +The directory structure is as follows: +``` +➜ my_midway_app tree +. +├── src +│ └── controller ## Controller directory +│ └── home.ts +├── view ## Template directory +│ └── hello.ejs +├── test +├── package.json +└── tsconfig.json +``` + + +We write some ejs format content in the template, such: +```typescript +// view/hello.ejs +hello <%= data %> +``` + + +Rendering in Controller. +```typescript +import { Inject, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async render() { + await this.ctx.render('hello.ejs', { + data: 'world', + }); + } +} +``` + +### Configure suffix + +The default suffix is `.html`. In order to change the suffix to `.ejs`, we can add a `defaultExtension` configuration. + +```typescript +// src/config/config.default.ts +export default { + // ... + view: { + defaultExtension: '.ejs', + mapping: { + '.ejs': 'ejs', + }, + }, + // ejs config + ejs: {} +} +``` + +In this way, we do not need to add suffixes when rendering. + +```typescript +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async render() { + await this.ctx.render('hello', { + data: 'world', + }); + } +} +``` + + +### Default rendering engine + +We can set the default rendering engine by `defaultViewEngine`. + +Its role is to use the engine specified by the `defaultViewEngine` field to render when the template suffix encountered, such as `.html` is not found in the `mapping` field of the configuration. + +```typescript +// src/config/config.default.ts +export default { + // ... + view: { + defaultViewEngine: 'ejs', + mapping: { + '.ejs': 'ejs', + }, + }, + // ejs config + ejs: {} +} +``` + +In this way, if the template is a suffix of `.html`, `ejs` will still be used for rendering because it is not specified in the `mapping`. + +### Configure multiple template directories + +If we need to encapsulate the code as a component, we need to support different template directories. + +The default template directory is `${appDir}/view`. We can add other directories to `rootDir` fields. + +```typescript +// src/config/config.default.ts + +// Modify the default directory of the default view component +export default { + // ... + view: { + rootDir: { + default: path.join(__dirname, './view') + } + }, +} + +// Other components need to add directory configuration +export default { + // ... + // Configuration of view components + view: { + rootDir: { + anotherRoot: path.join(__dirname, './view') + } + }, +} +``` + +Through the object merging mechanism, all `rootDir` can be merged together, and values are obtained inside the component for matching. + + + +## Use Nunjucks + + +Similar to ejs, just introduce the corresponding components. + + +1. Select the corresponding template installation dependency. +```bash +$ npm i @midwayjs/view-nunjucks@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/view-nunjucks": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +2. Introduce components and import them in `configuration.ts`: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as view from '@midwayjs/view-nunjucks'; +import { join } from 'path' + +@Configuration({ + imports: [ + view // import nunjucks components + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + +3. Add nunjucks configuration, such as default nunjucks. +```typescript +export default { + // ... + view: { + defaultViewEngine: 'nunjucks', + mapping: { + '.nj': 'nunjucks', + }, + }, +} +``` + + +4. Add templates to the view directory +```typescript +// view/test.nj +hi, {{ user }} +``` + + +Rendering in Controller. +```typescript +import { Inject, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async render() { + await ctx.render('test.nj', { user: 'midway' }); + } +} +``` +After the access, `hi, midway` is output. + + +If you need a custom filter, you can add it at the entrance. For example, a filter named `hello` is added below. +```typescript +import { App, Configuration, Inject } from '@midwayjs/core'; +import * as view from '@midwayjs/view-nunjucks'; +import { join } from 'path' + +@Configuration({ + imports: [view], + importConfigs: [join(__dirname, 'config')] +}) +export class MainConfiguration { + + @App() + app; + + @Inject() + env: view.NunjucksEnvironment; + + async onReady() { + this.env.addFilter('hello', (str) => { + return 'hi, '+ str; + }); + } +} + +``` +Can be used in the template +```typescript +{{name | hello}} +``` +Then render +```typescript +// controller +// ... +await ctx.render('test.nj', { name: 'midway' }); +``` +`hi, midway` is also output. + + + +## Custom template engine + +By default, we only provide ejs and nunjucks template engines. You can also write your own template engine code. + +### Implement template engine + +First, you need to create a template engine class for request scope, which will be initialized when each request is executed. You need to implement the `render` and `renderString` methods. If your template engine does not support a method, you can throw an exception. + +```typescript +// lib/view.ts +import { Provide, Config } from '@midwayjs/core'; +import { IViewEngine } from '@midwayjs/view'; + +@Provide() +export class MyView implements IViewEngine { + + @Config('xxxx') + viewConfig; + + async render(name: string, locals?: Record, options?: RenderOptions) { + return myengine.render(name, locals, options); + } + + async renderString(tpl: string, + locals?: Record, + options?: RenderOptions) { + + throw new Error('not implement'); + } +}; +``` + +These two methods accept three similar parameters, `renderString` the first parameter needs to pass in the template content to be parsed, while the `render` method parses the template file. + +`render(name, locals, viewOptions)` + +- name: the path from the `root` (default is `/view` ). +- Locals: data required by the template +- viewOptions: The template parameters for each rendering, the overridden configuration, can be overridden in the configuration file, which contains several parameters: + - root: the absolute path of the template + - Name: The original name value that calls the render method. + - locals: the original locals value that calls the render method. + +`renderString(tpl, locals, viewOptions)` + +- tpl: template name +- Locals: Same as `render` +- viewOptions: Same as `render` + +### Register template engine + +After implementing the custom template engine, we need to register it at the startup portal. + +By introducing `ViewManager`, we can use the `use` method to register a custom template engine. + +```typescript +// src/configuration.ts +import { Configuration, Inject, Provide } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as view from '@midwayjs/view'; +import { MyView } from './lib/my'; + +@Configuration({ + imports: [koa, view], + importConfigs: [join(__dirname, 'config')] +}) +export class MainConfiguration { + + @Inject() + viewManager: view.ViewManager; + + async onReady() { + this.viewManager.use('ejs', MyView); + } +} + +``` + + + +## Precautions + + +To use in egg(@midwayjs/web) scenarios, close view and its related plug-ins in `plugin.ts`. + + +```typescript +import { EggPlugin } from 'egg'; +export default { + // ... + view: false +} as EggPlugin; + +``` + + +Otherwise, the following similar errors will occur. +``` +TypeError: Cannot set property view of # which has only a getter +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/security.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/security.md new file mode 100644 index 000000000000..2aae88df53fb --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/security.md @@ -0,0 +1,362 @@ +# Security + +It is a common security component applicable to multiple frameworks such as `@midwayjs/faas`, `@midwayjs/web`, `@midwayjs/koa`, and `@midwayjs/express`. It supports multiple security policies such as `csrf` and `xss`. + +Related information: + +| Web support | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + + + +## Installation and use + +1. Installation Dependence + +```bash +$ npm i @midwayjs/security --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/security": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +2. Introduce components into the configuration + +```typescript +import * as security from '@midwayjs/security'; +@Configuration({ + imports: [ + // ...other components + security + ], +}) +export class MainConfiguration {} +``` + +--- + +## Prevent common security threats + + +### I. CSRF + +CSRF(Cross-site request forgery Cross-site Request Forgery) is an attack method that captive users to perform unintended operations on currently logged-in Web applications. + + +#### 1. Token synchronization mode +Render the token to the page when you respond to the page. After you enable the `csrf` configuration, you can obtain the `csrf token` by using `ctx.csrf`. Then, you can synchronize the output when you return to page html. + +```ts +@Controller('/') +export class HomeController { + @Inject() + ctx; + + @Get('/home') + async home() { + return '
+ title: + +
'; + } +} +``` + +The `_csrf` field in the preceding example can be changed in the configuration. For more information, see `Configuration-> csrf`. + + + +#### 2. Cookies mode + +If CSRF is configured by default, the token is set in the Cookie. You can use JS to obtain the token from the Cookies on the frontend page, and then add the ajax/fetch requests to the `header`, `query`, or `body`. + +```js +const csrftoken = Cookies.get('csrfToken'); +fetch('/api/post', { + method: 'POST', + headers: { + 'x-csrf-token': csrftoken + }, + ... +}); +``` + +By default, the framework contains the `CSRF token` in the `Cookie` file, which is easy to obtain when the front-end JS sends a request. However, cookies can be set for all subdomain names. Therefore, when our application cannot guarantee that all subdomain names are controlled, it may be at risk of being attacked by `CSRF` when stored in `cookies`. The framework provides a configuration item `useSession` to store token in the Session. + + +When the `CSRF token` is stored in a `Cookie`, if a user switch occurs in the same browser, the new user will still use the old token (previously used by the user). This will bring certain security risks. Therefore, you must call `ctx.rotateCsrfSecret()` to refresh the `CSRF token` every time you log in. For example: + + +```js +@Controller('/') +export class HomeController { + @Inject() + ctx; + + @Inject() + userService; + + @Get('/login') + async login(@Body('username') username: string, @Body('password') password: string) { + const user = await userService.login({ username, password }); + this.ctx.session = { user }; + this.ctx.rotateCsrfSecret(); + return { success: true }; + } +} +``` + +### II. XSS + +`XSS` (cross-site scripting cross-site scripting attack) attack is the most common Web attack and is a kind of code injection. It allows malicious users to inject code into the web page, and other users will be affected when watching the web page. + +`XSS` attack usually refers to injecting malicious instruction code into a web page by exploiting vulnerabilities left during web page development, so that users can load and execute malicious web page programs created by attackers. After the attack is successful, the attacker may be given higher permissions (such as performing some operations), private web content, sessions, cookies and other content. + + +#### 1. Reflective XSS attack + +The server receives insecure input from the client and triggers code execution on the client to initiate a `Web` attack. + +For example, when searching for a search website, the search results will display the search keywords. Enter `` in the search for keywords. After you click Search, if the page program does not handle the keywords, the code is directly executed on the page, and alert is displayed. + +The framework provides the `ctx.security.escape()` method for XSS filtering of strings. + +```ts +@Controller('/') +export class HomeController { + @Inject() + ctx; + + @Get('/home') + async home() { + const str = ''; + const escapedStr = this.ctx.security.escape(str); + // + return escapedStr; + } +} +``` + +In addition, when the content output by the website is used as a js script. `ctx.security.js()` is needed to filter at this time. + +In another case, you need to output `json` in `js`. If you do not escape the json, it is easily exploited as a `XSS` vulnerability. The framework provides `ctx.security.json (variable)` to provide json encode to prevent XSS attacks. + + +```ts +@Controller('/') +export class HomeController { + @Inject() + ctx; + + @Get('/home') + async home() { + return ''; + } +} +``` + +#### 2. Storage-type XSS attacks. + +By submitting content with malicious scripts and storing it on the server, a Web attack is launched when others see the content. For example, in the comment box of some websites, the user maliciously uses some code as the comment content. If there is no filtering, the malicious code will be executed when other users see the comment. + + +The framework provides `ctx.security.html()` for filtering. + + +#### 3. Other XSS prevention methods + +The browser itself has a certain ability to prevent various attacks, and they usually take effect by opening the Web security header. The framework has built-in support for some common Web security headers. + +**CSP** + +`Content Security Policy`, referred to as `CSP`, is used to define which resources can be loaded on a page to reduce the occurrence of `XSS`. + +This function is disabled by default, which can be enabled by the `csp: {enable: true}` configuration. This function can effectively prevent `XSS` attacks. To configure `CSP`, you must understand the `policy` policy of `CSP`. For more information, see [What is the CSP?](https://www.zhihu.com/question/21979782/answer/122682029) + + +**X-Download-Options:noopen** + +This feature is enabled by default. You can use the `noopen: {enable: false}` configuration to disable the Open button in the download box under IE to prevent files downloaded under IE from being enabled by default. + +**X-Content-Type-Options:nosniff** +The IE8 automatic sniffing mime function is disabled and turned off by default (it can be configured by `nosniff: {enable: true}` ). For example, text/plain is rendered as text/html, especially when the content of serve on this site is not necessarily trusted. + +**X-XSS-Protection** +Some XSS detection and prevention provided by IE, enabled by default (can be disabled by `xssProtection: {enable: false}` configuration) + +The default value of close is false, that is, set to 1; mode = block + +--- + + +## Configuration + +The default configuration is as follows: + +```ts +// src/config/config.default +export default { + // ... + + // default configuration + security: { + csrf: { + enable: true + type: 'ctoken', + useSession: false + cookieName: 'csrfToken', + sessionName: 'csrfToken', + headerName: 'x-csrf-token', + bodyName: '_csrf', + queryName: '_csrf', + refererWhiteList: [] + }, + xframe: { + enable: true + value: 'SAMEORIGIN', + }, + csp: { + enable: false + }, + hsts: { + enable: false + maxAge: 365*24*3600 + includeSubdomains: false + }, + noopen: { + enable: false + }, + nosniff: { + enable: false + }, + xssProtection: { + enable: true + value: '1; mode=block', + }, + }, +} + +``` + +### csrf + +| Configuration Item | Type | Description | Default | +| --- |--------------------------------------| --- | --- | +| enable | boolean | Whether to open | true | +| type | 'all' / 'any' / 'ctoken' / 'referer' | Csrf check type | 'ctoken' | +| useSession | boolean | Is CSRF token stored in session | false | +| cookieName | string | The field where the token is stored in the cookie | 'csrfToken' | +| sessionName | string | The field where the token is stored in the session | 'csrfToken' | +| headerName | string | The field where the token is stored in the header | 'x-csrf-token' | +| bodyName | string | The field where the token is stored in the body | '_csrf' | +| queryName | string | The field where the token is stored in the query | '_csrf' | +| refererWhiteList | Array\ | White list of allowed sources | [] | + +#### Does the configuration refererWhiteList not take effect? ++ Reason 1: You need to configure the host part of the referer in the refererWhiteList. For example, if the referer is `https:// midway-demo.com:1234/docs`, you need to configure `midway-demo.com:1234` in the refererWhiteList. ++ Reason 2: The refererWhiteList takes effect only when the type is `referer` in the csrf configuration. The default type is `ctoken` and needs to be changed to `referer`. ++ Reason 3: The referer field in the sent http request is not a standard url address (for example, no request protocol is added). Refer to [MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referer) + +### xframe + + +Xframe is used to configure the `X-Frame-Options` response header to indicate whether a page can be displayed in `frame`, `iframe`, `embed`, or `object`. Sites can avoid `clickjacking` attacks by ensuring that websites are not embedded in other people's sites. + +There are three possible values for `X-Frame-Options`: + ++ X-Frame-Options: deny: The page is not allowed to be displayed in frame. ++ X-Frame-Options: sameorigin: This page can be displayed in the frame of the same domain name page. ++ X-Frame-Options: allow-from https://example.com/:该页面可以在指定源的frame中展示 + + + +| Configuration Item | Type | Description of action | Default | +| --- | --- | --- | --- | +| enable | boolean | Whether to open | true | +| value | string | X-Frame-Options value | 'SAMEORIGIN' | + + + +### hsts + +`HTTP Strict Transport Security` (commonly referred to as `HSTS`) is a security feature that tells browsers that they can only access current resources through `HTTPS`, not `HTTP`. + +| Configuration Item | Type | Description of action | Default | +| --- | --- | --- | --- | +| enable | boolean | Whether to open | false | +| maxAge | number | In the `seconds` after the browser receives this request, all requests that access this domain name use HTTPS requests. | `365*24*3600` is one year | +| includeSubdomains | boolean | Does this rule apply to all subdomains of this website | false | + + +### csp + +`Content-Security-Policy` of HTTP response header allows site managers to control which resources are loaded on a specified page. This will help prevent cross-site scripting attacks (XSS). + + +| Configuration Item | Type | Description of action | Default | +| --- | --- | --- | --- | +| enable | boolean | Whether to open | false | +| policy | `Record` | Policy list | `{}` | +| reportOnly | boolean | Whether to open | false | +| supportIE | boolean | Does IE browser support | false | + +For detailed `policy` configuration, please refer to: [What is the Content Security Policy (CSP)? Ali gathering is safe ](https://www.zhihu.com/question/21979782/answer/122682029) + + +### noopen + +It is used to specify that users of `IE 8` or higher can save files without opening files. The "Open" option is not explicitly displayed in the download dialog box. + +| Configuration Item | Type | Description of action | Default | +| --- | --- | --- | --- | +| enable | boolean | Whether to open | false | + + + + +### nosniff + +When turned on, if the `MIME` type of a file read from `script` or `stylesheet` does not match the specified `MIME` type, the file is not allowed to be read. It is used to prevent cross-site scripting attacks such as `XSS`. + +| Configuration Item | Type | Description of action | Default | +| --- | --- | --- | --- | +| enable | boolean | Whether to open | false | + + + + +### xssProtection + +Enable the XSS filtering feature of the browser to prevent cross-site scripting attacks by `XSS`. + +The `X-XSS-Protection` response header is a feature of `IE`, `Chrome` and `Safari`. When a cross-site scripting attack (XSS (en-US)) is detected, the browser will stop loading the page. If the website is set up with a good `Content-Security-Policy` to disable inline JavaScript ('unsafe-inline '), modern browsers do not need these protections, but it can still provide protection for users of older browsers that do not yet support `CSP`. + +`X-XSS-Protection` the following four values can be configured + ++ `0`: XSS filtering is prohibited. ++ `1`: Enable XSS filtering (usually the browser is the default). If a cross-site scripting attack is detected, the browser will clear the page (delete the unsafe part). ++ `1;mode = block`: enables XSS filtering. If an attack is detected, the browser will not clear the page, but will prevent the page from loading. ++ `1; report =`: Chromium only to enable XSS filtering. If a cross-site scripting attack is detected, the browser will clear the page and send a violation report using the function of the CSP report-uri (en-US) instruction. + +| Configuration Item | Type | Description of action | Default | +| --- | --- | --- | --- | +| enable | boolean | Whether to open | false | +| value | string | X-XSS-Protection configuration | `1; mode=block` | + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/sequelize.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/sequelize.md new file mode 100644 index 000000000000..a319c233aa8b --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/sequelize.md @@ -0,0 +1,775 @@ +# Sequelize + +This document describes how to use Sequelize in Midway. + +:::tip + +The current module has been reconfigured since v3.4.0, and the historical writing method is compatible. For more information about how to query historical documents, see [here](../legacy/sequelize). + +::: + +Related information: + +| Description | | +| ----------------- | --- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## The difference with the old writing + +If you want to use the new version of the usage, please refer to the following process to modify the old code. The new and old codes cannot be mixed. + +Upgrade method: + +- 1. Please explicitly add `sequelize` and `sequelize-typescript` to the business dependency +- 2. Instead of using the `BaseTable` decorator, use the `Table` decorator exported by the `sequelize-typescript` package directly. +- 3. configure the adjustment in the `sequelize` section of `src/config.default`. refer to the following data source configuration section + - The 3.1 is modified to the form of a data source to `sequelize.dataSource` + - 3.2 declare the entity model in the `entities` field of the data source + +## Installation dependency + +```bash +$ npm i @midwayjs/sequelize@3 sequelize sequelize-typescript --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/sequelize": "^3.0.0", + "sequelize": "^6.21.3 ", + "sequelize-typescript": "^ 2.1.0" + // ... + }, + "devDependencies": { + // ... + } +} +``` + +## Install database Driver + +The commonly used database drivers are as follows. Select the database type to install the corresponding connection: + +```bash +# for MySQL or MariaDB, you can also use mysql2 instead +npm install mysql --save +npm install mysql2 --save + +# for PostgreSQL or CockroachDB +npm install pg --save + +# for SQLite +npm install sqlite3 --save + +# for Microsoft SQL Server +npm install mssql --save + +# for SQL .js +npm install SQL .js --save + +# for Oracle +npm install oracledb --save + +# for MongoDB(experimental) +npm install mongodb --save +``` + +In the following example, `mysql2` is used as an example. + +### Directory structure + +A basic reference directory structure is as follows. + + +``` +MyProject +├── src +│ ├── config +│ │ └── config.default.ts +│ ├── entity +│ │ └── person.entity.ts +│ ├── configuration.ts +│ └── service +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + +## Enable components + +Enable components in the `src/configuration.ts` file. + +```typescript +import { Configuration, ILifeCycle } from '@midwayjs/core'; +import { join } from 'path'; +import * as sequelize from '@midwayjs/sequelize'; + +@Configuration({ + imports: [ + // ... + sequelize + ], + importConfigs: [join(__dirname, './config')] +}) +export class MainConfiguration implements ILifeCycle { + // ... +} +``` + +## Model definition + +### 1. Create Model(Entity) + +We associate with the database through the model. The model in the application is the database table. In the Sequelize, the model is bound to the entity. Each Entity file is a Model and an Entity. + +In the example, you need an entity. Let's take `person` as an example. Create an entity directory and add the entity file `person.entity.ts` to the entity directory. A simple entity is as follows. + +```typescript +// src/entity/person.entity.ts +import { Table, Model, Column, HasMany } from 'sequelize-typescript'; + +@Table +export class Hobby extends Model { + @Column + name: string; +} + +@Table +export class Person extends Model { + @Column + name: string; + + @Column + birthday: Date; + + @HasMany(() => Hobby) + hobbies: Hobby[]; +} +``` + +Note that each attribute of the entity file here is actually one-to-one corresponding to the database table. Based on the existing database table, we add content up. + +The `@Table` decorator can be used without passing any parameters. For more information, see [Define options](https://sequelize.org/v5/manual/models-definition.html#configuration). + +```typescript +@Table({ + timestamps: true + ... +}) +export class Person extends Model {} +``` + + + +### 2. Primary key + +The primary key (id) will be inherited from the base class Model. Generally speaking, the primary key is of Integer type and is self-increasing. + +There are two ways to set the primary key, `@Column({primaryKey: true})` or `@PrimaryKey`. + +For example: + +```typescript +import { Table, Model, PrimaryKey } from 'sequelize-typescript'; + +@Table +export class Person extends Model { + @PrimaryKey + name: string; +} +``` + +### 3. Time column + +Mainly refers to `@CreatedAt`, `@UpdatedAt`, `@DeletedAt` columns marked by a single decorator. + +for example: + +```typescript +import { Table, Model, CreatedAt, UpdatedAt, DeletedAt } from 'sequelize-typescript'; + +@Table +export class Person extends Model { + @CreatedAt + creationDate: Date; + + @UpdatedAt + updatedOn: Date; + + @DeletedAt + deletionDate: Date; +} +``` + +| Decorator | Description | +| ------------ | ----------------------------------------------------------------------- | +| `@CreatedAt` | `timestamps = true` and `createdAt = 'creationDate'` are set. | +| `@UpdatedAt` | `timestamps = true` and `updatedAt = 'updatedOn'` are set | +| `@DeletedAt` | `timestamps = true`, `paranoid = true`, and `deletedAt = 'deletionDate'` | + +### 4. Ordinary column + +The @Column decorator is used to label normal columns and can be used without passing any parameters. However, you must be able to automatically infer the js type. For more information, see [Type inference](https://github.com/sequelize/sequelize-typescript#type-inference). + +```typescript +import { Table, Model, Column } from 'sequelize-typescript'; + +@Table +export class Person extends Model { + @Column + name: string; +} +``` + +Or specify the column type. + +```typescript +import { Table, Column, DataType } from 'sequelize-typescript'; + +@Table +export class Person extends Model { + @Column(DataType.TEXT) + name: string; +} +``` + +For more information, see [here](https://sequelize.org/v5/manual/models-definition.html#configuration). + +For example: + +```typescript +import { Table, Model, Column, DataType } from 'sequelize-typescript' + +@Table +export class Person extends Model { + @Column({ + type: DataType.FLOAT + comment: 'Some value', + ... + }) + value: number; +} +``` + +| Decorator | Description | +| ------------------------------------ | ------------------------------------------------------------------------------------------------- | +| `@Column` | Use the derived [dataType](https://sequelize.org/v5/manual/models-definition.html#data-types) as the type | +| `@Column(dataType: DataType)` | Explicit setting [dataType](https://sequelize.org/v5/manual/models-definition.html#data-types) | +| `@Column(options: AttributeOptions)` | Set [attribute options](https://sequelize.org/v5/manual/models-definition.html#configuration) | + +## Data source configuration + +In the new version, we have enabled the [data source mechanism](../data_source) and configured it in `src/config.default.ts`: + +```typescript +// src/config/config.default.ts + +import { Person } from '../entity/person.entity'; + +export default { + // ... + sequelize: { + dataSource: { + // The first data source, the name of the data source can be completely customized + default: { + database: 'test4', + username: 'root', + password: '123456', + host: '127.0.0.1', + port: 3306, + encrypt: false, + dialect: 'mysql', + define: { charset: 'utf8' }, + timezone: '+08:00', + // Locally, you can createTable directly through sync: true + sync: false, + + // Object format + entities: [Person], + + // The following scanning form is supported. For compatibility, we can match both .js and .ts files at the same time + entities: [ + 'entity', // Specify the directory + '**/entity/*.entity.{j,t}s', // Wildcard with suffix matching + ], + }, + + // second data source + default2: { + // ... + }, + }, + }, +}; +``` + + +## Model association + +Relationships can be directly described in the model through `HasMany`, `@HasOne`, `@BelongsTo`, `@BelongsToMany`, and `@ForeignKey` decorators. + +### One-to-many + +```typescript +import { Table, Model, Column, ForeignKey, BelongsTo, HasMany } from 'sequelize-typescript'; + +@Table +export class Player extends Model { + @Column + name: string; + + @Column + num: number; + + @ForeignKey(() => Team) + @Column + teamId: number; + + @BelongsTo(() => Team) + team: Team; +} + +@Table +export class Team extends Model { + @Column + name: string; + + @HasMany(() => Player) + players: Player[]; +} +``` + +`sequelize-typescript` associates internally and automatically queries related dependencies. + +For example, you can use `find` to query. + +```typescript +const team = await Team.findOne({ include: [Player] }); + +team.players.forEach((player) => { + console.log('Player ${player.name}'); +}); +``` + +### Many-to-many + +```typescript +import { Table, Model, Column, ForeignKey, BelongsToMany } from 'sequelize-typescript'; + +@Table +export class Book extends Model { + @BelongsToMany(() => Author, () => BookAuthor) + } +} + +@Table +export class Author extends Model { + @BelongsToMany(() => Book, () => BookAuthor) + books: Book[]; +} + +@Table +export class BookAuthor extends Model { + @ForeignKey(() => Book) + @Column + bookId: number; + + @ForeignKey(() => Author) + @Column + authorId: number; +} +``` + +The above types are unsafe in some scenarios, such as the above `BookAuthor`, the `books` type of `Author`, which may lose some attributes and need to be set manually. + +```typescript +@BelongsToMany(() => Book, () => BookAuthor) +books: Array; +``` + +### One to one + +For one-to-one, use `@HasOne(...)` (the foreign key of the relationship exists on another model) and `@BelongsTo(...)` (the foreign key of the relationship exists on this model). + +For example: + +```typescript +import { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript'; +import { User } from './user.entity'; + +@Table +export class Photo extends Model { + @ForeignKey(() => User) + @Column({ + comment: 'User Id', + }) + userId: number; + + @BelongsTo(() => User) + user: User; + + @Column({ + Comment: 'name', + }) + name: string; +} + +@Table +export class User extends Model { + @Column + name: string; +} +``` + + + +### Model Cyclic Dependency + +If you use the `@BelongsTo` decorator, it is easy to trigger a model circular dependency error, such as: + +``` +ReferenceError: Cannot access 'Photo' before initialization +``` + +You can wrap types with `ReturnType`. + +```typescript +import { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript'; +import { User } from './user.entity'; + +@Table +export class Photo extends Model { + // ... + @BelongsTo(() => User) + user: ReturnType<() => User>; +} +``` + + + + + + + +## Static operation method + +If it is a single data source, you can use the following static method. + +### Save + +Where it needs to be called, use the entity model to operate. + +```typescript +import { Provide } from '@midwayjs/core'; +import { Person } from '../entity/person.entity'; + +@Provide() +export class PersonService { + async createPerson() { + const person = new Person({ name: 'bob', age: 99 }); + await person.save(); + } +} +``` + +### Find and update + +```typescript +import { Provide } from '@midwayjs/core'; +import { Person } from '../entity/person.entity'; + +@Provide() +export class PersonService { + async updatePerson() { + const person = await Person.findOne(); + // Update + person.age = 100; + await person.save(); + + await Person.update ( + { + name: 'bobby', + }, + { + where: { id: 1} + } + ); + } +} +``` + +## Repository Mode + +Repository mode can separate static operations such as lookup and creation from the model definition. It also supports use with multiple sequelize instances (multiple data sources). + +### Start Repository mode + +Same as data source configuration, except that there is one more attribute. + +```typescript +// src/config/config.default.ts + +import { Person } from '../entity/person.entity'; + +export default { + // ... + sequelize: { + dataSource: { + default: { + // ... + entities: [Person] + + // This one more + repositoryMode: true + }, + }, + sync: false + }, +}; +``` + +If there are multiple data sources, be sure to turn this property on each data source. After the property is turned on, the original static operation method is no longer available. + +You need to use the `Repository` operation method. + +### Use Repository mode + +The basic API is the same as the static operation. Midway has made some simple packages to it. The `InjectRepository` decorator can be used to inject `Repository` into the service. + +```typescript +import { Controller, Get } from '@midwayjs/core'; +import { InjectRepository } from '@midwayjs/sequelize'; +import { Photo } from '../entity/photo.entity'; +import { User } from '../entity/user.entity'; +import { Op } from 'sequelize'; +import { Repository } from 'sequelize-typescript'; + +@Controller('/') +export class HomeController { + @InjectRepository(User) + userRepository: Repository; + + @InjectRepository(Photo) + photoRepository: Repository; + + @Get('/') + async home() { + // Query + let result = await this.photoRepository.findAll(); + console.log(result); + + // New + await this.photoRepository.create({ + name: '123', + }); + + // Delete + await this.photoRepository.destroy({ + where: { + name: '123', + }, + }); + + // Joint query + // SELECT * FROM photo WHERE name = "23" OR name = "34"; + let result = await this.photoRepository.findAll({ + where: { + [Op.or]: [{ name: '23' }, { name: '34' }] + }, + }); + // => result + + // even table query + let result = await this.userRepository.findAll({ include: [Photo] }); + // => result + } +} +``` + +More ways to use OP: [https:// sequelize.org/v5/manual/querying.html](https://sequelize.org/v5/manual/querying.html) + +### Multi-dataSource support + +In Repository mode, we can specify a specific data source in the `InjectRepository` parameters. + +```typescript +import { Controller } from '@midwayjs/core'; +import { InjectRepository } from '@midwayjs/sequelize'; +import { Photo } from '../entity/photo.entity'; +import { User } from '../entity/user.entity'; +import { Repository } from 'sequelize-typescript'; + +@Controller('/') +export class HomeController { + // Specify a data source + @InjectRepository(User, 'default') + userRepository: Repository; + // ... +} +``` + + + +## Advanced Features + +### Data source synchronization configuration + +sequelize can add the sync parameter when synchronizing the data source. + +```typescript +export default { + // ... + sequelize: { + dataSource: { + default: { + sync: true, + syncOptions: { + force: false, + alter: true, + }, + }, + }, + // You can use this to specify the default data source when there are multiple data sources + defaultDataSourceName: 'default', + }, +}; +``` + + + +### Specify the default data source + +When including multiple data sources, you can specify a default data source. + +```typescript +export default { + // ... + sequelize: { + dataSource: { + default1: { + // ... + }, + default2: { + // ... + }, + }, + // You can use this to specify the default data source when there are multiple data sources + defaultDataSourceName: 'default1', + }, +}; +``` + + + +### Get data source + +The data source is the created sequelize object, which we can obtain by injecting the built-in data source manager. + +```typescript +import { Configuration } from '@midwayjs/core'; +import { SequelizeDataSourceManager } from '@midwayjs/sequelize'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + + async onReady(container: IMidwayContainer) { + const dataSourceManager = await container.getAsync(SequelizeDataSourceManager); + const conn = dataSourceManager.getDataSource('default'); + await conn.authenticate(); + } +} +``` + +Starting with v3.8.0, it is also possible to inject via a decorator. + +```typescript +import { Configuration } from '@midwayjs/core'; +import { InjectDataSource } from '@midwayjs/sequelize'; +import { Sequelize } from 'sequelize-typescript'; + +@Configuration({ + //... +}) +export class MainConfiguration { + + // Inject the default data source + @InjectDataSource() + defaultDataSource: Sequelize; + + // inject custom data source + @InjectDataSource('default1') + customDataSource: Sequelize; + + async onReady(container: IMidwayContainer) { + //... + } +} +``` + +## Common problem + + + +### 1. Dialect needs to be explicitly supplied as of v4.0.0 + +The reason is that the data source in the configuration does not specify the `dialect` field, which confirms the structure, format of the data source and the result of the configuration merging. + + + +### 2. Generate entity columns + +Please refer to the modules provided by the community, such as [sequelize-typescript-generator](https://github.com/spinlud/sequelize-typescript-generator) + + + +### 3. Raw Query + +If you encounter something more complex, you can use the [raw query method](https://sequelize.org/v5/manual/raw-queries.html) + + + +### 4. TS2612 error + +If your model reports a TS2612 error, such as: + +``` +src/entity/AesTenantConfigInfo.ts:29:6 - error TS2612: Property 'id' will overwrite the base property in 'Model'. If this is intentional, add an initializer. Otherwise, add a 'declare' modifier or remove the redundant declaration. + +29 id?: number; + ~~ +``` + +It can be assigned a null value. + +```typescript +import { Table, Column } from 'sequelize-typescript'; + +@Table +export class User extends Model { + @Column({ + primaryKey: true, + autoIncrement: true, + type: DataType.BIGINT, + }) + id?: number = undefined; +} +``` + + + +## Other + +- The above document is translated from sequelize-typescript. For more API, please refer to the [English document](<(https://github.com/sequelize/sequelize-typescrip)>). +- Some [cases](https://github.com/ddzyan/midway-practice) diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/socketio.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/socketio.md new file mode 100644 index 000000000000..421bb038af05 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/socketio.md @@ -0,0 +1,1095 @@ +# SocketIO + +Socket.io is a common library in the industry, which can be used for real-time, two-way and event-based communication between browsers and servers. + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01YTye6U22gICvarVur_!!6000000007149-2-tps-1204-352.png) + + +Midway provides support and encapsulation for Socket.io, which can simply create a Socket.io service. This article demonstrates how to provide Socket.io service under Midway system. + +Midway uses the latest [Socket.io (v4.0.0)](https://socket.io/docs/v4) for development. + + + +Related information: + +**Provide services** + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ❌ | +| Can be used for integration | ✅ | +| Contains independent main framework | ✅ | +| Contains independent logs | ❌ | + + + +## Install dependency + + +Install Socket.io dependencies in existing projects. +```bash +$ npm i @midwayjs/socketio@3 --save +## optional dependencies +$ npm i @types/socket.io-client socket.io-client --save-dev +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/socket.io": "^3.0.0", + // Client optional + "socket.io-client": "^4.4.1 ", + // ... + }, + "devDependencies": { + // Client optional + "@types/socket.io-client": "^1.4.36 ", + // ... + } +} +``` + + + +## Open the component + +`@midwayjs/socket.io` can be used as an independent main framework. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as socketio from '@midwayjs/socketio'; + +@Configuration({ + imports: [socketio] + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + +``` + +It can also be attached to other main frameworks, such as `@midwayjs/koa`. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as socketio from '@midwayjs/socketio'; + +@Configuration({ + imports: [koa, socketio] + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + + +``` + + + + +## Directory structure + + +The following is the basic directory structure of the Socket.io project. Similar to traditional applications, we have created a `socket` directory to store service codes for Soscket.io services. +``` +. +├── package.json +├── src +│ ├── configuration.ts ## entry configuration file +│ ├── interface.ts +│ └── socket ## socket.io service file +│ └── hello.controller.ts +├── test +├── bootstrap.js ## service startup Portal +└── tsconfig.json +``` + + +## Socket.io works + + +The two-way channel between the Socket.io server and the Socket.io client (browser, Node.js or another programming language) is established through a [WebSocket connection](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). When it is not available, HTTP long polling will be used as a backup means. + + +The Socket.io code is built based on the Engine.io library and belongs to the upper-level implementation of Engine.io. Engine.io is responsible for the connection between the entire server and the client, including connection check, transmission method, etc. Socket.io is responsible for the reconnection, packet buffering, broadcasting and other features of the upper layer. + + +Socket.io(Engine.io) implements two Transports (transmission mode). + + +The first is HTTP long polling. HTTP Get requests are used for long-running (long connection) and Post requests are used for short-running (short connection). + + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01xhdZHA1XTEpUue7CQ_!!6000000002924-2-tps-1778-1068.png) + +The second is the WebSocket protocol, which is directly based on [WebSocket Connection](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) implementation. It provides a two-way and low-latency communication channel between the server and the client. + + +By default, Socket.io will first use HTTP long polling to connect and send a data similar to the following structure. +```typescript +{ + "sid": "FSDjX-WRwSA4zTZMALqx", // session id of connection + "upgrades": ["WebSocket"], // Upgradeable protocol + "pingInterval": 2500, //heartbeat interval + "pingTimeout": 20000 // heartbeat timeout +} +``` + +When the current service meets the requirements of upgrading to the WebSocket protocol, it will automatically upgrade to the WebSocket protocol, as shown in the following figure. +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01QHZi9x1mz2ZLecco3_!!6000000005024-2-tps-585-216.png) + +- 1, the first handshake, transmission sid and other structures +- 2. Send data using HTTP long polling +- 3. Use HTTP long polling to return data +- 4. Upgrade the protocol and use the WebSocket protocol to send data +- 5. When the protocol is upgraded, close the previous long polling + + + +After that, normal WebSocket communication began. + + +## Socket service + + +Midway defines the Socket service through the `@WSController` decorator. +```typescript +@WSController('/') +export class HelloController { + // ... +} +``` +The input of `@WSController` refers to the Namespace (non-path) of each Socket. If no Namespace is provided, each Socket.io will automatically create a`/`Namespace and put all client connections into it. + +:::info +The namespace here supports strings and regularization. +::: + + +When the Namespace has a client connection, a `connection` event will be triggered. We can use the `@OnWSConnection()` decorator in the code to decorate a method. When each client connects to the Namespace for the first time, the method will be automatically called. +```typescript +import { WSController, OnWSConnection, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/socketio'; + +@WSController('/') +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSConnection() + async onConnectionMethod() { + console.log('on client connect', this.ctx.id); + } +} + +``` + + +:::info +The ctx here is equivalent to the socket instance. +::: + + +## Messages and responses + + +Socket.io obtains data by monitoring events. Midway provides a `@OnWSMessage()` decorator to format the received event. Every time the client sends an event, the modified method will be executed. +```typescript +import { WSController, Provide, OnWSMessage, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/socketio'; + +@WSController('/') +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSMessage('myEvent') + async gotMessage(data) { + console.log('on data got', this.ctx.id, data); + } +} + +``` +Note that since Socket.io can pass multiple data in one event, the parameters here can be multiple. +```typescript + @OnWSMessage('myEvent') + async gotMessage(data1, data2, data3) { + // ... + } +``` +After the data is obtained, the data is processed through business logic, and then the result is returned to the client. When returned, we also send it to the client through another event. + + +The `@WSEmit` decorator returns the return value of the method to the client. +```typescript +import { WSController, OnWSConnection, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/socketio'; + +@WSController('/') +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSMessage('myEvent') + @WSEmit('myEventResult') + async gotMessage() { + return 'hello world'; // The hello world string is returned to the client here. + } +} +``` +The above code, our method returns the value Hello World, which will be automatically sent to the `myEventResult` event monitored by the client. + + + +## Socket Middleware + +The middleware in Socket is written similarly to the [Web middleware](../middleware), but the timing of loading is slightly different. + +Since Socket has two stages of connecting and receiving messages, middleware is divided into several categories. + +- The global Connection middleware will take effect on connections under all namespaces +- The global Message middleware will take effect for all message under the namespace. +- Controller middleware will take effect on connection and message under a single namespace. +- Connection middleware generates messages for connection under a single namespace. +- Message middleware will take effect on message under a single namespace. + +### Middleware writing + +Note that the middleware must return the result via `return`. + +```typescript +// src/middleware/socket.middleware.ts +import { Middleware } from '@midwayjs/core'; +import { Context, NextFunction } from '@midwayjs/socketio'; + +@Middleware() +export class SocketMiddleware { + resolve() { + return async (ctx: Context, next: NextFunction) => { + // ... + return await next(); + } + } +} + +``` + + + +### Global middleware + +Similar to web middleware, use the `socket.io` app instance to register middleware. + +```typescript +import * as socketio from '@midwayjs/socketio'; + +@Configuration({ + imports: [ + socketio + ], + // ... +}) +export class MainConfiguration { + + @App('socketIO') + app: Application; + + async onReady() { + // Can register global connection middleware + this.app.useConnectionMiddleware(SocketMiddleware); + // You can also register global Message middleware + this.app.useMiddleware(SocketMiddleware); + } +} + +``` + + + +### Middleware in Namespace + +Through the decorator, the middleware of different stages is registered. + +For example, Namespace-level middleware will take effect on connection and message under a single namespace. + +```typescript +// ... + +// Namespace-level middleware +@WSController('/api', { middleware: [SocketMiddleware]}) +export class APIController { +} + +``` + +Connection middleware, which takes effect when connected. + +```typescript +// ... + +@WSController('/api') +export class APIController { + + // Middleware when Connection is triggered + @OnWSConnection({ + middleware: [SocketMiddleware] + }) + init() { + // ... + } +} +``` + +Message middleware that takes effect when a specific message is received. + +```typescript +// ... + +@WSController('/api') +export class APIController { + + // Middleware when Message is triggered + @OnWSMessage('my', { + middleware: [SocketMiddleware] + }) + @WSEmit('ok') + async gotMyMessage() { + // ... + } +} +``` + + + +## Local test + +Because the socket.io framework can be started independently (attached to the default http service or with other midway frameworks). + +When starting as a standalone framework, you need to specify a port. + +```typescript +// src/config/config.default +export default { + // ... + socketIO: { + port: 3000 + }, +} +``` + +When starting as a sub-framework (for example, and http, because http does not specify a port during a single test (automatically generated using supertest), it cannot be tested well, and only one port can be explicitly specified in the test environment. + +```typescript +// src/config/config.unittest +export default { + // ... + koa: { + port: null, + }, + socketIO + port: 3000 + }, +} +``` + +:::tip + +- 1. The port here is only the port that the WebSocket service starts during testing. +- 2. The port in koa is null, which means that the http service will not be started without configuring the port in the test environment. + +::: + + +Like other Midway testing methods, we use `createApp` to start the project. + + +```typescript +import { createApp, close } from '@midwayjs/mock' +// The Framework definition used here is subject to the main framework. +import { Framework } from '@midwayjs/koa'; + +describe('/test/index.test.ts', () => { + it('should create app and test socket.io', async () => { + const app = await createApp(); + + //... + + await close(app); + }); + +}); +``` + + +You can use `socket.io-client` to test. You can also use a test client that is encapsulated by the `socket.io-client` module provided by Midway. + + +If our server processing logic is as follows (returns the result of adding the data passed by the client): +```typescript +@OnWSMessage('myEvent') +@WSEmit('myEventResult') +async gotMessage(data1, data2, data3) { + return { + name: 'harry', + result: data1 + data2 + data3 + }; +} +``` + + +The test code is as follows: +```typescript +import { createApp, close } from '@midwayjs/mock' +import { Framework } from '@midwayjs/koa'; +import { createSocketIOClient } from '@midwayjs/mock'; +import { once } from 'events'; + +describe('/test/index.test.ts', () => { + it('should test create socket app', async () => { + + // Create a service + const app = await createApp(); + + // Create a corresponding client + const client = await createSocketIOClient({ + port: 3000 + }); + + // Return the result + const data = await new Promise(resolve => { + client.on('myEventResult', resolve); + // Send event + client.send('myEvent', 1, 2, 3); + }); + + // Judgment result + expect(data).toEqual({ + name: 'harry', + result: 6 + }); + + // Close the client + await client.close(); + // Close the server + await close(app); + }); + +}); +``` +If you have multiple clients, you can use the `once` method of the `events` module that comes with the node to optimize the code. +```typescript +import { createApp, close } from '@midwayjs/mock' +import { Framework } from '@midwayjs/koa'; +import { createSocketIOClient } from '@midwayjs/mock'; +import { once } from 'events'; + +describe('/test/index.test.ts', () => { + + it('should test create socket app', async () => { + + // Create a service + const app = await createApp(); + + // create a client + const client = await createSocketIOClient({ + port: 3000 + }); + + // Monitor with promise writing of events + const gotEvent = once(client, 'myEventResult'); + // Send event + client.send('myEvent', 1, 2, 3); + // Waiting for return + const [data] = await gotEvent; + // Judgment result + expect(data).toEqual({ + name: 'harry', + result: 6 + }); + + // Close the client + await client.close(); + // Close the server + await close(app); + }); + +} +``` +The two writing methods have the same effect, just write as you understand. + + +## Message waiting for receipt (ack) + + +Socket.io supports a way of writing directly returning messages. When the client delivers a message, if the last parameter is a function(callback), the server can get the callback and return the data directly to the client without creating a new message. + + +Our service code does not need to be changed. `@midwayjs/socketio` will judge the last parameter and automatically return it to the client. + + +For example, server code: +```typescript +@OnWSMessage('myEvent') +@WSEmit('myEventResult') +async gotMessage(data1, data2, data3) { + return { + name: 'harry', + result: data1 + data2 + data3 + }; +} +``` +Client test code: +```typescript +import { createApp, close } from '@midwayjs/mock' +import { Framework } from '@midwayjs/koa'; +import { createSocketIOClient } from '@midwayjs/mock'; +import { once } from 'events'; + +describe('/test/index.test.ts', () => { + + it('should test create socket app', async () => { + + // Create a service + const app = await createApp(); + + // Create a corresponding client + const client = await createSocketIOClient({ + port: 3000 + }); + + // Send event, which is written in await + const data = await client.sendWithAck('myEvent', 1, 2, 3); + + // Judgment result + expect(data).toEqual({ + name: 'harry', + result: 6 + }); + + // Close the client + await client.close(); + // Close the server + await close(app); + }); + +}); +``` + + + +## Common messages and broadcasts + + +The following code example: + + +```typescript +import { Context, Application } from '@midwayjs/socketio'; +import { WSController, OnWSMessage, WSEmit, App, Inject } from '@midwayjs/core'; + +@WSController('/') +export class HelloSocketController { + + @Inject() + ctx: Context; + + @App('socketIO') + app: Application; + + @OnWSMessage('myEvent') + @WSEmit('myEventResult') + async gotMessage() { + // TODO + } +} +``` + + +Send to the client (or return directly in the form of a decorator). +```typescript +this.ctx.emit("hello", "can you hear me?", 1, 2, "abc"); +``` +Sent to all clients except the sender. +```typescript +this.ctx.broadcast.emit("broadcast", "hello friends! "); +``` +Send it to all clients in the `game` room except the sender. +```typescript +this.ctx.to("game").emit("nice game", "let's play a game"); +``` +It is sent to all clients in `game1` and `game2` rooms except the sender. +```typescript +this.ctx.to("game1").to("game2").emit("nice game", "let's play a game (too)"); +``` +It is sent to all clients in the `game` room, including the sender. +```typescript +this.app.in("game").emit("big-announcement", "the game will start soon"); +``` +Broadcast to clients `myNamespace` namespaces, including senders. +```typescript +// Send from app +this.app.of("myNamespace").emit("bigger-announcement", "the tournament will start soon"); +// Send from ctx +this.ctx.nsp.emit("bigger-announcement", "the tournament will start soon"); +``` +Send to specific namespace and room, including sender. +```typescript +// Send from app +this.app.of("myNamespace").to("room").emit("event", "message"); +// Send from ctx +this.ctx.nsp.emit("bigger-announcement", "the tournament will start soon"); +``` +Send to all clients connected to the current node (when there are multiple nodes, it is multi-process) +```typescript +this.app.local.emit("hi", "my lovely babies"); +``` + +## Application(io object) + + +The code created by the traditional Socket.io server is as follows: + +```typescript +const io = require("socket.io")(3000); + +io.on("connection", socket => { + // ... +}); +``` + +in the `@midwayjs/socketio` framework, the Application instance is the io instance with the same type and capability. The app instance injected by the `@App` decorator is an io object. + + +We can do some global things through this object. + + +For example, get all socket instances. + +```typescript +// Returns all socket instances +const sockets = await app.fetchSockets(); + +// Returns all socket instances in room1. +const sockets = await app.in("room1").fetchSockets(); + +// Returns an instance of a specific socketId +const sockets = await app.in(theSocketId).fetchSockets(); +``` + +Under multiple frameworks, the main framework is generally a Web framework. We can obtain the app of Socket.io by specifying the key. + +```typescript +import { Application as SocketApplication } from '@midwayjs/socketio'; +import { Controller, App } from '@midwayjs/core'; + +@Controller() +export class UserController { + + @App('socketIO') + socketApp: SocketApplication; +} +``` + + +In this way, we can call the existing socket connection through the app object of `@midwayjs/socketio` (equivalent to io). + + +For example, an HTTP request is called to broadcast to all clients under a specific namespace: + +```typescript +import { Application as SocketApplication } from '@midwayjs/socketio'; +import { Provide, Controller, App, Get } from '@midwayjs/core'; + +@Controller() +export class UserController { + + @App('socketIO') + socketApp: SocketApplication; + + @Get() + async invoke() { + // Broadcast the connection under/ + this.socketApp.of('/').emit('hi', 'everyone'); + } +} +``` + +For more io API, please refer to the [Socket.io Server instance documentation](https://socket.io/docs/v4/server-instance/). + + + +## Socket deployment + +### Socket service port + +The configuration sample of `@midwayjs/socketio` is as follows: + +```typescript +// src/config/config.default +export default { + // ... + socketIO: { + port: 7001 + }, +} +``` + +HTTP ports can be reused when `@midwayjs/socketio` and other `@midwayjs/Web`, `@midwayjs/Koa`, `@midwayjs/express` are enabled at the same time. + +```typescript +// src/config/config.default +export default { + // ... + koa: { + port: 7001 + }, + socketIO: { + // No configuration here + }, +} +``` + + + +### Nginx configuration + +Generally speaking, our Node.js service will have similar reverse proxy services such as Nginx before. Here, take the configuration of Nginx as an example. + +```nginx +http { + server { + listen 80; + server_name example.com; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + + proxy_pass http://localhost:7001; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + } +} +``` + + + +## Configuration + +### Available configuration + +| Property | Type | Description | +| --- | --- | --- | +| port | number | Optionally, if the port is passed, socket.io will create an HTTP service for the port and attach the socket service to it. If you want to work with other midway web frameworks, do not pass this parameter. | +| path | string | optional, server path | +| adapter | object | Adapters for distributed processing, such as configurable redis-adapter | +| connectTimeout | number | Client timeout, in MS, default value _45000_ | + +For more information about startup options, see [Socket.io documentation](https://socket.io/docs/v4/server-api/#new-Server-httpServer-options). + + + +## Adapter + +Adapter is an adaptation layer for Socket.io to communicate with multiple machines and processes during distributed deployment. Currently, there are several adapters officially provided by socket.io: + + + +- 1. cluster-adapter is used to adapt between single machine and multi-process +- 2. The redis-adapter is used to adapt between multiple machines and processes. + + + +In distributed scenarios, we generally use redis-adapater to implement functions. + + + + +### Configure redis adapter + +`@midwayjs/socketio` provides an adapter (adapter) entry configuration. You only need to initialize the adapter instance and pass it in. + +:::tip + +Socket.io has updated the original adapter package name. The current package name is `@socket.io/redis-adapter` (originally called `socket.io-redis` ). For more information about migration, see the [official documentation](https://github.com/socketio/socket.io-redis-adapter#migrating-from-socketio-redis). + +::: + +The installation is as follows: + +```bash +$ npm i @socket.io/redis-adapter --save +``` + + + +For more information, see [Official documentation](https://github.com/socketio/socket.io-redis-adapter): + +```typescript +// src/config/config.default +import { createAdapter } from '@socket.io/redis-adapter'; +import Redis from 'ioredis'; + +// github document creates a redis instance + +const pubClient = new redis (/* redis configuration */); +const subClient = pubClient.duplicate(); + +export default { + // ... + socketIO: { + adapter: createAdapter(pubClient, subClient) + }, +} +``` + +By running Socket.io using the `@socket.io/redis-adapter` adapter, you can run multiple Socket.io instances in different processes or servers, all of which can broadcast and send events to each other. + +In addition, there are some special APIs on the Adapter, which can be viewed in [documents](https://github.com/socketio/socket.io-redis-adapter#api). + +## Sticky session + +Since Node.js often uses multi-process (cluster) mode at startup, if the same session (sid) cannot access the same process multiple times, socket.io will report an error. + +There are two solutions. + + + +### Use the WebSocket protocol + +The easiest way is to only enable the WebSocket protocol (disable long polling), so that the above problems can be circumvented. + +You need to configure both server and client. + +```typescript +// Server +export default { + //... + socketIO: { + //... + transports: ['websocket'], + }, +} + +// client +const socket = io("http://127.0.0.1:7001", { + transports: ['websocket'] +}); +``` + + + +### Adjust the process model + +This is a relatively complicated method, but in the scenario of pm2 deployment, it is the only solution to support both sticky sessions and polling support. + +The first step is to disable the ports enabled in the configuration, such as: + +```typescript +// src/config/config.default +export default { + koa: { + // port: 7001, + }, + socketIO: { + //... + }, +}; + +``` + +If development needs, you can add the port in `config.local`, or directly add the port in the scripts of `package.json`. + +```json +"scripts": { + "dev": "cross-env NODE_ENV=local midway-bin dev --ts --port=7001", +}, +``` + + + +In the second step, adjust the content of your `bootstrap.js` file to the following code. + +```typescript +const { Bootstrap, ClusterManager, setupStickyMaster } = require('@midwayjs/bootstrap'); +const http = require('http'); + +// Create a process manager to handle child processes +const clusterManager = new ClusterManager({ + exec: __filename, + count: 4, + sticky: true, // enable sticky session support +}); + +if (clusterManager. isPrimary()) { + // The main process starts an http server to monitor + const httpServer = http. createServer(); + setupStickyMaster(httpServer); + + // start child process + clusterManager.start().then(() => { + // listening port + httpServer.listen(7001); + console.log('main process is ok'); + }); + + clusterManager.onStop(async () => { + // close http server when stopped + await httpServer. close(); + }); +} else { + // subprocess logic + Bootstrap + .run() + .then(() => { + console.log('child is ready'); + }); +} + +``` + +When pm2 starts, there is no need to specify the `-i` parameter to start the worker, directly `pm2 --name=xxx ./bootstrap.js` to make it start only one process. + + + + +## Common API + + +### Get the number of connections +```typescript +Const count = app.engine.clientsCount; //Get all connections +Const count = app.of('/').sockets.size; // Get the number of connections in a single namespace +``` + + +### Modify sid generation +```typescript +const uuid = require("uuid"); + +app.engine.generateId = (req) => { + return uuid.v4(); // must be unique across all Socket.IO servers +} +``` + + + +## Frequently Asked Questions + + +### The server/client is not connected and does not respond + + +1. The port server is consistent with the client + + +```typescript +export default { + koa: { + Port: 7001, // Port here + } +} + +// or + +export default { + socketIO: { + Port: 7001, // Port here + } +} +``` + + +Consistent with the following ports. +```typescript +// socket.io client +const socket = io('************:7001', { + //... +}); + +// midway's socket.io test client +const client = await createSocketIOClient({ + port: 7001 +}); +``` + +2. The path of the server and the path of the client must be consistent. Path refers to the part of the startup parameter. + +```typescript +// config.default +export default { + socketIO: { + Path: '/testPath' // This is the server path + } +} +``` +Consistent with the path below + +```typescript +// socket.io client +const socket = io('************:7001', { + Path: '/testPath' // here is the path of the client +}); + +// Midway's Socket. io test client +const client = await createSocketIOClient({ + path: '/testPath' +}); +``` + + + +3. The namespace of the server and the namespace of the client should be consistent. + +```typescript +// server +@WSController('/test') // here is the namespace of the server +export class HelloController { +} + +// socket.io client +const io = require("socket.io-client") +io('*****:3000/test', {}); // Here is the namespace of the client + + +// midway's socket.io test client +const client = await createSocketIOClient({ + namespace: '/test', +}); +``` + + + +### Configure CORS + + +If a cross-domain error occurs, cors information needs to be configured at startup. +```typescript +// config.default +export default { + socketIO: { + cors: { + origin: "http://localhost:8080 ", + methods: ["GET", "POST"] + } + } +} +``` +For specific parameters, see [Socket.io Handling CORS](https://socket.io/docs/v4/handling-cors/). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/static_file.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/static_file.md new file mode 100644 index 000000000000..0780a58f4ab8 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/static_file.md @@ -0,0 +1,224 @@ +# Static file hosting + +midway provides static resource hosting components based on the [koa-static-cache](https://github.com/koajs/static-cache) module. + +Related information: + +| Web support | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | 💬 | +| @midwayjs/web | ✅ | +| @midwayjs/express | ❌ | + +:::caution + +💬 Some function computing platforms do not support streaming request responses. Please refer to the corresponding platform capabilities. + +::: + +## Installation dependency + +```bash +$ npm i @midwayjs/static-file@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/static-file": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## Introducing components + + +First, introduce components and import them in `configuration.ts`: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as staticFile from '@midwayjs/static-file'; +import { join } from 'path' + +@Configuration({ + imports: [ + koa + staticFile + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + + +## Use + +By default, the `public` directory in the root directory of the project is hosted. + +For example: + +``` +➜ my_midway_app tree +. +├── src +├── public +| ├── index.html +│ └── hello.js +│ +├── test +├── package.json +└── tsconfig.json +``` + +You can directly use the path to access `GET /public/index.html` and obtain the corresponding results. + + + +## Configuration + +### Modify default behavior + +The hosting of the resource uses the `dirs` field, which has a `default` attribute that we can modify. + +```typescript +// {app_root}/src/config/config.default.ts +export default { + // ... + staticFile: { + dirs: { + default: { + prefix: '/', + dir: 'xxx', + }, + } + }, +} +``` + +The value of the object in `dirs` is merged with the value under the `staticFile` and passed into the `koa-static-cache` middleware. + +### Add a new directory + +You can modify dirs and add a new directory. the key is not repeated, and the value is merged with the default configuration. + +```typescript +// {app_root}/src/config/config.default.ts +export default { + // ... + staticFile: { + dirs: { + default: { + prefix: '/', + dir: 'xxx', + }, + another: { + prefix: '/', + dir: 'xxx', + }, + } + // ... + }, +} +``` + + + +### Available configuration + +All [koa-static-cache](https://github.com/koajs/static-cache) configurations are supported. The default configuration is as follows: + +| Property | Default | Description | +| ------- |---------------------------------------------------| ------------------------------------------------------------ | +| dirs | \{"default": \{prefix: "/public", "dir": "xxxx"}} | Managed directories, in order to support multiple directories, are objects.
In addition to the default, other keys can be added at will, and the object values in dirs will be merged with the external default values | +| dynamic | true | Load files dynamically instead of caching after initialization reading | +| preload | false | Whether the cache is being initialized | +| maxAge | prod is 31536000, others are 0 | Maximum cache time | +| buffer | prod is true and the rest is false | Use buffer character to return | + +For more configuration, please refer to [koa-static-cache](https://github.com/koajs/static-cache) . + + + +## Frequently Asked Questions + +### 1. The route under the function does not take effect + +Function routes need to be explicitly configured to take effect. Generally, a wildwith route is added for static files, such as `/*` or `/public/*`. + +```typescript +import { + Provide, + ServerlessTrigger, + ServerlessTriggerType +} from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloHTTPService { + + @ServerlessTrigger(ServerlessTriggerType.HTTP, { + path: '/public/*', + method: 'get', + }) + async handleStaticFile() { + // This function can have no method body, just to let the gateway register an additional route + } +} + +``` + + + +### 2. Default index.html + +Since [koa-static-cache](https://github.com/koajs/static-cache) does not support the default `index.html` configuration, it can be solved by its alias function. + +You can configure `/` to point to `/index.html`. Wildcards and regular expressions are not supported. + +```typescript +export default { + // ... + staticFile: { + dirs: { + default: { + prefix: '/', + alias: { + '/': '/index.html', + }, + }, + }, + // ... + }, +} +``` + + + +### 3. When egg (@midwayjs/web) does not take effect + +Since egg comes with a static hosting plug-in, if the static plug-in is enabled, it will conflict with this component. + +If you want to use this component, be sure to close the egg plug-in. + +```typescript +// src/config/plugin.ts +import { EggPlugin } from 'egg'; +export default { + // ... + static: false, +} as EggPlugin; +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/swagger.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/swagger.md new file mode 100644 index 000000000000..55292e97c25b --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/swagger.md @@ -0,0 +1,1302 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Swagger +Based on the latest [OpenAPI 3.0.3](https://swagger.io/specification/), the new version of Swagger components is implemented. + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ❌ | +| Can be used for integration | ❌ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## Installation dependency + +```bash +$ npm install @midwayjs/swagger@3 --save +$ npm install swagger-ui-dist --save-dev +``` + +If you want to output Swagger API pages on the server, you need to install the swagger-ui-dist into the dependency. + +```bash +$ npm install swagger-ui-dist --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/swagger": "^3.0.0", + // If you want to use it on the server + "swagger-ui-dist": "4.2.1", + // ... + }, + "devDependencies": { + // If you don't want to use it on the server + "swagger-ui-dist": "4.2.1", + // ... + } +} +``` + + + +## Open the component + +Add components to ```configuration.ts```. + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as swagger from '@midwayjs/swagger'; + +@Configuration({ + imports: [ + // ... + swagger + ] +}) +export class MainConfiguration { + +} +``` + +You can configure the enabled environment, for example, the following code refers to "only enabled in local environment". + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as swagger from '@midwayjs/swagger'; + +@Configuration({ + imports: [ + // ... + { + component: swagger + enabledEnvironment: ['local'] + } + ] +}) +export class MainConfiguration { + +} +``` + +Then start the project and access the address: + +- UI: http://127.0.0.1:7001/swagger-ui/index.html +- JSON: http://127.0.0.1:7001/swagger-ui/index.json + +The path can be configured by `swaggerPath` parameters. + + + +## Data type + +### Automatic type extraction + +The Swagger component identifies the `@Body()`, `@Query()`, `@Param()` decorators for each routing method in each `@Controller` and extracts routing method parameters and types. + +For example, the following code: + +```typescript +@Get('/') +async home ( + @Query('uid') uid: number + @Query('tid') tid: string + @Query('isBoolean') isBoolean: boolean +) { + // ... +} +``` + +The basic Boolean, string, and numeric types are displayed as follows: + +![](https://img.alicdn.com/imgextra/i2/O1CN01KGk0B325xe6cV5HCo_!!6000000007593-2-tps-1110-854.png) + + + +### Types and schema + +We often use objects in parameters and use defined classes as types. At this time, swagger components can also be automatically identified, and can also be combined with common types for identification. + +For example, the following code: + +```typescript +@Post('/:id', { summary: 'test'}) +async create(@Body() createCatDto: CreateCatDto, @Param('id') id: number) { + // ... +} +``` + +The definition of `CreateCatDto` type is as follows. We use `ApiProperty` to define each attribute. + +```typescript +import { ApiProperty } from "@midwayjs/swagger"; + +export class CreateCatDto { + @ApiProperty({ example: 'Kitty', description: 'The name of the Catname'}) + name: string; + + @ApiProperty({ example: '1', description: 'The name of the Catage'}) + age: number; + + @ApiProperty({ example: 'bbbb', description: 'The name of the Catbreed'}) + breed: string; +} +``` + +The effect is as follows. The component will automatically extract two of the parameters: + +![swagger1](https://img.alicdn.com/imgextra/i2/O1CN01qpyb7k1uheVEFq8CI_!!6000000006069-2-tps-1220-1046.png) + +At the same time, since the example of each attribute is defined in the class, the sample value is automatically filled in. + +In Swagger, each type will be described by a `Schema`. We have already defined a `CreateCatDto` Schema, which looks like the following. + +Note that we will reuse these Schemas. + +![swagger2](https://img.alicdn.com/imgextra/i2/O1CN01iZYONb1tAqW35GM3C_!!6000000005862-2-tps-1050-694.png) + + + +### Base type + +We can define common types by setting type in the `@ApiProperty(...)` decorator. + +In most cases, the underlying type can be automatically identified without explicitly declaring the `type`. + +**String** + +```typescript +@ApiProperty({ + type: 'string', + // ... +}) +name: string; +``` + +**Boolean** + +```typescript +@ApiProperty({ + type: 'boolean', + example: 'true', + // ... +}) +isPure: boolean; +``` + +**Number Type** + +```typescript +@ApiProperty({ + type: 'number', + example: '1', + description: 'The name of the Catage' +}) +age: number; +``` + +In addition, you can also use the format field to define a more precise length. + +```typescript +@ApiProperty({ + type: 'integer', + format: 'int32', + example: '1', + description: 'The name of the Catage' +}) +age: number; +``` + + + +### Array type + +If the array type is an array type, you can configure the type field and use the `type` of the `items` to specify the type. + +```typescript +@ApiProperty({ + type: 'array', + items: { + type: 'string', + }, + example: ['1'] + } +}) +breeds: string[]; +``` + +### Enumeration type + +If it is an enumeration type, it can be defined by configuring the enmu field. + +```typescript +enum HelloWorld { + One = 'One', + Two = 'Two', + Three = 'Three', +} + +@ApiProperty({ + enum: ['One', 'Two', 'Three'] + description: 'The name of the Catage' +}) +hello: HelloWorld; +``` + +If the field is at the top level, the display effect is as follows: + +![swagger3](https://img.alicdn.com/imgextra/i1/O1CN015M37MU1KgtdNfqsgp_!!6000000001194-0-tps-1406-426.jpg) + + + +### Complex object types + +If the type of a property is an existing complex type, you can use the `type` parameter to specify the complex type. + +```typescript +export class Cat { + /** + * The name of the Catcomment + * @example Kitty + */ + @ApiProperty({ example: 'Kitty', description: 'The name of the Cat'}) + name: string; + + @ApiProperty({ example: 1, description: 'The age of the Cat' }) + age: number; + + @ApiProperty({ example: '2022-12-12 11:11:11', description: 'The age of the CatDSate' }) + agedata?: Date; + + @ApiProperty({ + example: 'Maine Coon', + description: 'The breed of the Cat', + }) + breed: string; +} + +export class CreateCatDto { + + // ... + + @ApiProperty({ + Type: Cat, // There is no need to specify example here. + }) + related: Cat; +} +``` + +The effect is as follows: + +![](https://img.alicdn.com/imgextra/i3/O1CN01KADwTb1rkS4gJExuP_!!6000000005669-2-tps-1376-1070.png) + + + +### Complex object array type + +If the type of an attribute is a complex array type, the writing is slightly different. + +Except that the `type` is declared as `array`, the `items` property only supports strings. You must use the `getSchemaPath` method to import a different type. + +In addition, if the `Cat` type has not been declared in the `type` field of other attributes, you need to use the `@ApiExtraModel` decorator to declare additional external types. + +```typescript +import { ApiProperty, getSchemaPath, ApiExtraModel } from '@midwayjs/swagger'; + +class Cat { + // ... +} + +@ApiExtraModel(Cat) +export class CreateCatDto { + // ... + + @ApiProperty({ + type: 'array', + items: { + $ref: getSchemaPath(Cat) + } + }) + relatedList: Cat[]; +} + + +``` + +The effect is as follows: + +![](https://img.alicdn.com/imgextra/i1/O1CN01h4sQJ41dP0uq4fgi7_!!6000000003727-2-tps-1332-666.png) + + + +### Circular dependencies + +When you have circular dependencies between classes, use lazy functions to provide type information. + +For example looping over the `type` field. + +```typescript +class Photo { + // ... + @ApiProperty({ + type: () => Album + }) + album: Album; +} +class Album { + // ... + @ApiProperty({ + type: () => Photo + }) + photo: Photo; +} +``` + +`getSchemaPath` can also be used. + +```typescript +export class CreateCatDto { + // ... + + @ApiProperty({ + type: 'array', + items: { + $ref: () => getSchemaPath(Cat) + } + }) + relatedList: Cat[]; +} +``` + + + + + +## Request definition + +The paths defined by the [OpenAPI](https://swagger.io/specification/) are each routing path, and each routing path has the definition of HTTP methods, such as GET, POST, DELETE, PUT, etc. + +### Query definition + +Use `@ApiQuery` to define Query data. + +The `@Query` decorator is automatically identified. + +```typescript +@Get('/get_user') +async getUser(@Query('name') name: string) { + return 'hello'; +} +``` + +If `@Query` is in the form of an object, you need to specify a name parameter in `@ApiQuery`, and the object type needs to be used with `@ApiProperty`, otherwise the form will become read-only. + +```typescript +export class UserDTO { + @ApiProperty() + name: string; +} + +@Get('/get_user') +@ApiQuery({ + name: 'query' +}) +async getUser(@Query() dto: UserDTO) { + // ... +} +``` + + + +### Body definition + +Use `@ApiBody` to define Body data. + +The `@Body` object type needs to be used with `@ApiProperty`. + +```typescript +export class UserDTO { + @ApiProperty() + name: string; +} + +@Post('/update_user') +async upateUser(@Body() dto: UserDTO) { + // ... +} +``` + +For additional details, please use `@ApiBody` enhancement. + +Note that Swagger stipulates that there can only be one `Body` definition. If `@ApiBody` is configured, the data extracted by the type will be automatically overwritten. + +For example, in the following example, the type of `Body` will be replaced with `Cat`. + +```typescript +@ApiBody({ + type: Cat +}) +async upateUser(@Body() dto: UserDTO) { + // ... +} +``` + +### File upload definition + +File upload is a special case in Post request. + +You can implement multiple files and `Fields` types by defining properties in DTO. + + +```typescript +import { ApiProperty, BodyContentType } from "@midwayjs/swagger"; + +export class CreateCatDto { + // ... + @ApiProperty({ + type: 'array', + items: { + type: 'string', + format: 'binary', + } + }) + files: any; +} + +// ... + +@Post('/test1') +@ApiBody({ + contentType: BodyContentType.Multipart, + schema: { + type: CreateCatDto, + } +}) +async upload1(@Files() files, @Fields() fields) { + // ... +} +``` +The Swagger UI shows: +![swagger6](https://img.alicdn.com/imgextra/i3/O1CN01w9dZxe1YQJv3uOycZ_!!6000000003053-0-tps-1524-1118.jpg) + +If you don't need multiple files, use schema definition. + +```typescript +export class CreateCatDto { + // ... + @ApiProperty({ + type: 'string', + format: 'binary', + }) + file: any; +} +``` + +The Swagger UI shows: +![swagger4](https://img.alicdn.com/imgextra/i3/O1CN01KlDHNt24mMglN1fyH_!!6000000007433-0-tps-1598-434.jpg) + + +### Request Header + +The Header parameter is defined by the ```@ApiHeader({...})``` decorator. + +```typescript +@ApiHeader({ + name: 'x-test-one', + description: 'this is test one' +}) +@ApiTags(['hello']) +@Controller('/hello') +export class HelloController {} +``` + +### Request Response + +```@ApiResponse({...})``` can be used to customize request Response. + +```typescript +@Get('/:id') +@ApiResponse({ + status: 200 + description: 'The found record', + type: Cat +}) +findOne(@Param('id') id: string, @Query('test') test: any): Cat { + return this.catsService.findOne(+id); +} +``` + +Other decorators that do not require status are also available: + +* ```@ApiOkResponse()``` +* ```@ApiCreatedResponse()``` +* ```@ApiAcceptedResponse()``` +* ```@ApiNoContentResponse()``` +* ```@ApiMovedPermanentlyResponse()``` +* ```@ApiBadRequestResponse()``` +* ```@ApiUnauthorizedResponse()``` +* ```@ApiNotFoundResponse()``` +* ```@ApiForbiddenResponse()``` +* ```@ApiMethodNotAllowedResponse()``` +* ```@ApiNotAcceptableResponse()``` +* ```@ApiRequestTimeoutResponse()``` +* ```@ApiConflictResponse()``` +* ```@ApiTooManyRequestsResponse()``` +* ```@ApiGoneResponse()``` +* ```@ApiPayloadTooLargeResponse()``` +* ```@ApiUnsupportedMediaTypeResponse()``` +* ```@ApiUnprocessableEntityResponse()``` +* ```@ApiInternalServerErrorResponse()``` +* ```@ApiNotImplementedResponse()``` +* ```@ApiBadGatewayResponse()``` +* ```@ApiServiceUnavailableResponse()``` +* ```@ApiGatewayTimeoutResponse()``` +* ```@ApiDefaultResponse()``` + +The definition of the data model returned by the HTTP request can also be specified by specifying the type. Of course, this data model needs to describe each field through the decorator ```@ApiProperty```. + +```typescript +import { ApiProperty } from '@midwayjs/swagger'; + +export class Cat { + @ApiProperty({ example: 'Kitty', description: 'The name of the Cat'}) + name: string; + + @ApiProperty({ example: 1, description: 'The age of the Cat' }) + age: number; + + @ApiProperty({ + example: 'Maine Coon', + description: 'The breed of the Cat', + }) + breed: string; +} +``` + +Swagger also supports extended fields with the prefix ```x-```, you can use the ```@ApiExtension(x-..., {...})``` decorator. + +```typescript +@ApiExtension('x-hello', { hello: 'world' }) +``` + +When you do not want to define the model type by type, we can add additional `schema` type descriptions by adding `@ApiExtraModel` to the Controller or Model Class. + +```typescript +@ApiExtraModel(TestExtraModel) +@Controller() +class HelloController { + @Post('/:id', { summary: 'test'}) + @ApiResponse({ + status: 200 + content: { + 'application/json ': { + schema: { + properties: { + data: { '$ref': getSchemaPath(TestExtraModel)} + } + } + } + } + }) + async create(@Body() createCatDto: CreateCatDto, @Param('id') id: number): Promise { + return this.catsService.create(createCatDto); + } +} + +// or +@ApiExtraModel(TestExtraModel) +class TestModel { + @ApiProperty({ + item: { + $ref: getSchemaPath(TestExtraModel) + }, + description: 'The name of the Catage' + }) + one: TestExtraModel; +} +``` + +### Generic returns data + +The Swagger itself does not support generic data. As a type of Typescript, generics will be erased during the build period and cannot be read at runtime. + +We can define it in some trick ways. + +For example, we need to add some common package structure to the return value. + +```typescript +{ + code: 200, + message: 'xxx', + data: any +} +``` + +To do this, we can write a method where the input parameter is the returned data and returns a wrapped class. + +```typescript +import { Type } from '@midwayjs/swagger'; + +type Res = { + code: number; + message: string; + data: T; +} + +export function SuccessWrapper(ResourceCls: Type): Type> { + class Successed { + @ApiProperty({ description: 'Status Code'}) + code: number; + + @ApiProperty({ description: 'message'}) + message: string; + + @ApiProperty({ + type: ResourceCls + }) + data: T; + } + + return Successed; +} +``` + +We can implement our own return class based on this method. + +```typescript +class ViewCat extends SuccessWrapper(Cat) {} +``` + +When using, you can directly specify this class. + +```typescript +@Get('/:id') +@ApiResponse({ + status: 200 + description: 'The found record', + type: ViewCat +}) +async findOne(@Param('id') id: string, @Query('test') test: any): ViewCat { + // ... +} +``` + + + +## More definition examples + +There are more ways to write in Swagger, and the framework supports them. For more usage, please refer to our [test case](https://github.com/midwayjs/midway/blob/main/packages/swagger/test/parser.test.ts). + + + +## Advanced usage + +### Route Tags +Swagger can add tags to each route for grouping. + +There are two ways to add tags. + + + +By default, the framework generates tags based on the Controller's path. For example, the following code will generate a `hello` tag, which will apply to all routes in this controller. + +```typescript +@Controller('/hello') +export class HelloController {} +``` + +If you need to customize the tags, you can use `@ApiTags([...])` to customize the Controller tags. + +```typescript +@ApiTags(['hello']) +@Controller('/hello') +export class HelloController {} +``` + +Starting from `v3.17.3`, you can control whether to automatically generate Controller tags by configuring `isGenerateTagForController`. + +```typescript +// src/config/config.default.ts +export default { + swagger: { + isGenerateTagForController: false + } +} +``` + + + + + +You can add `@ApiTags` directly to the route method. + +```typescript +// ... +export class HomeController { + @ApiTags(['bbb']) + @Get('/') + async home(): Promise { + // ... + } +} +``` + +You can also add tags through `@ApiOperation`. + +```typescript +// ... +export class HomeController { + @ApiOperation({ tags: ['bbb'] }) + @Get('/') + async home(): Promise { + // ... + } +} +``` + +The priority of `@ApiTags` is higher than `@ApiOperation`. If both exist, `@ApiTags` will override `@ApiOperation`. + +Similarly, `@ApiTags` on the route will also override `@ApiTags` on the controller. + + + + + +You can add descriptions to Tags by configuring. + +```typescript +// src/config/config.default.ts + +export default { + swagger: { + tags: [ + { + name: 'api', + description: 'API Document' + }, + { + name: 'hello', + description: 'Other Router' + }, + ] + } +} +``` + + + + +### Authorization verification + +You can add authorization and authentication configurations to configure authentication methods. You can configure ```basic```, ```bearer```, ```cookie```, ```oauth2```, ```apikey```, and ```custom```. + + + +#### basic + +Enable basic authentication + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'basic', + }, + }, +} +``` + +Association Controller + +```typescript +@ApiBasicAuth() +@Controller('/hello') +export class HelloController {} +``` + +#### **bearer** + +Enable bearer authentication (with bearerFormat set to JWT). + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'bearer', + }, + }, +} +``` + +Association Controller + +```typescript +@ApiBearerAuth() +@Controller('/hello') +export class HelloController {} +``` + +#### oauth2 + +Enable oauth2 authentication + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'oauth2', + flows: { + implicit: { + authorizationUrl: 'http://example.org/api/oauth/dialog', + scopes: { + 'write:pets': 'modify pets in your account', + 'read:pets': 'read your pets' + } + }, + authorizationCode: { + authorizationUrl: 'https://example.com/api/oauth/dialog', + tokenUrl: 'https://example.com/api/oauth/token', + scopes: { + 'write:pets': 'modify pets in your account', + 'read:pets': 'read your pets' + } + }, + }, + }, + }, +} +``` + +Association Controller + +```typescript +@ApiOAuth2() +@Controller('/hello') +export class HelloController {} +``` + +#### cookie +Enable cookie authentication + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'cookie', + securityName: 'testforcookie', + cookieName: 'connect.sid', + }, + }, +} +``` + +Association Controller + +```typescript +@ApiCookieAuth('testforcookie') +@Controller('/hello') +export class HelloController {} +``` + +#### apikey + +Enable cookie authentication + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'apikey', + name: 'api_key' + }, + }, +} +``` + +Association Controller + +```typescript +@ApiSecurity('api_key') +@Controller('/hello') +export class HelloController {} +``` + +#### custom verification + +Custom verification method, you need to design your own parameter configuration. + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'custom', + name: 'mycustom' + // ... + }, + }, +} +``` + +Association Controller + +```typescript +@ApiSecurity('mycustom') +@Controller('/hello') +export class HelloController {} +``` + + + +### Ignore routing verification + +You can set `@ApiExcludeSecurity` to ignore validation of a route. + +```typescript +@Controller('/api') +@ApiSecurity('api_key') +class APIController { + // ... + + @Get('/get_user') + @ApiExcludeSecurity() + async getUser() { + // ... + } +} +``` + + + +### Ignore routing + +Configuring `@ApiExcludeController` can ignore the entire Controller's routing. + +```typescript +@ApiExcludeController() +@Controller('/hello') +export class HelloController {} +``` + +Configure `@ApiExcludeEndpoint` to ignore individual routes. + +```typescript +@Controller('/hello') +export class HelloController { + + @ApiExcludeEndpoint() + @Get() + async getUser() { + // ... + } +} +``` + +If you need to meet more dynamic scenarios, you can configure routing filters to filter in batches. + +```typescript +// src/config/config.default.ts +import { RouterOption } from '@midwayjs/core'; + +export default { + // ... + swagger: { + routerFilter: (url: string, options: RouterOption) => { + return url === '/hello/getUser'; + } + }, +} +``` + +`routerFilter` is used to pass in a filter function, including `url` and `routerOptions` two parameters. `routerOptions` contains basic routing information. + +Whenever a route is matched, the `routerFilter` method will be automatically executed. When `routerFilter` returns true, it means that this route will be filtered. + + + +## Parameter configuration + +Swagger components provide the same parameter configuration capability as the [OpenAPI](https://swagger.io/specification/), which can be implemented through custom configuration. + +The configuration items are as follows: + +```typescript +/** + * see https://swagger.io/specification/ + */ +export interface SwaggerOptions { + /** + * default value: My Project + * https://swagger.io/specification/#info-object title field + */ + title?: string; + /** + * default value: This is a swagger-ui for midwayjs project + * https://swagger.io/specification/#info-object description field + */ + description?: string; + /** + * Default value: 1.0.0 + * https://swagger.io/specification/#info-object version field + */ + version?: string; + /** + * https://swagger.io/specification/#info-object contact field + */ + contact?: ContactObject; + /** + * https://swagger.io/specification/#info-object license field + */ + license?: LicenseObject; + /** + * https://swagger.io/specification/#info-object termsOfService field + */ + termsOfService?: string; + /** + * https://swagger.io/specification/#openapi-object externalDocs field + */ + externalDocs?: ExternalDocumentationObject; + /** + * https://swagger.io/specification/#openapi-object servers 字段 + */ + servers?: Array; + /** + * https://swagger.io/specification/#openapi-object tags field + */ + tags?: Array; + /** + * Please refer to the https://swagger.io/specification/#security-scheme-object + */ + auth?: AuthOptions | AuthOptions[]; + /** + * Default: /swagger-ui + * path to access swagger ui + */ + swaggerPath?: string; + /** + * ascii sorting route tags + * You can use 1-xxx, 2-xxx, 3-xxx to define tag + */ + tagSortable?: boolean; + /** + * Configuration Required in UI Display + * Please refer to the https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md#display + */ + displayOptions ?: { + deepLinking?: boolean; + displayOperationId?: boolean; + defaultModelsExpandDepth?: number; + defaultModelExpandDepth?: number; + defaultModelRendering?: 'example' | 'model'; + displayRequestDuration?: boolean; + docExpansion?: 'list' | 'full' | 'none'; + filter?: boolean | string; + maxDisplayedTags?: number; + showExtensions?: boolean; + showCommonExtensions?: boolean; + useUnsafeMarkdown?: boolean; + tryItOutEnabled?: boolean; + }; + + documentOptions?: { + /** + * Custom operationIdFactory for generating operationId + * @default () => controllerKey_webRouter.methodKey + */ + operationIdFactory?: ( + controllerKey: string, + webRouter: RouterOption + ) => string; + }; +} +/** + * Inherited from https://swagger.io/specification/#security-scheme-object + */ +export interface AuthOptions extends Omit { + /** + * Type of verification right + * basic => http basic verification + * bear => http jwt verification + * cookie => cookie verification + * oauth2 => use oauth2 + * apikey => apiKey + * custom => custom type + */ + authType: AuthType; + /** + * https://swagger.io/specification/#security-scheme-object type field + */ + type?: SecuritySchemeType; + /** + * authType = cookie can be modified by ApiCookie the name associated with the decorator + */ + securityName?: string; + /** + * authType = cookie can be modified, cookie name + */ + cookieName?: string; +} +``` + + + +## Decorator list + +All decorators of the component refer to the design of [@nestjs/swagger](https://github.com/nestjs/swagger) and are prefixed with ```Api```. All decorators are listed here: + +| Decorator | Support mode | +| --------------------------- | ----------------- | +| ```@ApiBody``` | Method | +| ```@ApiExcludeEndpoint``` | Method | +| ```@ApiExcludeController``` | Controller | +| ```@ApiHeader``` | Controller/Method | +| ```@ApiHeaders``` | Controller/Method | +| ```@ApiOperation``` | Method | +| ```@ApiProperty``` | Model Property | +| ```@ApiPropertyOptional``` | Model Property | +| ```@ApiResponseProperty``` | Model Property | +| ```@ApiQuery``` | Method | +| ```@ApiResponse``` | Method | +| ```@ApiTags``` | Controller/Method | +| ```@ApiExtension``` | Method | +| ```@ApiBasicAuth``` | Controller | +| ```@ApiBearerAuth``` | Controller | +| ```@ApiCookieAuth``` | Controller | +| ```@ApiOAuth2``` | Controller | +| ```@ApiSecurity``` | Controller | +| ```@ApiExcludeSecurity``` | Method | +| ```@ApiParam``` | Method | +| ```@ApiExtraModel``` | Controller | + + + +## UI rendering + +### Rendering from Swagger-ui-dist + +By default, if the `swagger-ui-dist` package is installed, the component will call `renderSwaggerUIDist` to render swagger ui by default. If you need to pass the options of swagger-ui, you can pass the `swaggerUIRenderOptions` option. + +```typescript +// src/config/config.default.ts +import { renderSwaggerUIDist } from '@midwayjs/swagger'; + +export default { + // ... + swagger: { + swaggerUIRender: renderSwaggerUIDist, + swaggerUIRenderOptions: { + // ... + } + }, +} +``` + +If you want to adjust the UI configuration, you can replace the default `swagger-initializer.js` with a custom file. + +```typescript +// src/config/config.default.ts +import { AppInfo } from '@midwayjs/core'; +import { renderSwaggerUIDist } from '@midwayjs/swagger'; +import { join } from 'path'; + +export default (appInfo: AppInfo) { + return { + // ... + swagger: { + swaggerUIRender: renderSwaggerUIDist, + swaggerUIRenderOptions: { + customInitializer: join(appInfo.appDir, 'resource/swagger-initializer.js'), + } + }, + } +} +``` + +The content of the customized `swagger-initializer.js` is roughly as follows: + +```javascript +window.onload = function() { + window.ui = SwaggerUIBundle({ + url: "/index.json", + dom_id: '#swagger-ui', + deepLinking: true, + presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIStandalonePreset + ], + plugins: [ + SwaggerUIBundle.plugins.DownloadUrl + ], + layout: "StandaloneLayout", + persistAuthorization: true, + }); +}; + +``` + +The url points to the current swagger json and can be modified by yourself. For the complete `swagger-ui` configuration, please refer to [here](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage /configuration.md). + +### Rendering from CDN addresses such as unpkg + +If the `swagger-ui-dist` package is not installed, the `renderSwaggerUIRemote` method is automatically used for rendering, and the cdn resource is provided by `unpkg.com` by default. + +```typescript +// src/config/config.default.ts +import { renderSwaggerUIRemote } from '@midwayjs/swagger'; + +export default { + // ... + swagger: { + swaggerUIRender: renderSwaggerUIRemote, + swaggerUIRenderOptions: { + // ... + } + }, +} +``` + + + +### Only Swagger JSON is provided + +If you only want to provide Swagger JSON, you can configure `renderJSON` to only render JSON without introducing the `swagger-ui-dist` package. + +```typescript +// src/config/config.default.ts +import { renderJSON } from '@midwayjs/swagger'; + +export default { + // ... + swagger: { + swaggerUIRender: renderJSON, + }, +} +``` + + + +## Frequently Asked Questions + +### `summary` or `description` in route annotations such as `@Get` do not take effect + +When there is an `@ApiOperation`, the `summary` or `description` in the `@ApiOperation` will be used first, so you only need to write one in routing annotations such as `@ApiOperation` and `@Get`. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/tablestore.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/tablestore.md new file mode 100644 index 000000000000..53bcc7056697 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/tablestore.md @@ -0,0 +1,185 @@ +# TableStore + +this topic describes how to use midway to access alibaba cloud TableStore. + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + +## Installation dependency + +```bash +$ npm i @midwayjs/tablestore@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/tablestore": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## Introducing components + + +First, introduce components and import them in `configuration.ts`: +```typescript +import { Configuration } from '@midwayjs/core'; +import * as tablestore from '@midwayjs/tablestore'; +import { join } from 'path' + +@Configuration({ + imports: [ + tablestore // Import tablestore Components + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + +## Configuration + + +For example: + +**Single-client configuration** +```typescript +// src/config/config.default +export default { + // ... + tableStore: { + client: { + accessKeyId: '', + secretAccessKey: '', + stsToken: '', /*When you use the STS authorization, you need to fill in. ref:https://help.aliyun.com/document_detail/27364.html */ + endpoint: '', + instancename: '' + }, + }, +} +``` + +**Multiple client configuration, need to configure multiple** + +```typescript +// src/config/config.default +export default { + // ... + tableStore: { + clients: { + db1: { + accessKeyId: '', + secretAccessKey: '', + stsToken: '', /*When you use the STS authorization, you need to fill in. ref:https://help.aliyun.com/document_detail/27364.html */ + endpoint: '', + instancename: '' + }, + db2: { + accessKeyId: '', + secretAccessKey: '', + stsToken: '', /*When you use the STS authorization, you need to fill in. ref:https://help.aliyun.com/document_detail/27364.html */ + endpoint: '', + instancename: '' + }, + }, + }, +} +``` +For more parameters, please refer to the [aliyun tablestore sdk](https://github.com/aliyun/aliyun-tablestore-nodejs-sdk) document. + + +## Use TableStore service + + +We can inject it into any code. +```typescript +import { Provide, Controller, Inject, Get } from '@midwayjs/core'; +import { TableStoreService } from '@midwayjs/tablestore'; + +@Provide() +export class UserService { + + @Inject() + tableStoreService: TableStoreService; + + async invoke() { + await this.tableStoreService.putRow(params); + } +} +``` + + +Different instances can be obtained using `TableStoreServiceFactory`. +```typescript +import { TableStoreServiceFactory } from '@midwayjs/tablestore'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + tableStoreServiceFactory: TableStoreServiceFactory; + + async save() { + const db1 = await this.tableStoreServiceFactory.get('db1'); + const db2 = await this.tableStoreServiceFactory.get('db2'); + + //... + + } +} +``` + + +Example: getRow +```typescript +import { join } from 'path'; +import { + TableStoreService + Long + CompositeCondition + SingleColumnCondition + LogicalOperator + ComparatorType +} from '@midwayjs/tablestore'; + +@Provide() +export class UserService { + + @Inject() + tableStoreService: TableStoreService; + + async getInfo() { + + const data = await tableStoreService.getRow({ + tableName: "sampleTable ", + primaryKey: [{ 'gid': Long.fromNumber(20013) }, { 'uid': Long.fromNumber(20013) }] + columnFilter: condition + }); + + // TODO + + } +} +``` +As shown in the example, the types exported in the original tablestore package should have been proxied and taken over by @midwayjs/tablestore. For more specific method parameters, see the [example](https://github.com/midwayjs/midway/tree/2.x/packages/tablestore/test/sample). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/tags.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/tags.md new file mode 100644 index 000000000000..fbb61707c675 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/tags.md @@ -0,0 +1,421 @@ +# label components + +Generic label components for `@midwayjs/faas`, `@midwayjs/web`, `@midwayjs/koa` and `@midwayjs/express` multiple frameworks. + +### scenes to be used +Tags are an abstract server-side common systematization capability that can be used for various purposes, such as: ++ Organize and manage resources + - Implement a taxonomy system (for content, crowd, etc.) + - Resource management system + + Add various color tags, object and scene tags to pictures, and filter pictures by tags + + Video and other material tags ++ access control + - Permissions system (admin, editor, guest) ++ status system (editing, published, etc.) + +Based on the addition, deletion, modification and query provided by the tag system, as well as the addition, deletion, modification and query of `entities` bound to tags through tags, more advanced business logic can be easily implemented. + +The labeling system is for this kind of business scenario, allowing the server to achieve more efficient and convenient business development based on labeling capabilities. + +Related Information: + +| web support | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + + +### how to use? + +1. Install dependencies + +```bash +$ npm i @midwayjs/tags --save +``` + +2. Introduce components in configuration + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as tags from '@midwayjs/tags'; +@Configuration({ + imports: [ + //... + tags + ], +}) +export class MainConfiguration {} +``` + +3. Add configuration + +```typescript +// src/config/config.local.ts +export default { + tags: { + clients: { + 'tagGroup1': { + // Use local memory as data storage + dialectType: 'memory', + }, + }, + } +} +``` + +4. Call in the code +```typescript +// src/testTags.ts +import { Provide, Inject, InjectClient } from '@midwayjs/core'; +import { TagServiceFactory, TagClient } from '@midwayjs/tags'; +@Provide() +export class TestTagsService { + @Inject() + tags: TagServiceFactory; + + // Equivalent to this.tags.get('tagGroup1') + @InjectClient(TagServiceFactory, 'tagGroup1') + tagClient: TagClient; + + @ServerlessTrigger(ServerlessTriggerType. HTTP, { path: '/tags/list', method: 'get'}) + async listTags() { + // You can also use this.tagClient directly + const tagClient: TagClient = this. tags. get('tagGroup1'); + // add new tag + const tagInfo = await tagClient. new({ + name: 'test-tag-name', + desc: 'tag desc', + }); + /* + tagInfo = { + success: true, + id: 1, + } + */ + // list top 20 tags + const tags = await tagClient. list({ count: true }); + /* + tags: { + list: [ + { + id: 1, + name: 'test-tag-name', + desc: 'tag desc' + } + ], + total: 1 + } + */ + return tags; + } +} + +``` + +### method + +#### Add tag new + +```typescript +new(tagDefine: { + // Tag name, cannot be repeated in the same group + name: string; + // label description + desc?: string; +}): Promise<{ + success: boolean; + message: string; + // label id + id?: number; +}>; +``` +#### Remove tags remove +Deleting a label will also delete the entity relationship bound to this label + +```typescript +remove(tagIdOrName: number | string): Promise<{ + success: boolean; + message: string; + // label id + id?: number; +}>; +``` +#### Update tag update +Fine-tune the basic information of a label +```typescript +update(tagIdOrName: number | string, params: Partial< { + name: string; + desc?: string; +}>): Promise<{ + success: boolean; + message: string; + // label id + id?: number; +}>; +``` +#### Enumerate tags list +Search tags, support pagination + +```typescript +list(listOptions?: { + // Search tags, support passing in tag id and tag name + tags?: Array; + // When searching, whether to use the intersection or union of the tags, the values are and and or + type?: MATCH_TYPE; + count?: boolean; + pageSize?: number; + page?: number; +}): Promise<{ + // label list + list: { + id: number; + name: string; + desc: string; + createAt: number; + updateAt: number; + }[]; + // total number of tags + total?: number; +}>; +``` +#### Binding entity bind +Binding an entity means binding anything else to a label. The entity here can be a picture or a file. The id of the entity is controlled by the user + +```typescript +bind(bindOptions: { + // label list + tags: Array; + // If there is no label, automatically create a label and bind it, the default is false + autoCreateTag?: boolean; + // entity id + objectId: number, +}): Promise<{ + success: boolean; + message: string; +}> +``` +#### Unbind entity unbind + +```typescript +unbind(unbindOptions: { + // Unbound multiple tags, tag id or tag name + tags: Array, + // entity id + objectId: number, +}): Promise<{ + success: boolean; + message: string; +}> +``` +#### List entities by label listObjects + +```typescript +listObjects(listOptions?: { + // tag id or tag name + tags?: Array; + count?: boolean; + // When searching, whether to use the intersection or union of the tags, the values are and and or + type?: MATCH_TYPE; + pageSize?: number; + page?: number; +}): Promise<{ + // list of entity ids + list: number[]; + // total number of entities + total?: number; +}>; +``` +#### Obtain tags based on entities listObjectTags + + +```typescript +listObjectTags(listOptions?: { + // entity id + objectId: number; + count?: boolean; + pageSize?: number; + page?: number; + +}): Promise<{ + list: { // label list + name: string; + desc?: string; + id: number; + createAt: number; + updateAt: number; + }[]; + // total number of tags + total?: number; +}>; +``` +### configuration + +Tags supports memory storage (default) and mysql database storage. The following is a configuration example: +```typescript +// src/config/config.local.ts +export default { + tags: { + clients: { + 'tagGroup1': { + // Use local memory as data storage + dialectType: 'memory', + }, + 'tagGroup2': { + // use mysql as data store + dialectType: 'mysql', + // Automatically synchronize the table structure + sync: true, + // mysql connection instance + instance: mysqlConnection. promise(), + }, + }, + } +} +``` +#### Memory storage configuration + +| Configuration | Value Type | Default Value | Configuration Description | +| -- | -- | -- | -- | +| dialectType | string `memory` | - | Configured as `memory`, enable memory storage | + +#### Mysql storage configuration + +If you want to use Mysql database as data storage, you need to pass Mysql's `database connection object` into the configuration of tags. + + +| Configuration | Value Type | Default Value | Configuration Description | +| -- | -- | -- | -- | +| dialectType | string `mysql` | - | Configure to `mysql`, then enable Mysql storage | +| sync | boolean | `false` | Automatically synchronize the table structure of Tags, the Tags component will create two data tables, see the data table information below for details | +| instance | `{ query: (sql: string, placeholder?: any[])}: Promise<[]>` | - | Mysql connection example, need to provide a query method, you can check the example below | +| tablePrefix | string | - | data table prefix | +| tableSeparator | string | `_` | splicing separator of data table | + +The following is an example of database connection using `mysql2` npm package: + +```typescript +// src/config/config.local.ts +const mysql = require('mysql2'); +export default () => { + const connection = mysql.createConnection({ + host: 'db4free.net', + user: 'tag***', + password: 'tag***', + database: 'tag***', + charset: 'utf8', + }); + return { + tags: { + clients: { + 'tagGroup': { + dialectType: 'mysql', + sync: true, + instance: { // mysql connection instance containing query + query: (...args) => { + return connection.promise().query(...args); + } + }, + }, + }, + } + } +} +``` + +You can also consider making the database connection in the `onConfigLoad` life cycle in `configuration.ts`, the advantage of this is that the database connection can be closed when it is closed: + +```typescript +// src/configuration.ts +import { Config, Configuration } from '@midwayjs/core'; +import { join } from 'path'; +import * as tags from '@midwayjs/tags'; +import { ITagMysqlDialectOption } from '@midwayjs/tags'; +const mysql = require('mysql2'); + +@Configuration({ + imports: [ + tags + ], +}) +export class MainConfiguration { + connection; + + @Config() + tags; + + async onConfigLoad(container) { + // create mysql connection + this.connection = mysql.createConnection({ + host: 'db4free.net', + user: 'tag***', + password: 'tag***', + database: 'tag***', + charset: 'utf8', + }); + let dialect: ITagMysqlDialectOption = { + dialectType: 'mysql', + sync: true, + instance: { + query: (...args) => { + return this.connection.promise().query(...args); + } + } + }; + + return { + tags: dialect + } + } + + async onStop() { + // close mysql connection + this.connection.close(); + } +} + +``` + + +##### Data table information + +The Tags component needs two data tables to store data, namely `tag` and `relationship`. The real table names of these two tables in the database are through the `table name prefix` and `table name separator` in the configuration. Spliced with `client name/group name`, for example: + + +```typescript +const clientName = 'local-test'; +const { tablePrefix = 'a', tableSeparator = '_' } = tagOptions; +const tagTableName = `${tablePrefix}${tableSeparator}${clientName}${tableSeparator}tag`; +// tagTableName: a_local-test_tag +const relationshipTableName = `${tablePrefix}${tableSeparator}${clientName}${tableSeparator}relationship` +// relationshipTableName: a_local-test-relationship +``` + + +When you enable the automatic table structure synchronization of `sync` in the configuration, if there are no these two tables, the corresponding data table will be created according to the following table structure: + +`tag` table structure: +```sql +CREATE TABLE `tag` ( + `id` BIGINT unsigned NOT NULL AUTO_INCREMENT, + `group` varchar(32) NULL, + `name` varchar(32) NULL, + `descri` varchar(128) NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + `update_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY (id) +) +``` +`relationship` table structure: +```sql +CREATE TABLE `relationship` ( + `id` BIGINT unsigned NOT NULL AUTO_INCREMENT, + `tid` BIGINT unsigned NOT NULL, + `oid` BIGINT unsigned NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + `update_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY (id) +) +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/tenant.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/tenant.md new file mode 100644 index 000000000000..d51834ea82df --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/tenant.md @@ -0,0 +1,142 @@ +# Tenant + +Here's how to quickly use tenant components in Midway. + +Related Information: + +| Description | | +| ------------------------------- | ---- | +| Available for standard projects | ✅ | +| Available for Serverless | ✅ | +| Available for integration | ✅ | +| Contains independent main frame | ❌ | +| Contains standalone logs | ❌ | + + + +## Tenant definition + +Tenant management is a function often required in the middle and back-end business development process. + +During development, different users need to be stored in different data sources, namespaces or areas. These different data areas are collectively called "tenants". + + + +## Install dependencies + +`@midwayjs/tenant` is the main function package. + +```bash +$ npm i @midwayjs/tenant@3 --save +``` + +Or add the following dependencies in `package.json` and reinstall. + +```json +{ + "dependencies": { + "@midwayjs/tenant": "^3.0.0", + // ... + } +} +``` + + + + +##Introduce components + + +First, introduce the component and import it in `src/configuration.ts`: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as tenant from '@midwayjs/tenant'; + +@Configuration({ + imports: [ + // ... + tenant, + ], +}) +export class MainConfiguration { +} +``` + + + +## Tenant information access + +Different tenant data are isolated from each other. Generally speaking, each user data is associated with relevant tenant information. After the user information is obtained through user authentication, its corresponding tenant data is obtained for subsequent data reading and writing. + +In Midway, tenant data can be saved in the request object ctx and can be used by all subsequent request scope objects. However, it is not enough for tenant information to be used only in the request link. It needs to be effective in different scopes, which requires a new architecture to support it. + +The component provides a `TenantManager` to manage tenant information. + +You need to save tenant information in each request link before retrieving it later. + +The format of tenant information can be defined as required. + +for example: + +```typescript +interface TenantInfo { + id: string; + name: string; +} +``` + +For example, save in middleware. + +```typescript +import { TenantManager } from '@midwayjs/tenant'; +import { Middleware, Inject } from '@midwayjs/core'; + +@Middleware() +class TenantMiddleware { + @Inject() + tenantManager: TenantManager; + + resolve() { + return async(ctx, next) => { + //Set tenant information in the request link + await this.tenantManager.setCurrentTenant({ + id: '123', + name: 'my tenant' + }); + } + } +} +``` + +Obtained in subsequent singleton services. + +```typescript +import { TenantManager } from '@midwayjs/tenant'; +import { Inject, Singleton } from '@midwayjs/core'; +import { TenantInfo } from '../interface'; + +@Singleton() +class TenantService { + @Inject() + tenantManager: TenantManager; + + async getTenantInfo() { + const tenantInfo = await this.tenantManager.getCurrentTenant(); + if (tenantInfo) { + console.log(tenantInfo.name); + // output => my tenant + } + } +} +``` + + + +:::tip + +* 1. Tenant information will definitely be associated with the request. If necessary, you can add middleware to different Frameworks. +* 2. The tenant information saved in each request is isolated +* 3. Regardless of whether it is a singleton or a request scope, you can only obtain the tenant data corresponding to the current request. + +::: diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/upload.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/upload.md new file mode 100644 index 000000000000..f1d7e2e54473 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/upload.md @@ -0,0 +1,444 @@ +# File Upload + +Universal upload component for `@midwayjs/faas`, `@midwayjs/web`, `@midwayjs/koa` and `@midwayjs/express` multiple frameworks, supports `file` (server temporary file) and `stream` (stream) two modes. + +Related Information: + +| web support | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | 💬 | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + +:::caution + +💬 Some function computing platforms do not support streaming request responses. Please refer to the corresponding platform capabilities. + +::: + +## Install dependencies + +```bash +$ npm i @midwayjs/upload@3 --save +``` + +Or add the following dependencies in `package.json` and reinstall. + +```json +{ + "dependencies": { + "@midwayjs/upload": "^3.0.0", + //... + }, + "devDependencies": { + //... + } +} +``` + + + +## Enable component + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as upload from '@midwayjs/upload'; + +@Configuration({ + imports: [ + // ...other components + upload + ], + //... +}) +export class MainConfiguration {} +``` + +3. Get the uploaded file in the code + +```typescript +import { Controller, Inject, Post, Files, Fields } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx; + + @Post('/upload') + async upload(@Files() files, @Fields() fields) { + /* + files = [ + { + filename: 'test.pdf', // original name of the file + data: '/var/tmp/xxx.pdf', // when the mode is file, it is the server temporary file address + fieldname: 'test1', // form field name + mimeType: 'application/pdf', // mime + }, + { + filename: 'test.pdf', // original name of the file + data: ReadStream, // when the mode is stream, it is the server temporary file address + fieldname: 'test2', // form field name + mimeType: 'application/pdf', // mime + }, + // ...file supports uploading multiple files at the same time + ] + + */ + return { + files, + fields + } + } +} +``` + +:::caution + +If the swagger component is enabled at the same time, please be sure to add the type of the upload parameter (the type corresponding to the decorator, and the type in @ApiBody), otherwise an error will be reported. For more information, please refer to the file upload section of swagger. + +::: + +## configuration + +### default allocation + +The default configuration is as follows, and generally does not need to be modified. + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/upload'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + //... + upload: { + // mode: UploadMode, the default is file, that is, upload to the temporary directory of the server, and can be configured as stream + mode: 'file', + // fileSize: string, the maximum upload file size, the default is 10mb + fileSize: '10mb', + // whitelist: string[], file extension whitelist + whitelist: uploadWhiteList. filter(ext => ext !== '.pdf'), + // tmpdir: string, temporary storage path for uploaded files + tmpdir: join(tmpdir(), 'midway-upload-files'), + // cleanTimeout: number, how long the uploaded file is automatically deleted in the temporary directory, the default is 5 minutes + cleanTimeout: 5 * 60 * 1000, + // base64: boolean, set whether the original body is in base64 format, the default is false, generally used for compatibility with Tencent Cloud + base64: false, + // Parse the file information in the body only when the matching path reaches /api/upload + match: /\/api\/upload/, + }, +} + +``` + + + +### Upload mode - file + +`file` is the default and recommended by the framework. + +Configure upload mode as `file` string, or use `UploadMode.File` exported by `@midwayjs/upload` package. + +When using the file mode, the `data` obtained from `this.ctx.files` is the `temporary file address` of the uploaded file on the server, and the content of this file can be obtained later by `fs.createReadStream` and other methods. + +When using the file mode, it supports uploading multiple files at the same time, and multiple files will be stored in `this.ctx.files` in the form of an array. + + + +:::caution + +When the `file` mode is adopted, since the upload component will match according to the `method` of the request and some of the iconic content in `headers` when receiving the request, if it is considered to be a file upload request, the request will be Parse and `write` the files in it to the temporary cache directory of the server. You can set the path that allows parsing files through `match` or `ignore` configuration of this component. + +After configuring `match` or `ignore`, you can ensure that your normal post and other request interfaces will not be illegally used by users for uploading, and you can `avoid` the risk of the server cache being full. + +You can check the section `Configuring the upload path to allow (match) or ignore (ignore)` below to configure it. + +::: + + + + +### Upload mode - stream + +Configure upload mode as `stream` string, or use `UploadMode.Stream` exported by `@midwayjs/upload` package to configure. + + +When using the stream mode, the `data` obtained from `this.ctx.files` is `ReadStream`, and then the data stream can be transferred to other `WriteStream` or `TransformStream` through `pipe` and other methods. + + +When using stream mode, only one file is uploaded at the same time, that is, there is only one file data object in `this.ctx.files` array. + + +In addition, the stream mode `will not` generate temporary files on the server, so there is no need to manually clear the temporary file cache after getting the uploaded content. + + + +### Upload whitelist + +Through the `whitelist` attribute, configure the file extensions that are allowed to be uploaded. If `null` is configured, the extensions will not be verified. + +:::caution + +If the configuration is `null`, the suffix name of the uploaded file will not be verified. If the file upload mode (mode=file) is adopted, it may be used by attackers to upload `.php`, `.asp` and other suffixes The WebShell implements the attack behavior. + +Of course, since the `@midwayjs/upload` component will `rerandomly generate` the file name of the uploaded temporary file, as long as the developer `does not return` the address of the uploaded temporary file to the user, then even if the user uploads For some unexpected files, there is no need to worry too much about being used. + +::: + + +If the uploaded file suffix does not match, a `400` error will be responded, and the default values are as follows: + +```ts +'.jpg', +'.jpeg', +'.png', +'.gif', +'.bmp', +'.wbmp', +'.webp', +'.tif', +'.psd', +'.svg', +'.js', +'.jsx', +'.json', +'.css', +'.less', +'.html', +'.htm', +'.xml', +'.pdf', +'.zip', +'.gz', +'.tgz', +'.gzip', +'.mp3', +'.mp4', +'.avi', +``` + +The default suffix whitelist can be obtained through the `uploadWhiteList` exported in the `@midwayjs/upload` package. + +In addition, midway upload component, in order to avoid some `malicious users`, uses some technical means to `forge` some extensions that can be truncated, so it will filter the binary data of the obtained extensions, and only support `0x2e` (that is, the English dot `.`), `0x30-0x39` (that is, the number `0-9`), `0x61-0x7a` (that is, the lowercase letters `a-z`) are used as extensions, and other characters will be Automatically ignored. + +Starting with v3.14.0, you can pass a function that can dynamically return a whitelist based on different conditions. + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/upload'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + upload: { + whitelist: (ctx) => { + if (ctx.path === '/') { + return [ + '.jpg', + '.jpeg', + ]; + } else { + return [ + '.jpg', + ] + }; + }, + // ... + }, +} +``` + + + +### MIME type checking + +Some `malicious users` will try to modify the extension of `.php` and other WebShells to `.jpg` to bypass the whitelist filtering rules based on the extension. In some server environments, this jpg file will still be used as PHP scripts to execute, pose a security risk. + +Therefore, the `@midwayjs/upload` component provides the `mimeTypeWhiteList` configuration parameter **【Please note that this parameter has no default value setting, that is, no verification by default】**, you can set the allowed file MIME format through this configuration, A rule is a `secondary array` consisting of an array `[extension, mime, [...moreMime]]`, for example: + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/upload'; +export default { + //... + upload: { + //... + // extension whitelist + whitelist: uploadWhiteList, + // Only the following file types are allowed to be uploaded + mimeTypeWhiteList: { + '.jpg': 'image/jpeg', + // Multiple MIME types can also be set, for example, the following files that allow the .jpeg suffix are jpg or png + '.jpeg': ['image/jpeg', 'image/png'], + // other types + '.gif': 'image/gif', + '.bmp': 'image/bmp', + '.wbmp': 'image/vnd.wap.wbmp', + '.webp': 'image/webp', + } + }, +} +``` + + +You can also use the `DefaultUploadFileMimeType` variable provided by the `@midwayjs/upload` component as the default MIME validation rule, which provides commonly used `.jpg`, `.png`, `.psd` and other file extensions MIME data: + +```typescript +// src/config/config.default.ts +import { uploadWhiteList, DefaultUploadFileMimeType } from '@midwayjs/upload'; +export default { + //... + upload: { + //... + // extension whitelist + whitelist: uploadWhiteList, + // Only the following file types are allowed to be uploaded + mimeTypeWhiteList: DefaultUploadFileMimeType, + }, +} +``` + +You can query the file format and corresponding MIME mapping through `https://mimetype.io/`. For the MIME identification of files, we use [file-type@16](https://www. npmjs.com/package/file-type) this npm package, please note the file types it supports. + +:::info + +The MIME type verification rule is only applicable to the file upload mode `mode=file`, and after setting this verification rule, since the file content needs to be read for matching, the upload performance will be slightly affected. + +However, we still recommend that you set the `mimeTypeWhiteList` parameter if possible, which will improve your application security. + +::: + +Starting with v3.14.0, you can pass a function that dynamically returns MIME rules based on different conditions. + +```typescript +// src/config/config.default.ts +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + upload: { + mimeTypeWhiteList: (ctx) => { + if (ctx.path === '/') { + return { + '.jpg': 'image/jpeg', + }; + } else { + return { + '.jpeg': ['image/jpeg', 'image/png'], + } + }; + } + }, +} +``` + + + +### Configure match or ignore + +When the upload component is enabled, when the `method` of the request is one of `POST/PUT/DELETE/PATCH`, if it is judged that `headers['content-type']` of the request contains `multipart/form-data` and When `boundary` is set, it will `**automatically enter**` upload file parsing logic. + +This will cause: If the user may manually analyze the request information of the website, manually call any interface such as `post`, and upload a file, it will trigger the parsing logic of the `upload` component, and create a file in the temporary directory The temporary cache of uploaded files will generate unnecessary `load` on the website server, and may `affect` the normal business logic processing of the server in severe cases. + +Therefore, you can add `match` or `ignore` configuration to the configuration to set which api paths are allowed to upload. + + + +### Same name Field + +The componennt support Field with the same name since v3.16.6. + +```typescript +// src/config/config.default.ts +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + upload: { + allowFieldsDuplication: true + }, +} + +``` + +After `allowFieldsDuplication` is enabled, Fields with the same name will be merged into an array. + +```typescript +import { Controller, Inject, Post, Files, Fields } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Post('/upload') + async upload(@Files() files, @Fields() fields) { + /* + fields = { + name: ['name1', 'name2'], + otherName: 'nameOther' + // ... + } + + */ + } +} +``` + + + + + + +## Temporary files and cleanup + + +If you use the `file` mode to get uploaded files, the uploaded files will be stored in the folder pointed to by the `tmpdir` option in the configuration of the `upload` component that you set in the `config` file. + +You can control the automatic temporary file cleanup time by using `cleanTimeout` in the configuration, the default value is `5 * 60 * 1000`, that is, the uploaded file will be automatically cleaned up after `5 minutes`, set it to `0` To disable the automatic cleaning function. + +You can also actively clean up the temporary files uploaded by the current request by calling `await ctx.cleanupRequestFiles()` in the code. + + + +## Safety warning + +1. Please pay attention to whether to enable `extension whitelist` (whiteList), if the extension whitelist is set to `null`, it may be used by attackers to upload `.php`, `.asp` and other WebShells. +2. Please pay attention to whether to set `match` or `ignore` rules, otherwise common `POST/PUT` and other interfaces may be exploited by attackers, resulting in increased server load and large space occupation. +3. Please pay attention to whether to set the `file type rule` (fileTypeWhiteList), otherwise the attacker may forge the file type to upload. + + + +## Front-end file upload example + +### 1. The form of html form + +```html +
+ Name:
+ File:
+ +
+``` + +### 2. Fetch FormData method + +```js +const fileInput = document. querySelector('#your-file-input'); +const formData = new FormData(); +formData.append('file', fileInput.files[0]); + +fetch('/api/upload', { + method: 'POST', + body: formData, +}); +``` + + + +## Postman test example + +![](https://img.alicdn.com/imgextra/i4/O1CN01iv9ESW1uIShNiRjBF_!!6000000006014-2-tps-2086-1746.png) \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/validate.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/validate.md new file mode 100644 index 000000000000..7bfd2bd9d347 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/validate.md @@ -0,0 +1,965 @@ +# Parameter verification + +We often need to perform type checking and parameter conversion when calling a method. Midway provides a simple ability to quickly check the type of a parameter. This ability comes from [joi](https://joi.dev/api/). + +Related information: + +| Description | | +| ----------------------------------- | --- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## Background + +The most commonly used parameter check is the controller (Controller), and you can also use this capability in any Class. + + +Let's take the user used in the controller (Controller) as an example. + +```text +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ └── user.ts +│ ├── interface.ts +│ └── service +│ └── user.ts +├── test +├── package.json +└── tsconfig.json +``` + +Under normal circumstances, we obtain all post results from the `body` and perform some verifications. + +```typescript +// src/interface.ts +export interface User { + id: number; + firstName: string; + lastName: string; + age: number; +} + +// src/controller/home.ts +import { Controller, Get, Provide } from '@midwayjs/core'; + +@Controller('/api/user') +export class HomeController { + + @Post('/') + async updateUser(@Body() user: User ) { + if ( !user.id || typeof user.id !== 'number') { + throw new Error('id error'); + } + + if (user.age <= 30) { + throw new Error('age not match'); + } + // xxx + } +} +``` +If each method needs to be verified in this way, it will be very complicated. + +In response to this situation, Midway provides Validate components. `@Validate` and `@Rule` decorators are used to **quickly define verification rules** to help users **reduce these duplicate codes**. + +Note that starting with v3, `@Rule` and `@Validate` decorators are exported from `@midwayjs/validate`. + + + +## Installation dependency + +```bash +$ npm i @midwayjs/validate@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/validate": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + +## Open the component + +Add components to `configuration.ts`. + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as validate from '@midwayjs/validate'; +import { join } from 'path'; + +@Configuration({ + imports: [koa, validate], + importConfigs: [join(__dirname, './config')] +}) +export class MainConfiguration { + @App() + app: koa.Application; + + async onReady() { + // ... + } +} +``` + +## Define inspection rules + + +According to the above logic, we need to **redefine a new Class** because the decorator can only be decorated on the actual Class, not the interface. + + +To facilitate subsequent processing, we put the user in a `src/dto` directory. + + +> Data Transfer Object DTO is a simple container for a set of aggregated data that needs to be transmitted across process or network boundaries. It should not contain business logic and limit its behavior to activities such as internal consistency checking and basic verification. + +```typescript +// src/dto/user.ts +import { Rule, RuleType } from '@midwayjs/validate'; + +export class UserDTO { + @Rule(RuleType.number().required()) + id: number; + + @Rule(RuleType.string().required()) + firstName: string; + + @Rule(RuleType.string().max(10)) + lastName: string; + + @Rule(RuleType.number().max(60)) + age: number; +} +``` + +Since this class belongs to a `PlainObject` and does not need to be managed by dependency injection, we do not need to provide a `@Provide` decorator. + + +This User Class provides three attributes and their corresponding verification rules. + + +- `id` is a required number type. +- `firstName` a required string type +- `lastName` an optional string type with a maximum length of 10 +- A maximum number of `age` is not more than 60. + +The `@Rule` decorator is used to **modify the attributes** that need to be verified. Its parameters are a chain method of verification rules provided by the `RuleType` object. + +:::info +The `RuleType` here is the joi object itself. +::: + +[joi](https://joi.dev/api/) provides a lot of verification types. You can also verify fields in objects and arrays, such as `RuleType.string().email()` commonly used for strings, and regular check for `RuleType.string().pattern(/xxxx/)`. You can query API documents of [joi](https://joi.dev/api/). + + + +## Check parameters + + +After defining the type, it can be directly used in the business code. + +```typescript +// src/controller/home.ts +import { Controller, Get, Provide, Body } from '@midwayjs/core'; +import { UserDTO } from './dto/user'; + +@Controller('/api/user') +export class HomeController { + + @Post('/') + async updateUser(@Body() user: UserDTO ) { + // user.id + } +} +``` + +All the verification codes have disappeared, and the business has become purer. Of course, remember to replace the original user interface with Class. + +Once the verification fails, the browser or console will report a similar error. + +``` +ValidationError: "id" is required +``` + +In addition, because the type of `id` is defined, the id is automatically changed to a number when a string is obtained. + +```typescript +async updateUser(@Body() user: UserDTO ) { + // typeof user.id === 'number' +} +``` + + + +If you need to configure information separately at the method level, you can use the `@Validate` decorator, such as configuring the error status separately. + +```typescript +// src/controller/home.ts +import { Controller, Get, Provide } from '@midwayjs/core'; +import { Validate } from '@midwayjs/validate'; +import { UserDTO } from './dto/user'; + +@Controller('/api/user') +export class HomeController { + + @Post('/') + @Validate({ + errorStatus: 422, + }) + async updateUser(@Body() user: UserDTO ) { + // user.id + } +} +``` + +In general, use the global default configuration. + +## General scenario verification + +If the parameter is not a DTO, you can use the `@Valid` decorator for verification. The `@Valid` decorator can directly pass a Joi rule. + +```typescript +// src/controller/home.ts +import { Controller, Get, Query } from '@midwayjs/core'; +import { Valid, RuleType } from '@midwayjs/validate'; +import { UserDTO } from './dto/user'; + +@Controller('/api/user') +export class HomeController { + @get('/') + async getUser(@Valid(RuleType.number().required()) @Query('id') id: number) { + // ... + } +} +``` + +In non-Web scenarios, if there are no Web class decorators such as `@Body`, you can also use the `@Valid` decorator for verification. If no parameters are passed, the DTO rules will be reused. + +For example in a service: + +```typescript +import { Valid } from '@midwayjs/validate'; +import { Provide } from '@midwayjs/core'; +import { UserDTO } from './dto/user'; + +@Provide() +export class UserService { + async updateUser(@Valid() user: UserDTO ) { + //... + } +} +``` + +If the parameter is not DTO, there is no rule, and a validation rule in Joi format can also be passed through the parameter. + +```typescript +import { Valid, RuleType } from '@midwayjs/validate'; +import { Provide } from '@midwayjs/core'; + +@Provide() +export class UserService { + async updateUser(@Valid(RuleType. number(). required()) userAge: number ) { + //... + } +} +``` + + + +## Validate pipeline + +If your parameters are basic types, such as `number`, `string`, `boolean`, you can use the pipeline provided by the component for validation. + +The default web parameter decorators can all be piped as the second argument. + +for example: + +```typescript +import { ParseIntPipe } from '@midwayjs/validate'; + +@Controller('/api/user') +export class HomeController { + + @Post('/update_age') + async updateAge(@Body('age', [ParseIntPipe]) age: number ) { + //... + } +} +``` + +The `ParseIntPipe` pipeline can convert strings and numeric data into numbers, so that the `age` field obtained from the request parameters will pass the validation of the pipeline and be converted into a numeric format. + +The built-in pipelines that can be used are: + +* `ParseIntPipe` +* `ParseFloatPipe` +* `ParseBoolPipe` +* `DefaultValuePipe` + + + +`ParseIntPipe` is used to convert an argument to an integer. + +```typescript +import { ParseIntPipe } from '@midwayjs/validate'; + +//... +async update(@Body('age', [ParseIntPipe]) age: number) { + return age; +} + +update({ age: '12'} ); => 12 +update({ age: '12.2'} ); => Error +update({ age: 'abc'} ); => Error +``` + +`ParseFloatPipe` is used to convert the parameter to a floating point number. + +```typescript +import { ParseFloatPipe } from '@midwayjs/validate'; + +//... +async update(@Body('size', [ParseFloatPipe]) size: number) { + return size; +} + +update({ size: '12.2'} ); => 12.2 +update({ size: '12'} ); => 12 +``` + +`ParseBoolPipe` is used to convert parameters to boolean values. + +```typescript +import { ParseBoolPipe } from '@midwayjs/validate'; + +//... +async update(@Body('isMale', [ParseBoolPipe]) isMale: boolean) { + return isMale; +} + +update({ isMale: 'true'} ); => true +update({ isMale: '0'} ); => Error +``` + +`DefaultValuePipe` is used to set the default value. + +```typescript +import { DefaultValuePipe } from '@midwayjs/validate'; + +//... +async update(@Body('nickName', [new DefaultValuePipe('anonymous')]) nickName: string) { + return nickName; +} + +update({ nickName: undefined} ); => 'anonymous' +``` + + + +## Custom validate pipeline + +If the default pipeline does not meet the requirements, you can quickly implement a custom validation pipeline through inheritance. The component has provided a `ParsePipe` class for quick writing. + +```typescript +import { Pipe } from '@midwayjs/core'; +import { ParsePipe, RuleType } from '@midwayjs/validate'; + +@Pipe() +export class ParseCustomDataPipe extends ParsePipe { + getSchema(): RuleType. AnySchema { + //... + } +} +``` + +`getSchema` method is used to return a validation rule conforming to `Joi` format. + +For example, the code of `ParseIntPipe` is as follows. When the pipeline is executed, the schema will be automatically obtained for verification, and the value will be returned after the verification is successful. + +```typescript +import { Pipe } from '@midwayjs/core'; +import { ParsePipe, RuleType } from '@midwayjs/validate'; + +@Pipe() +export class ParseIntPipe extends ParsePipe { + getSchema() { + return RuleType.number().integer().required(); + } +} +``` + + + +## Check rule + + + +### Common verification writing + +```typescript +RuleType.number().required(); // Number, required +RuleType.string().empty('') // string is not required +RuleType.number().max(10).min(1); //Number, Maximum and Minimum +RuleType.number().greater(10).less(50); // Number, greater than 10, less than 50 + +RuleType.string().max(10).min(5); //String, maximum length 10, minimum 5 +RuleType.string().length(20); //String, length 20 +RuleType.string().pattern(/^[abc]+$/); // String, matching regular format + +RuleType.object().length(5); // Object, key number equals 5 + + +RuleType.array().items(RuleType.string()); //Array, each element is a string +RuleType.array().max(10); // Array, maximum length is 10 +RuleType.array().min(10); //Array, minimum length is 10 +RuleType.array().length(10); // Array, length 10 + +RuleType.string().allow('') // non-required fields pass in an empty string + +export enum DeviceType { + iOS = 'ios', + Android = 'android', +} +RuleType.string().valid(...Object.values(DeviceType)) // validate by enum +``` + + + +### Cascade Check + +Midway supports that the attribute in the Class for each check is still an object. + + +We add an attribute `school` to `UserDTO` and give a `SchoolDTO` type. + +```typescript +import { Rule, RuleType } from '@midwayjs/validate'; + +export class SchoolDTO { + @Rule(RuleType.string().required()) + name: string; + @Rule(RuleType.string()) + address: string; +} + +export class UserDTO { + @Rule(RuleType.number().required()) + id: number; + + @Rule(RuleType.string().required()) + firstName: string; + + @Rule(RuleType.string().max(10)) + lastName: string; + + // Complex object + @Rule(getSchema(SchoolDTO).required()) + school: SchoolDTO; + + // Object array. + @Rule(RuleType.array().items(getSchema(SchoolDTO)).required()) + schoolList: SchoolDTO[]; +} +``` + +In this case, the parameter of the `@Rule` decorator can be the type that needs to be verified. + + + +### Inheritance check + + +Midway supports the verification inheritance method, which allows developers to verify parameters when they extract common object attributes. + + +For example, we `CommonUserDTO` the following to extract some common attributes of the interface, and then `UserDTO` specific parameters required as special interfaces. + +```typescript +import { Rule, RuleType } from '@midwayjs/validate'; + +export class CommonUserDTO { + @Rule(RuleType.string().required()) + token: string; + @Rule(RuleType.string()) + workId: string; +} + +export class UserDTO extends CommonUserDTO { + + @Rule(RuleType.string().required()) + name: string; +} +``` + + +The old version needs to be added to the subclass, the new version does not need ~ + +:::info +If the attribute name is the same, the rule of the current attribute is taken for verification and will not be merged with the parent class. +::: + + + +### Multi-type verification + +Starting from v3.4.5, Midway supports different types of verification for a certain attribute. + +For example, a type can be either a normal type or a complex type. + +```typescript +import { Rule, RuleType, getSchema } from '@midwayjs/validate'; + +export class SchoolDTO { + @Rule(RuleType.string().required()) + name: string; + @Rule(RuleType.string()) + address: string; +} + +export class UserDTO { + + @Rule(RuleType.string().required()) + name: string; + + @Rule(RuleType.alternatives([RuleType.string(), getSchema(SchoolDTO)]).required()) + school: string | SchoolDTO; +} +``` + +We can use `getSchema` methods to get the current joi schema from a DTO to perform complex logical processing. + + + +### Create a new DTO from the original DTO + + +Sometimes, we want to get some attributes from a DTO and become a new DTO class. + + +Midway provides `PickDto` and `OmitDto` methods to create a new DTO based on the existing DTO type. + + +The `PickDto` is used to get some attributes from the existing DTO and become the new DTO, while the `OmitDto` is used to remove some of them, such: + + +```typescript +// src/dto/user.ts +import { Rule, RuleType, PickDto } from '@midwayjs/validate'; + +export class UserDTO { + @Rule(RuleType.number().required()) + id: number; + + @Rule(RuleType.string().required()) + firstName: string; + + @Rule(RuleType.string().max(10)) + lastName: string; + + @Rule(RuleType.number().max(60)) + age: number; +} + +// Inherit a new DTO +export class SimpleUserDTO extends PickDto(UserDTO, ['firstName', 'lastName']) {} + +// const simpleUser = new SimpleUserDTO(); +// Contains only firstName and lastName attributes +// simpleUser.firstName = xxx + +export class NewUserDTO extends OmitDto(UserDTO, ['age']) {} + +// const newUser = new NewUserDTO(); +// newUser.age definition and attribute do not exist + +// Use +async login(@Body() user: NewUserDTO) { + // ... +} + +``` + + + +### Reuse verification rules + +If many fields are required for strings or similar requirements, writing the `RuleType.string().required()` is a bit long, and the duplicate part can be assigned to a new rule object for reuse. + + +```typescript + +// Define your department's specifications or commonly used ones in a document yourself. +const requiredString = RuleType.string().required(); + +export class UserDTO { + + @Rule(requiredString) // So you don't have to write it so long + name: string; + + @Rule(requiredString) // Same as above + nickName: string; + + @Rule(requiredString) // Same as above + description: string; +} + +// Define your department's specifications or commonly used ones in a document yourself. +const maxString = (length)=> RuleType.string().max(length); + +export class UserDTO { + + @Rule(requiredString) // Same as above + name: string; + + @Rule(requiredString) // Same as above + nickName: string; + + @Rule(requiredString) // Same as above + description: string; + + @Rule(maxString(50)) // This way, you can change the parameter + info: string; + + @Rule(maxString(50).required()) // This will do + info2: string; +} +``` + + + +## Multilingual + +In Validate, the [i18n](./i18n) component is also relied on to internationalize check messages. + +By default, both `en_US` and `zh_CN` are available. When a request fails, the specified language is returned. + + + +### Specify the language through the decorator + +By default, messages will be returned following the `defaultLocale` of i18n components and the browser's access language. However, we can specify the currently translated language in the decorator, such: + +```typescript +@Controller('/user') +export class UserController { + @Post('/') + @Validate({ + locale: 'en_US', + }) + async getUser(@Body() bodyData: UserDTO) { + // ... + } +} + +``` + + + +### Specify language through parameters + +In addition to decorator designation, we can also use the standard i18n to specify the language through parameters. + +For example, Query parameters. + +``` +Get /user/get_user?locale=zh_CN +``` + +For more information, see [i18n](./i18n). + + + +### Translation in other languages + +By default, Midway provides both `en_US` and `zh_CN` translations. If additional translations are required, you can configure them in i18n. + +For example: + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // Add translation + zh_TW: { + validate: require('../../locales/zh_TW.json') + }, + } +} +``` + +If possible, we hope you will submit the translation to Midway for everyone to use. + + + +## Custom error text + + + +### Specifies the text of a single rule + +If you only want to define an error message for a rule in a DTO, you can simply specify. + +```typescript +export class UserDTO { + @Rule(RuleType.number().required().error(new Error('my custom message'))) + id: number; +} +``` + +All rules on this `id` attribute will return a custom message if the verification fails. + + + +### Global Specify Partial Text + +By configuring the `validate` multilingual text table of the i18n component, you can selectively replace most of the check text, and all rules will apply the text. + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // Put your translated text here + localeTable: { + zh_CN: { + validate: { + 'string. Max': 'Hello World', + }, + }, + }, + } +} +``` + +The `validate` here is the language table keyword configured by the `@midwayjs/validate` component in the i18n component. + +Because the [default language table](https://github.com/midwayjs/midway/tree/main/packages/validate/locales) is also in the form of an object, we can easily find the fields and replace them. + +Since these texts distinguish languages, they need to be handled carefully, for example, replacing different languages. + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // Put your translated text here + localeTable: { + zh_CN: { + validate: { + 'string.max': '字符超长', + }, + }, + en_US: { + validate: { + 'string.max': 'string is too long', + }, + }, + }, + } +} +``` + + + +### Fully customize error text + +If you want to completely customize the wrong text, you can solve it by replacing the built-in language translation text. + +For example: + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + localeTable: { + // Replace Chinese translation + zh_CN: { + validate: require('../../locales/custom.json'), + }, + }, + } +} +``` + + + + + +## Default configuration + +We can do some configuration for validate components. + +| Configuration Item | Type | Description | +| ------------------ | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| errorStatus | number | When the verification error occurs, the returned Http status code takes effect in the http scenario. The default 422 | +| locale | string | The default language for verifying the error text. Currently, there are two languages: `en_US` and `zh_CN`. The default language is `en_US`. | +| validationOptions | Joi's ValidationOptions options | Commonly used options are allowUnknown, stripUnknown and other options. If configured, the global validation allows undefined fields to appear. For more information, please see joi's [ValidationOptions option](https://joi.dev/api/?v= 17.6.0#anyvalidatevalue-options). | + + + + + +## Independent verification service + +The bottom layer of the component provides a single instance of `ValidateService` verification service class, which can be used in middleware or independent services if necessary. In fact, all the verification decorators will eventually go to this method. + +`ValidateService` provides a `validate` method for verifying DTO. + +Let's take the `UserDTO` defined above as an example. + +```typescript +import { ValidateService } from '@midwayjs/validate'; + +export class UserService { + + @Inject() + validateService: ValidateService; + + async inovke() { + + // ... + const result = this.validateService.validate(UserDTO, { + name: 'harry', + nickName: 'harry' + }); + + // Failed to return to re. Error + // Successfully returned result.value + } +} +``` + +The result returned by the `validate` method contains two attributes: `error` and `value`. Failure will return a `MidwayValidationError` error, and success will return a formatted DTO object. + + + +## Frequently Asked Questions + +### 1. Allow undefined fields + +Since some users want to allow undefined fields during parameter verification, they can be set separately on the global configuration and decorator. The former takes effect on the global and the latter takes effect on a single verification. + +```typescript +// src/config/config.default.ts +export default { + // ... + validate: { + validationOptions: { + allowUnknown: true, // global takes effect + } + } +} +``` + +Or on the decorator. + +```typescript +@Controller('/api/user') +export class HomeController { + + @Post('/') + @Validate({ + validationOptions: { + allowUnknown: true + } + }) + async updateUser(@Body() user: UserDTO ) { + // user.id + } +} +``` + + + +### 2. Remove undefined attributes from parameters + +It is also a validationOptions attribute, which can directly eliminate some attributes in the passed-in parameters. + +```typescript +// src/config/config.default.ts +export default { + // ... + validate: { + validationOptions: { + stripUnknown: true, // global takes effect + } + } +} +``` + +Or on the decorator. + +```typescript +@Controller('/api/user') +export class HomeController { + + @Post('/') + @Validate({ + validationOptions: { + stripUnknown: true + } + }) + async updateUser(@Body() user: UserDTO ) { + } +} +``` + + + +### 3. Handling verification errors + +As mentioned above, Midway will throw `MidwayValidationError` error when the check fails, which we can handle in the [exception handler](../error_filter). + +For example: + +```typescript +// src/filter/validate.filter +import { Catch } from '@midwayjs/core'; +import { MidwayValidationError } from '@midwayjs/validate'; +import { Context } from '@midwayjs/koa'; + +@Catch(MidwayValidationError) +export class ValidateErrorFilter { + async catch(err: MidwayValidationError, ctx: Context) { + // ... + return { + status: 422 + message: 'Check parameter error,' + err.message + } + } +} +``` + + + +### 4. Temporarily disable global verification + +After the component is enabled, as long as the parameter uses DTO, it will be automatically verified. If a parameter does not need to be verified temporarily, you can use the following writing method. + +```typescript +@Controller('/api/user') +export class HomeController { + + @Post('/') + async updateUser(@Body() user: Partial ) { + } +} +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/ws.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/ws.md new file mode 100644 index 000000000000..15e47723927e --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/extensions/ws.md @@ -0,0 +1,476 @@ +# WebSocket + +The [ws](https://www.npmjs.com/package/ws) module is an implementation of a WebSocket protocol on the Node side, which allows the client (usually the browser) to persist and connect to the server side. +This feature of continuous connection makes WebSocket particularly suitable for use in scenarios such as games or chat rooms. + +Midway provides support and encapsulation of [ws](https://www.npmjs.com/package/ws) module, which can simply create a WebSocket service. + +Related information: + +**Provide services** + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ❌ | +| Can be used for integration | ✅ | +| Contains independent main framework | ❌ | +| Contains independent logs | ❌ | + + + +## Installation dependency + + +Install WebSocket dependencies in existing projects. +```bash +$ npm i @midwayjs/ws@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/ws": "^3.0.0", + // ... + } +} +``` + +## Open the component + +`@midwayjs/ws` can be used as an independent main framework. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as ws from '@midwayjs/ws'; + +@Configuration({ + imports: [ws] + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + +``` + +It can also be attached to other main frameworks, such as `@midwayjs/koa`. + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as ws from '@midwayjs/ws'; + +@Configuration({ + imports: [koa, ws] + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + +``` + + + +## Directory structure + + +The following is the basic directory structure of WebSocket project. Similar to traditional applications, we have created a `socket` directory to store service codes for WebSocket services. +``` +. +├── package.json +├── src +│ ├── configuration.ts ## entry configuration file +│ ├── interface.ts +│ └── socket ## ws service file +│ └── hello.controller.ts +├── test +├── bootstrap.js ## service startup portal +└── tsconfig.json +``` + + +## Socket service + + +Midway defines WebSocket services through the `@WSController` decorator. +```typescript +import { WSController } from '@midwayjs/core'; + +@WSController() +export class HelloSocketController { + // ... +} +``` +When there is a client connection, `connection` event will be triggered. We can use the `@OnWSConnection()` decorator in the code to decorate a method. When each client connects to the service for the first time, the method will be automatically called. +```typescript +import { WSController, OnWSConnection, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/ws'; +import * as http from 'http'; + +@WSController() +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSConnection() + async onConnectionMethod(socket: Context, request: http.IncomingMessage) { + console.log('namespace / got a connection ${this.ctx.readyState}'); + } +} + +``` + + +:::info +The ctx here is equivalent to the WebSocket instance. +::: + + +## Messages and responses + + +The WebSocket is to obtain data by monitoring events. Midway provides a `@OnWSMessage()` decorator to format the received event. Every time the client sends an event, the modified method will be executed. +```typescript +import { WSController, OnWSMessage, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/ws'; + +@WSController() +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSMessage('message') + async gotMessage(data) { + return { name: 'harry', result: parseInt(data) +5 }; + } +} + +``` + + +We can send messages to all connected clients through the `@WSBroadCast` decorator. +```typescript +import { WSController, OnWSConnection, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/ws'; + +@WSController() +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSMessage('message') + @WSBroadCast() + async gotMyMessage(data) { + return { name: 'harry', result: parseInt(data) +5 }; + } + + @OnWSDisConnection() + async disconnect(id: number) { + console.log('disconnect '+ id); + } +} + +``` +With the `@OnWSDisConnection` decorator, do some extra processing when the client is disconnected. + + + +## WebSocket Server instance + +The App provided by this component is the WebSocket Server instance itself, which can be obtained as follows. + +```typescript +import { Controller, App } from '@midwayjs/core'; +import { Application } from '@midwayjs/ws'; + +@Controller() +export class HomeController { + + @App('webSocket') + wsApp: Application; +} +``` + +For example, we can broadcast messages in other Controller or Service. + +```typescript +import { Controller, App } from '@midwayjs/core'; +import { Application } from '@midwayjs/ws'; + +@Controller() +export class HomeController { + + @App('webSocket') + wsApp: Application; + + async invoke() { + this.wsApp.clients.forEach(ws => { + // ws.send('something'); + }); + } +} +``` + + + +## Heartbeat check + +Sometimes the connection between the server and the client may be interrupted, and neither the server nor the client is aware of the disconnection. + +Heartbeat check proactive disconnect requests can be configured by enabling `enableServerHeartbeatCheck`. + +```typescript +// src/config/config.default +export default { + // ... + webSocket: { + enableServerHeartbeatCheck: true, + }, +} +``` + +The default check time is `30*1000` milliseconds, which can be modified through `serverHeartbeatInterval`, and the configuration unit is milliseconds. + +```typescript +// src/config/config.default +export default { + // ... + webSocket: { + serverHeartbeatInterval: 30000, + }, +} +``` + +This configuration will automatically send `ping` packets at regular intervals. If the client does not return a message in the next time interval, it will be automatically `terminate`. + +If the client wants to know the status of the server, it can do so by listening to the `ping` message. + +```typescript +import WebSocket from 'ws'; + +function heartbeat() { + clearTimeout(this.pingTimeout); + + // After each ping is received, delay and wait. If the server ping message is not received next time, it is considered that there is a problem. + this.pingTimeout = setTimeout(() => { + //Reconnect or abort + }, 30000 + 1000); +} + +const client = new WebSocket('wss://websocket-echo.com/'); + +// ... +client.on('ping', heartbeat); +``` + + + +## Local test + +### Configure test ports + +Because the ws framework can be started independently (attached to the default http service, it can also be started with other midway frameworks). + +When starting as a standalone framework, you need to specify a port. + +```typescript +// src/config/config.default +export default { + // ... + webSocket: { + port: 3000 + }, +} +``` + +When starting as a sub-framework (for example, and HTTP, because HTTP does not specify a port during a single test (automatically generated using SuperTest), it cannot be tested well, and only one port can be explicitly specified in the Test environment. + +```typescript +// src/config/config.unittest +export default { + // ... + koa: { + port: null + }, + webSocket + port: 3000 + }, +} +``` + +:::tip + +- 1. The port here is only the port that the WebSocket service starts during testing. +- 2. The port in koa is null, which means that the http service will not be started without configuring the port in the test environment. + +::: + +### Test code + +Like other Midway testing methods, we use `createApp` to start the project. + +```typescript +import { createApp, close } from '@midwayjs/mock' +// The Framework definition used here is subject to the main framework. +import { Framework } from '@midwayjs/koa'; + +describe('/test/index.test.ts', () => { + + it('should create app and test webSocket', async () => { + const app = await createApp(); + + //... + + await close(app); + }); + +}); +``` + + +### Test client + +You can use `ws` to test. You can also use the `ws` module-based test client provided by Midway. + + +For example: +```typescript +import { createApp, close, createWebSocketClient } from '@midwayjs/mock'; +import { sleep } from '@midwayjs/core'; + +//... omit describe + +it('should test create websocket app', async () => { + + // Create a service + const app = await createApp(); + + // Create a client + const client = await createWebSocketClient('ws://localhost:3000'); + + const result = await new Promise(resolve => { + + client.on('message', (data) => { + // xxxx + resolve(data); + }); + + // Send event + client.send(1); + + }); + + // Judgment result + expect(JSON.parse(result)).toEqual({ + name: 'harry', + result: 6 + }); + + await sleep(1000); + + // Close the client + await client.close(); + + // Close the server + await close(app); + +}); +``` + + +Use the `once` method of the `events` module that comes with node to optimize the code. +```typescript +import { sleep } from '@midwayjs/core'; +import { once } from 'events'; +import { createApp, close, createWebSocketClient } from '@midwayjs/mock'; + +//... omit describe + +it('should test create websocket app', async () => { + + // Create a service + const app = await createApp(process.cwd()); + + // Create a client + const client = await createWebSocketClient('ws://localhost:3000'); + + // Send event + client.send(1); + + // Monitor with promise writing of events + let gotEvent = once(client, 'message'); + // Waiting for return + let [data] = await gotEvent; + + // Judgment result + expect(JSON.parse(data)).toEqual({ + name: 'harry', + result: 6 + }); + + await sleep(1000); + + // Close the client + await client.close(); + + // Close the server + await close(app); +}); + +``` +The two writing methods have the same effect, just write as you understand. + + + +## Configuration + +## Default configuration + +The configuration sample of `@midwayjs/ws` is as follows: + +```typescript +// src/config/config.default +export default { + // ... + webSocket: { + port: 7001 + }, +} +``` + +When `@midwayjs/ws` and other `@midwayjs/web`, `@midwayjs/koa`, `@midwayjs/express` are enabled at the same time, ports can be reused. + +```typescript +// src/config/config.default +export default { + // ... + koa: { + port: 7001 + } + webSocket: { + // No configuration here + }, +} +``` + + + +| Property | Type | Description | +| --- | --- | --- | +| port | number | Optionally, if the port is passed, ws will create an HTTP service for the port. If you want to work with other midway web frameworks, do not pass this parameter. | +| server | httpServer | Optional, when passing port, you can specify an existing webServer | + +For more information about startup options, see [ws documentation](https://github.com/websockets/ws). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/alias_path.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/alias_path.md new file mode 100644 index 000000000000..270ab10041c3 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/alias_path.md @@ -0,0 +1,68 @@ +# About Alias Path + +We do not recommend using Alias Path, Node and TS that do not support this function natively. Even if they do, they are now implemented through various Hack methods (starting from v18, Node.js already has a exports scheme, but the type is not supported yet, so you can wait for it later). + +If you must want to use it, please look down. + +## Support for Local Development (dev Phase) + +Tsc does not convert the module path of import when compiling ts into js, so when you configure paths in `tsconfig.json`, if you use paths in ts and import the corresponding module, there is a high probability that the module cannot be found when compiling js. + +The solution is to either use paths, or use paths to import some declarations instead of specific values, or use [tsconfig-paths](https://github.com/dividab/tsconfig-paths) to hook out the module path resolution logic in node to support paths in `tsconfig.json`. + +```bash +$ npm i tsconfig-paths --save-dev +``` + +The use tsconfig-paths can be introduced in `src/configuration.ts`. + +```typescript +// src/configuration.ts + +import 'tsconfig-paths/register'; +// ... +``` + +:::info + +The above method will only take effect for dev phase (ts-node). + +::: + + + +## test support (jest test) + +In the test, due to Jest's special environment, alias needs to be processed again. `moduleNameMapper` functions in Jest's configuration file can be used to replace the loaded modules to realize alias functions in disguise. + +```typescript +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'] + coveragePathIgnorePatterns: ['/test/'] + moduleNameMapper: { + '^@/(.*)$': '/src/$1' + } +}; +``` + +Note that the alias prefix used here is the @symbol. If it is another alias name, please modify it yourself. + + + +## Runtime support + +`tsconfig-paths` replace paths in memory after ts runs. After compilation, paths with @symbols will still be output, so that files cannot be found after deployment. Some libraries in the community will do some replacement support in ts compilation. + +For example: + +- https://github.com/justkey007/tsc-alias + + + +## Other + +An mwcc compiler is embedded in the old version CLI, which replaces Alias content in the builder based on the fixed TS version. However, due to the dependency of TS private API, the TS version cannot be upgraded and the functions of the new version cannot be enjoyed. + +We removed this compiler from the CLI 2.0 version. \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/framework_problem.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/framework_problem.md new file mode 100644 index 000000000000..ec5f0ef28e03 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/framework_problem.md @@ -0,0 +1,72 @@ +# Frequently Asked Framework Issues + +## Multiple @midwayjs/core warnings + +`@midwayjs/core` Package Generally speaking, npm will allow the same dependency to have an instance in the node_modules, and the rest of the modules will be linked to the node_modules/@midwayjs/core through a soft link. + + +In the following command, `npm ls` lists the dependency trees of a package under the project. +```bash +$ npm ls @midwayjs/core +``` +The ratio is shown in the following figure. +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01Td86gC1tQsKjRB8XU_!!6000000005897-2-tps-541-183.png) +The gray `deduped` means that the package is linked to the same module by npm soft, which is normal. + + +Let's look at the problematic examples. +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01gsnexD1i6lA7kM48q_!!6000000004364-2-tps-1010-308.png) + + +This is a lerna project. The decorator package in the bottom demo-docs is not **deduped** marked at the back, indicating that this package exists independently and is wrong. + + +According to this idea, we can gradually investigate why this happened. + + +For example, the above figure may be npm install used in a single module instead of lerna installation. + + +We can gradually investigate according to the following ideas: + + +- 1. Contains different versions of decorator packages (for example, package-lock lock packages, or depend on hard-coded versions) +- 2. The hoist mode of lerna is not used correctly (for example, the above figure may be the npm install used in a single module instead of lerna installation) + + + +## xxx is not valid in current context + + +This is when the class associated with an attribute in the dependency injection container cannot be found in the dependency injection container. This error may be recursive and deeper. + +For example: +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01sTvqNX1NiDcoiyS2a_!!6000000001603-2-tps-1053-141.png) +The core of the error is the first attribute, which cannot be found in a class. + + +For example, the core of the above figure is `packageBuildInfoHsfService` this injected class cannot be found. +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01BBe4gu1KHhqnT0S75_!!6000000001139-2-tps-765-166.png) +At this time, you need to go to the corresponding class to see if the provide name has been customized. + + +Common problems are: + +- 1. The name exported by the Provide decorator is incorrect and cannot correspond to the attribute. +- 2. If the Provide is empty, the high probability is that the case is not written correctly. +- 3. If the injection is a component, the component name may be missing. + + +Simple solution: The `@Inject` decorator does not add parameters, and the property definition is clearly written to the class, so that the midway can automatically find the corresponding class and inject it (not applicable to polymorphisms). +```typescript +@Inject() +service: PackageBuildInfoHsfService; +``` + +## TypeError: (0 ,decorator_1.Framework) is not a function + +The reason is that the wrong version is used, such as the lower version of the framework and the higher version of the component (the 2.x framework uses the 3.x component). + +![](https://img.alicdn.com/imgextra/i3/O1CN01G7gzCj1EkCpW1gaJl_!!6000000000389-2-tps-1461-491.png) + +Solution: Confirm your large version of the framework (@midwayjs/core version is the framework version), select the corresponding document, and use the corresponding component. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/git_problem.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/git_problem.md new file mode 100644 index 000000000000..4202ee73a9f4 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/git_problem.md @@ -0,0 +1,77 @@ +# Common git problems + +## File name case problem + + +Because git is not sensitive to case by default, if the file name is changed from small to uppercase, the file cannot be found to be changed and not submitted to the warehouse. + + +What's more frightening is that mac is also case-insensitive, and it often happens that it can run locally and execute errors when it goes to the server. + + +For this reason, we 'd better turn off the default case of git. + + +The following command. + +```bash +$ git config core.ignorecase false ## takes effect for the current project +$ git config --global --add core.ignorecase false ## takes effect globally +``` + + +## Line wrapping problem under windows + + +When creating or cloning code on the Windows, developing or submitting code, the following errors may occur: + +``` +Delete '␍'eslint(prettier/prettier) +``` + +The reasons are as follows: + + +Due to historical reasons, the line breaks of text files under windows and linux are inconsistent. + + +- Windows use both the carriage return CR(carriage-return character) and the line feed LF(linefeed character) when wrapping lines. +- Mac and Linux systems, on the other hand, use only the line break LF. +- The old version of the Mac system used the carriage return CR. + + + + +| Windows | Linux/Mac | Old Mac(pre-OSX | +| --- | --- | --- | +| CRLF | LF | CR | +| '\n\r' | '\n' | '\r' | + +Therefore, incompatibility problems occur when text files are created and used under different systems. + + +The solution is as follows: + + +Set global git text line breaks +```bash +$ git config --global core.autocrlf false +``` +Note: After git global configuration, you need to pull the code again. + +If you are using the vscode editor, the solution is as follows: + + +In the lower-right corner of the editor, manually change `CRLF` to `LF` + +This method can only modify the line break of the current file, using vscode to create a new file line break is also `CRLF`, you can add the following configuration in `settings.json` + +``` +"files.eol": "\n", +``` + + +Reference: + +- [Delete `␍` eslint(prettier/prettier) Error Solution](https://juejin.cn/post/6844904069304156168) +- [Configure a Git processing line terminator](https://docs.github.com/cn/github/getting-started-with-github/configuring-git-to-handle-line-endings) diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/npm_problem.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/npm_problem.md new file mode 100644 index 000000000000..39b7b0860043 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/npm_problem.md @@ -0,0 +1,45 @@ +# Common npm problems + +## 1. Do not want to generate package-lock.json + + +In some cases, the lock version is not particularly easy to use, but there will be many strange problems. We will disable npm's function of generating `package-lock.json` files. + + +You can enter the following command. +```bash +$ npm config set package-lock false +``` + +## 2. Maximum call stack size exceeded to report an error + + +Generally, after npm install, npm install a package. + + +Solution: + + +- 1. Delete node_modules +- 2. Delete package-lock.json +- 3. Re-npm install + + + +If there are still problems, you can try to try again using node v14/npm6. + + +## 3. Python/Canvas reported an error + + +Appears when installing jest module using node v15/npm7. + + +For example: +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01fctCcQ2191p8aMfDd_!!6000000006941-2-tps-1623-295.png) + + +Solution: Add the `--legacy-peer-deps` parameter to npm I. + + +Reason: The test framework Jest relies on jsdom,npm7 will automatically install the canvas package that its peerDependencies depends on, and the installation and compilation of canvas requires a python3 environment. \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/ts_problem.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/ts_problem.md new file mode 100644 index 000000000000..39c50fe6fbbc --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/faq/ts_problem.md @@ -0,0 +1,106 @@ +# Common TS questions + + +TS has many compilation static checks, such as inconsistent types and undefined objects, which are the best by default. It is hoped that users can reasonably consider coding styles and habits, switch configurations carefully, and enjoy the benefits of TS static checks. + + +## Dependency package definition error + + +If the TS version of the dependency package and the project itself are inconsistent, an error will occur at compile time. + + +You can turn off dependency package checking at `tsconfig.json`. + +```typescript +{ + "compilerOptions": { + "skipLibCheck": true + }, +} +``` + + +## TS2564 initialization unassigned error + + +The error is as follows: + +```yaml +error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor. +``` +The reason is that the initialization attribute check of TS is enabled. If there is no initialization assignment, an error will be reported. + + +Treatment method: + + +The first: remove the check rule of tsconfig.json + +```json +{ + "strictPropertyInitialization": false // or remove +} +``` + +The second type: attribute plus exclamation mark + +```typescript +export class HomeController { + @Inject() + userService! : UserService; +} +``` + + +## TS6133 Object Declaration Not Used Error + + +The error is as follows: +```yaml +error TS6133: 'app' is declared but its value is never read. +``` +The reason is that the object with TS turned on is not checked. If it is declared but not used, an error will be reported. + + +Treatment method: + + +The first: remove undefined variables + + +The second: remove tsconfig.json's inspection rules +```json +{ + "compilerOptions": { + "noUnusedLocals": false + }, +} +``` + + +## The typings defined in the tsconfig does not take effect + + +In tsconfig.json, if the typeRoots is defined and the include is defined, if the include does not contain the content in the typeRoot, an error will be reported in dev/build. + + +This is a ts/ts-node problem. For issue, see [#782](https://github.com/TypeStrong/ts-node/issues/782) and [#22217](https://github.com/microsoft/TypeScript/issues/22217). + + +For example: +```json +"typeRoots": [ + "./node_modules/@types ", + "./typings" +], +"include": [ + "src ", + "typings" +], +"exclude": [ + "dist ", + "node_modules" +], +``` +As mentioned above, if the typings is not written in the include, the definition cannot be found in dev/build and an error will be reported. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/guard.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/guard.md new file mode 100644 index 000000000000..da2ab7c30c2f --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/guard.md @@ -0,0 +1,248 @@ +# Guards + +Starting from v3.6.0, Midway provides guard capability. + +The guard determines whether a given request is handled by the routing handler based on certain conditions that appear at runtime (such as permissions, roles, access control lists, etc.). + +In ordinary applications, these logics are usually processed in the middleware, but the logic of the middleware is too common, and it cannot be combined with routing methods gracefully. For this reason, we have designed guards after the middleware and before entering the routing method, which can facilitate method authentication and other processing. + +For the following code, we will take `@midwayjs/koa` as an example. + + + +## Write guards + + +In general, you can write a guard in the `src/guard` folder. + + +Create a `src/guard/auth.guard.ts` to verify whether the route can be accessed by the user. + +``` +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ ├── user.controller.ts +│ │ └── home.controller.ts +│ ├── interface.ts +│ ├── guard +│ │ └── auth.guard.ts +│ └── service +│ └── user.service.ts +├── test +├── package.json +└── tsconfig.json +``` + + +Midway uses the `@Guard` decorator to identify the guard. The sample code is as follows. + + +```typescript +import { IMiddleware, Guard, IGuard } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Guard() +export class AuthGuard implements IGuard { + async canActivate(context: Context, suppilerClz, methodName: string): Promise { + // ... + } +} +``` + +`canActivate` method is used to verify whether subsequent methods can be accessed in the request. When true is returned, subsequent methods will be executed. When false is `canActivate`, 403 error codes will be thrown. + + + +## Use guards + +Guards can be applied to different frameworks. In http, they can be applied globally, to Controllers, and to methods. In other Framework implementations, they can only be used on methods. + + + +### Routing guard + +After writing the guard, we need to apply it to each controller route. + +Using `UseGuard` decorators, we can apply them to classes and methods. + +```typescript +import { Controller } from '@midwayjs/core'; +import { AuthGuard } from '../guard/auth.guard'; + +@UseGuard(AuthGuard) +@Controller('/') +export class HomeController { + +} +``` + + +Apply guards on methods. + +```typescript +import { Controller, Get } from '@midwayjs/core'; +import { ReportMiddleware } from '../middleware/report.middlweare'; +import { AuthGuard } from '../guard/auth.guard'; + +@Controller('/') +export class HomeController { + + @UseGuard(AuthGuard) + @Get('/', { middleware: [ ReportMiddleware ]}) + async home() { + } +} +``` + +You can also pass in arrays. + +```typescript +@UseGuard([AuthGuard, Auth2Guard]) +``` + + + +### Global guard + + +We need to join the guard list of the current framework before the application starts. `useGuard` method, we can add the guard to the guard list. + +```typescript +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { AuthGuard } from './guard/auth.guard'; + +@Configuration({ + imports: [koa] + // ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + this.app.useGuard(AuthGuard); + } +} +``` + +In the same way, multiple guards can be added. + +```typescript +async onReady() { + this.app.useGuard([AuthGuard, Auth2Guard]); +} +``` + + + +## Custom error + +By default, when the guard's `canActivate` method returns false, the framework throws a 403 error (`ForbiddenError` ). + +You can also decide on your own in the guard the errors that need to be thrown. + +```typescript +import { IMiddleware, Guard, IGuard, httpError } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Guard() +export class AuthGuard implements IGuard { + async canActivate(context: Context, suppilerClz, methodName: string): Promise { + // ... + if (methodName ==='xxx') { + throw new httpError.ForbiddenError(); + } + + return true; + } +} +``` + +:::tip + +Note that the global error handler will also intercept errors thrown by guards. + +::: + + + +## Difference from middleware + +Guards will be executed **after** the global middleware and **before** the business logic of the routing method. + +Middleware generally writes general processing logic, such as login, user identification, security verification, etc., while guards are more suitable for routing-based permission control because they are inside the routing. + +Although there is routing information in the middleware, it is impossible to clearly know which actual routing controller is entered (unless additional query matching), while guards have entered the routing method, which has a relatively large advantage in performance. + + + +## Example of Role-Based Authentication + +In general, we associate method access with roles, and let's simply implement a user role-based access control. + +First, we define a `@Role` decorator to set the access permissions of the method. + +```typescript +// src/decorator/role.decorator.ts +import { savePropertyMetadata } from '@midwayjs/core'; + +export const ROLE_META_KEY = 'role:name' + +export function Role(roleName: string | string[]): MethodDecorator { + return (target, propertyKey, descriptor) => { + roleName = [].concat(roleName); + // Save only metadata + savePropertyMetadata(ROLE_META_KEY, roleName, target, propertyKey); + }; +} +``` + +Write a guard for role authentication. + +```typescript +import { IMiddleware, Guard, IGuard, getPropertyMetadata } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { ROLE_META_KEY } from '../decorator/role.decorator.ts'; + +@Guard() +export class AuthGuard implements IGuard { + async canActivate(context: Context, supplierClz, methodName: string): Promise { + // Get role information from class metadata + const roleNameList = getPropertyMetadata(ROLE_META_KEY, supplierClz, methodName); + if (roleNameList && roleNameList.length && context.user.role) { + // Assume that the middleware has already obtained the user role information and saved it to context.user.role + // Directly determine whether to change the role + return roleNameList.includes(context.user.role); + } + + return false; + } +} +``` + +Use this guard on the route. + +```typescript +import { Controller, Get } from '@midwayjs/core'; +import { ReportMiddleware } from '../middleware/report.middlweare'; +import { AuthGuard } from '../guard/auth.guard'; + +@UseGuard(AuthGuard) +@Controller('/user') +export class HomeController { + + // Only admin access is allowed + @Role(['admin']) + @Get('/getUserRoles') + async getUserRoles() { + // ... + } +} +``` + +Only when `ctx.user.role` returns `admin` is allowed to access the `/getUserRoles` route. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/api.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/api.md new file mode 100644 index 000000000000..480ea9acfb7d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/api.md @@ -0,0 +1,506 @@ +# API Development + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Routing + +In Midway Hooks, you can quickly create interfaces through the `Api()` function provided by `@midwayjs/hooks`. + +Hello World example: + +```ts title="/src/hello.ts" +import { + Api + Get +} from '@midwayjs/hooks'; + +export default Api ( + Get(), // Http Path: /api/hello + async () => { + return 'Hello World!'; + } +); +``` + +An API interface consists of the following parts: + +- `Api()`: defines an API function. +- `Get(path?: String)`: specifies the Http trigger, the request method is set to GET, and the optional `path` is the path of the interface. If you do not specify a path, the path is generated based on the `function name and file name`. By default, the path is prefixed with `/API`. +- `Handler: async (...args: any[]) => { ... }`: user logic, processes requests and returns results + +You can also specify the path, as shown in the following example. + +```ts title="/src/hello.ts" +import { + Api + Get +} from '@midwayjs/hooks'; + +export default Api ( + Get('/hello'), // Http Path: /hello + async () => { + return 'Hello World!'; + } +); +``` + +## Request context (Context / Request / Response) + +You can get the request context object through the `useContext` provided by `@midwayjs/hooks`. + +Taking [Koa](https://koajs.com/) framework as an example, the `useContext` will return Koa's [Context](https://koajs.com/#context) object. + +Basic example: + +1. Get the request Method and Path + +```ts +import { + Api + Get + useContext +} from '@midwayjs/hooks'; +import { Context } from '@midwayjs/koa'; + +export default Api(Get(), async () => { + const ctx = useContext(); + return { + method: ctx.method + path: ctx.path + }; +}); +``` + +2. Set the returned Header + +```ts +import { + Api + Get + useContext +} from '@midwayjs/hooks'; + +export default Api(Get(), async () => { + const ctx = useContext(); + ctx.set('X-Powered-By', 'Midway'); + return 'Hello World!'; +}); +``` + +At the same time, we can also set Header by `SetHeader()`. + +## Http trigger + +| Trigger | Notes | +| ------------------------ | --------------------------- | +| `All(path?: string)` | Accept all Http Method requests | +| `Get(path?: string)` | Accept GET request | +| `Post(path?: string)` | Accept POST request | +| `Put(path?: string)` | Accept PUT request | +| `Delete(path?: string)` | Accept DELETE request | +| `Patch(path?: string)` | Accept PATCH request | +| `Head(path?: string)` | Accept HEAD request | +| `Options(path?: string)` | Accept OPTIONS request | + +## Request + +### Pass parameter Data + +In Midway Hooks, the input parameter of the interface is the parameter that declares the function. + +The basic example is as follows: + +```ts +import { + Api + Post +} from '@midwayjs/hooks'; + +export default Api ( + Post(), // Http Path: /api/say + async (name: string) => { + return 'Hello ${name}!'; + } +); +``` + +You can call the interface in two ways. + +1. Full stack project: based on zero Api, import interface and call +2. Manual call: Use fetch to `Handler(...args: any[])` input parameters under Http, and you can pass parameters by setting the args parameter of Http Body during manual request. + + + + +```ts +import say from './api'; + +const response = await say('Midway'); +console.log(response); // Hello Midway! +``` + + + + + +```ts +fetch('/api/say', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + args: ['Midway'] + }), +}) + .then((res) => res.text()) + .then((res) => console.log(res)); // Hello Midway! +``` + + + + +### Query parameter Query + +You can use the `Query` parameter to pass the parameter to the URL. + +If you want the interface path to be `/articles? Page = 0 & limit = 10`, you can write like this. + +```ts +import { + Api + Get + Query + useContext +} from '@midwayjs/hooks'; + +export default Api ( + Get() + Query< { + page: string; + limit: string; + }>(), + async () => { + const ctx = useContext(); + return { + page: ctx.query.page + limit: ctx.query.limit + }; + } +); +``` + +Front-end call + + + + +```ts +import getArticles from './api'; +const response = await getArticles({ + query: { page: '0', limit: '10'} +}); +console.log(response); // { page: '0', limit: '10'} +``` + + + + + +```ts +fetch('/api/articles?page=0&limit=10') + .then((res) => res.json()) + .then((res) => console.log(res)); // { page: '0', limit: '10'} +``` + + + + +### Path parameter Params + +Path parameters can realize the functions of dynamic paths and obtaining parameters from paths. When you use this feature, you must manually set the path and use `Params` to declare the type. + +If you want the interface path to be `/article/100` and get a value with id `100`, you can write as follows: + +```ts +import { + Api + Get + Params + useContext +} from '@midwayjs/hooks'; + +export default Api ( + Get('/article/:id') + Params<{ id: string }>() + async () => { + const ctx = useContext(); + return { + article: ctx.params.id + }; + } +); +``` + +Front-end call + + + + +```ts +import getArticle from './api/article'; +const response = await getArticle({ + params: { id: '100'} +}); +console.log(response); // { article: '100'} +``` + + + + + +```ts +fetch('/article/100') + .then((res) => res.json()) + .then((res) => console.log(res)); // { article: '100'} +``` + + + + +### Request header Headers + +The request header can realize the function of passing parameters through Http Headers. When using this function, the type must be declared by `Headers`. + +If you want to request `/auth` and pass token in the `Request Headers`, you can write as follows: + +```ts +import { + Api + Get + Headers + useContext +} from '@midwayjs/hooks'; + +export default Api ( + Get('/auth') + Headers<{ token: string }>() + async () => { + const ctx = useContext(); + return { + token: ctx.headers.token + }; + } +); +``` + +Front-end call + + + + +```ts +import getAuth from './api/auth'; +const response = await getAuth({ + headers: { token: '123456'} +}); +console.log(response); // { token: '123456'} +``` + + + + + +```ts +fetch('/auth', { + headers: { + token: '123456', + }, +}) + .then((res) => res.json()) + .then((res) => console.log(res)); // { token: '123456'} +``` + + + + +## Response Response + +### Status code HttpCode + +`HttpCode(status: number)` is supported. + + + + +```ts +import { + Api + Get + HttpCode +} from '@midwayjs/hooks'; + +export default Api ( + Get() + HttpCode(201) + async () => { + return 'Hello World!'; + } +); +``` + + + + + +```ts +import { + Api + Get + useContext +} from '@midwayjs/hooks'; + +export default Api(Get(), async () => { + const ctx = useContext(); + ctx.status = 201; + return 'Hello World!'; +}); +``` + + + + +### Response header SetHeader + +`SetHeader(key: string, value: string)` + + + + +```ts +import { + Api + Get + SetHeader +} from '@midwayjs/hooks'; + +export default Api ( + Get() + SetHeader('X-Powered-By', 'Midway') + async () => { + return 'Hello World!'; + } +); +``` + + + + + +```ts +import { + Api + Get + useContext +} from '@midwayjs/hooks'; + +export default Api(Get(), async () => { + const ctx = useContext(); + ctx.set('X-Powered-By', 'Midway'); + return 'Hello World!'; +}); +``` + + + + +### Redirect Redirect + +Support: `Redirect(url: string, code?: number = 302)` + + + + +```ts +import { + Api + Get + Redirect +} from '@midwayjs/hooks'; + +export default Api ( + Get('/demo') + Redirect('/hello') + async () => {} +); +``` + + + + + +```ts +import { + Api + Get + useContext +} from '@midwayjs/hooks'; + +export default Api ( + Get('/demo') + async () => { + const ctx = useContext(); + ctx.redirect('/hello'); + } +); +``` + + + + +### Return value type ContentType + +Supported: `ContentType(type: string)`. + + + + +```ts +import { + Api + Get + ContentType +} from '@midwayjs/hooks'; + +export default Api ( + Get() + ContentType('text/html') + async () => { + return '

Hello World!

'; + } +); +``` + +
+ + + +```ts +import { + Api + Get + ContentType +} from '@midwayjs/hooks'; + +export default Api ( + Get() + ContentType('text/html') + async () => { + return '

Hello World!

'; + } +); +``` + +
+
diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/builtin-hooks.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/builtin-hooks.md new file mode 100644 index 000000000000..c2139704427f --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/builtin-hooks.md @@ -0,0 +1,104 @@ +# Hooks + +Midway Hooks can use the `Hooks` function to obtain the runtime context. + +## Grammar + +Hooks needs to be used in the Api interface. + +Effective examples: + +```ts +import { + Api + Get + useContext +} from '@midwayjs/hooks'; +import { Context } from '@midwayjs/koa'; + +export default Api(Get(), async () => { + const ctx = useContext(); + console.log(ctx.method); + // ... +}); +``` + +Invalid example: + +```ts +import { useContext } from '@midwayjs/hooks'; + +const ctx = useContext(); // will throw error +``` + +## Hooks Supported + +### useContext + +The `useContext()` function will return the context related to this request, and the `Context` returned is determined by the framework used at the bottom. + +Taking [Koa](https://koajs.com/) framework as an example, the `useContext` will return Koa's [Context](https://koajs.com/#context) object. + +Take obtaining the request Method and Path as an example. + +```ts +import { + Api + Get + useContext +} from '@midwayjs/hooks'; +import { Context } from '@midwayjs/koa'; + +export default Api(Get(), async () => { + const ctx = useContext(); + return { + method: ctx.method + path: ctx.path + }; +}); +``` + +You can label the type of the current context by generics. + +```ts +// Koa +import { Context } from '@midwayjs/koa'; +const ctx = useContext(); + +// FaaS +import { Context } from '@midwayjs/faas'; +const ctx = useContext(); +``` + +## Create reusable Hooks + +You can create reusable Hooks for use in multiple interfaces. + +```ts +import { + Api + Get + useContext +} from '@midwayjs/hooks'; +import { Context } from '@midwayjs/koa'; + +function useIp() { + const ctx = useContext(); + return ctx.ip; +} + +export default Api(Get(), async () => { + const ip = useIp(); + return { + ip + }; +}); +``` + +Integrated call: + +```ts +import getIp from './api'; +const { ip } = await getIp(); +console.log(ip); // 127.0.0.1 +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/client.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/client.md new file mode 100644 index 000000000000..6b65921bdb6d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/client.md @@ -0,0 +1,166 @@ +# Front-end request client + +In Midway Hooks' full stack application, we use `@midwayjs/rpc` as the default request client. All generated interfaces call the server through `@midwayjs/rpc`. + +## Configuration + +`@midwayjs/rpc` provides `setupHttpClient` methods to configure the requesting client (📢The `setupHttpClient` should be placed at the entrance of the front-end code.). + +The supported configuration items are as follows: + +```ts +type SetupOptions = { + baseURL?: string; + withCredentials?: boolean; + fetcher?: Fetcher; + middleware?: Middleware[]; +}; + +type Fetcher = ( + req: HttpRequestOptions + options: SetupOptions +) => Promise; + +type Middleware = ( + ctx: Context + next: () => Promise +) => void; + +type Context = { + req: HttpRequestOptions; + res: any; +}; + +type HttpRequestOptions = { + url: string; + method: HttpMethod; + data ?: { + args: any[]; + }; + + // query & headers + query?: Record; + headers?: Record; +}; +``` + +### baseURL: string + +The basic URL of the request. Default value:`/`. + +```ts +import { setupHttpClient } from '@midwayjs/rpc'; + +setupHttpClient({ + baseURL: + process.env.NODE_ENV === + 'development' + ? 'http://localhost:7001' + : 'https://api.example.com', +}); +``` + +### withCredentials: boolean + +Default value: `false`. For more information, see [MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/withCredentials). + +```ts +import { setupHttpClient } from '@midwayjs/rpc'; + +setupHttpClient({ + withCredentials: true +}); +``` + +### fetcher: Fetcher + +`@midwayjs/rpc` uses [redaxios](https://github.com/developit/redaxios) as the request client by default, a mini client that follows the axios api. + +By setting the `fetcher`, you can replace the default requesting client. In this example, `axios` is used as the default request client. + +```ts +import axios from 'axios'; +import { setupHttpClient } from '@midwayjs/rpc'; +import type { Fetcher } from '@midwayjs/rpc'; + +const fetcher: Fetcher = async ( + req + options +) => { + const response = await axios({ + method: req.method + url: req.url + data: req.data + params: req.query + headers: req.headers + baseURL: options.baseURL + withCredentials: + options.withCredentials + }); + return response.data; +}; + +setupHttpClient({ fetcher }); +``` + +### middleware: Middleware [] + +In `@midwayjs/rpc`, we can set up middleware for printing parameters, return value handling errors, etc. + +Take printing the address and return value of the current request as an example: + +```ts +import { setupHttpClient } from '@midwayjs/rpc'; +import type { Middleware } from '@midwayjs/rpc'; + +const logger: Middleware = async ( + ctx + next +) => { + console.log('<-- ${ctx.req.url}'); + await next(); + console.log ( + '--> ${ctx.req.url} ${ctx.res}' + ); +}; + +setupHttpClient({ + middleware: [logger] +}); +``` + +You can also use it to handle errors uniformly: + +When using the default `fetcher`, the `err` type refers to [Axios Response Schema](https://axios-http.com/docs/res_schema). + +```ts +import { setupHttpClient } from '@midwayjs/rpc'; +import type { Middleware } from '@midwayjs/rpc'; + +const ErrorHandler: Middleware = async ( + ctx + next +) => { + try { + await next(); + } catch (err) { + switch (err.status) { + case 401: + location.href = '/login'; + break; + case 500: + alert('Internal Server Error'); + break; + default: + alert ( + 'Unknown Error, status: ${err.status}' + ); + break; + } + } +}; + +setupHttpClient({ + middleware: [ErrorHandler] +}); +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/component.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/component.md new file mode 100644 index 000000000000..5f0b9ff18616 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/component.md @@ -0,0 +1,59 @@ +# Using Midway Components + +Midway provides a series of components, including Cache / Http / Redis, etc. +In Midway Hooks, we can directly use Midway components to quickly implement functions. + +## Introducing components + +Midway Hooks uses `createConfiguration()` in `configuration.ts` to configure the project, and its Api is consistent with the `@Configuration()` provided by `@midwayjs/core`. + +Take the `@midwayjs/cache` component as an example: + +```ts +import { + createConfiguration, + hooks +} from '@midwayjs/hooks'; +import * as Koa from '@midwayjs/koa'; +import { join } from 'path'; +import * as cache from '@midwayjs/cache'; + +export default createConfiguration({ + imports: [cache, Koa, Hooks()], + importConfigs: [ + join(__dirname, 'config') + ], +}); +``` + +You can import components through `imports` and `importConfigs` configuration files. + +## Use components + +In `@midwayjs/cache`, `CacheManager` classes are provided to operate the cache. + +In Midway Hooks, you can get instances of classes at runtime through the `useInject(class)` provided by `@midwayjs/hooks`. + +```ts +import { + Api, + Get, + useInject +} from '@midwayjs/hooks'; +import { CacheManager } from '@midwayjs/cache'; + +export default Api(Get(), async () => { + const cache = await useInject ( + CacheManager + ); + + await cache.set('name', 'Midway'); + const result = await cache.get ( + 'name' + ); + + return 'Hello ${result}!'; +}); +``` + +The `useInject(CacheManager)` here is the same as the function of `@Inject() cache: CacheManager`. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/config.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/config.md new file mode 100644 index 000000000000..2ded53daf4d8 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/config.md @@ -0,0 +1,40 @@ +# Project configuration + +You can configure the project by using the `midway.config.ts` parameter in the root directory of the project. + +> If it is a pure interface project, because the configuration needs to be read in the build environment, please use the JavaScript, the configuration file name is `midway.config.js` + +## source: string + +Configure the backend root directory. Default value: `./src` for pure service interfaces. Default value: `./src/api` for full-stack applications. + +## routes: RouteConfig [] + +Enable file system routing and configuration, the default is `undefined`. For more information about the format, see [Simple Mode & File System Routing](./file-route). + +## dev.ignorePattern: IgnorePattern + +When configuring a full stack application, which requests developed locally should be ignored and not processed by the server. + +## build.outDir: string + +Configure the output directory of the full stack application. Default value: `./dist`. + +## vite: ViteConfig + +`Import {defineConfig} from' @midwayjs/hooks-kit '`only. + +Configure Vite for full-stack applications. For more information, see [Vite](https://vitejs.dev/config/). + +Examples: + +```ts +import react from '@vitejs/plugin-react'; +import { defineConfig } from '@midwayjs/hooks-kit'; + +export default defineConfig({ + vite: { + plugins: [react()] + }, +}); +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/cors.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/cors.md new file mode 100644 index 000000000000..8bd1fae068e0 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/cors.md @@ -0,0 +1,54 @@ +# Cross-domain CORS + +In Midway Hooks, you can use [@koa/cors](https://github.com/koajs/cors) to configure the cross-border function. + +## Usage + +Install the `@koa/cors` dependency. + +``` +npm install @koa/cors +``` + +Enable `@koa/cors` middleware in `configuration.ts`. + +```ts +import { + createConfiguration + hooks +} from '@midwayjs/hooks'; +import * as Koa from '@midwayjs/koa'; +import cors from '@koa/cors'; + +export default createConfiguration({ + imports: [ + Koa + hooks({ + // highlight-start + middleware: [ + cors({ origin: '*' }) + ], + // highlight-end + }), + ], +}); +``` + +The following [Configuration Items](https://github.com/koajs/cors#corsoptions) are supported: + +```javascript +/** + * CORS middleware + * + * @param {Object} [options] + * - {String|Function(ctx)} origin 'Access-Control-Allow-Origin', default is request Origin header + * - {String|Array} allowMethods 'Access-Control-Allow-Methods', default is 'GET,HEAD,PUT,POST,DELETE,PATCH' + * - {String|Array} exposeHeaders 'Access-Control-Expose-Headers' + * - {String|Array} allowHeaders 'Access-Control-Allow-Headers' + * - {String|Number} maxAge 'Access-Control-Max-Age' in seconds + * - {Boolean|Function(ctx)} credentials 'Access-Control-Allow-Credentials', default is false. + * - {Boolean} keepHeadersOnError Add set headers to 'err.header' if an error is thrown + * @return {Function} cors middleware + * @api public + */ +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/debug.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/debug.md new file mode 100644 index 000000000000..cec001610eb6 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/debug.md @@ -0,0 +1,30 @@ +# Debug + +Thanks to the support of the editor, we can quickly debug the application locally. + +## VSCode + +### JavaScript Debug Terminal + +Create a JavaScript Debug Terminal in VSCode. + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/98602/1622789601759-d2634846-49f7-4487-be6f-0dc9e5f80082.png#clientId=u3a1b2f6d-ebe0-4&from=paste&height=192&id=p5BOe&margin=%5Bobject%20Object%5D&name=image.png&originHeight=192&originWidth=375&originalType=binary&size=31856&status=done&style=none&taskId=u7286159b-9369-4d17-8a6a-c43a6f52556&width=375) + + +Run a command (such as `npm start`) on the command line to automatically enable debugging mode. + +### Debug Scripts + +Open the `package.json` and view the `debug` button above the `scripts`. + +![](https://cdn.nlark.com/yuque/0/2021/png/98602/1622789617835-64b2099a-6b94-41c4-81fa-4f0bb0763ebb.png#clientId=u7ee4f0d0-4c66-4&from=paste&height=225&id=u459844f5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=225&originWidth=565&originalType=binary&size=26636&status=done&style=none&taskId=u3838b111-c93e-41e0-81ce-01c1bdd6ad4&width=565) + +Select the `start` command to start the debugging mode. + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/98602/1622789623261-57851b50-421e-45fa-9dd9-95ac7d48776e.png#clientId=u7ee4f0d0-4c66-4&from=paste&height=170&id=ue315d401&margin=%5Bobject%20Object%5D&name=image.png&originHeight=170&originWidth=427&originalType=binary&size=19905&status=done&style=none&taskId=u8b079aa2-8376-4014-b48b-ed27ef66da6&width=427) + +## Jetbrains (WebStorm/IDEA...) + +Open the `package.json`, select the `scripts` you want to execute, and click the `debug` button to start local debugging. + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/98602/1622789628840-eb403a2a-a864-4fd6-8f57-3f576c9b3417.png#clientId=u7ee4f0d0-4c66-4&from=paste&height=176&id=uc2a06ce8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=176&originWidth=548&originalType=binary&size=28656&status=done&style=none&taskId=ucb4c5c34-6e56-47c9-a724-4ed700dce9d&width=548) diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/deploy.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/deploy.md new file mode 100644 index 000000000000..18b1dc5c024c --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/deploy.md @@ -0,0 +1,142 @@ +# Deployment + +Midway Hooks supports Api Server and integration. + +## Api Server deployment + +For more information about how to deploy Api Server, see [Start and deploy Api Server](/docs/deployment). + +If you use a single file deployment, you can refer to the example: [hooks-api-bundle-starter](https://github.com/midwayjs/hooks/blob/main/examples/api-bundle/readme.md) + +## Integrated deployment + +The integrated construction product includes the front and back ends, which can be divided into the following categories according to the difficulty of deployment. + +- The front and back ends are deployed on the same server, and the back end hosts HTML & static resources & provides interfaces +- Static resources are deployed to CDN, backend hosting HTML & providing interfaces +- Static resources are deployed to CDN,HTML is hosted by a separate service (CDN / Nginx / etc.), and the backend only provides interfaces. + +Next, I will introduce how the three deployment modes land, their advantages and existing problems. + +### The front and back ends are deployed on the same server + +This is the default deployment mode for full stack suites. + +Advantages: The simplest, upload the packaged product directly to the server, and provide services after startup +Weaknesses: + +- Backend services need to process & send files +- Static resources are not located in CDN, and the access speed of different regions is unstable. + +The overall deployment architecture is shown in the figure: + +![](https://img.alicdn.com/imgextra/i1/O1CN01GYtN9n1T2tbEXWOwf_!!6000000002325-2-tps-2064-648.png) + +### Static resources are deployed to CDN, backend hosting HTML & providing interfaces + +This is also the current front-end mainstream deployment mode. + +Advantages: + +- Static resources are hosted by CDN to ensure user access speed. +- The back end hosts HTML to ensure that the returned HTML file is up to date + +Weaknesses: + +- The backend still needs to host HTML, still needs to process & send files, and the page cannot be accessed if the service goes down. + +The overall access architecture is shown in the figure: + +![](https://img.alicdn.com/imgextra/i4/O1CN01ue3LJg1HeernvfxgQ_!!6000000000783-55-tps-267-367.svg) + +#### Specify a static resource public domain name + +You can specify the public domain name of static resources by setting the `site. base` option in `midway.config.ts`. + +```ts +import react from '@vitejs/plugin-react'; +import { defineConfig } from '@midwayjs/hooks-kit'; + +export default defineConfig({ + vite: { + plugins: [react()] + base: 'https://cdn.example.com', + }, +}); +``` + +When accessing the page, the static resource will point to the address of the CDN. + +#### Deploy static files + +In a full stack suite project, the default build directory is dist, where `dist/_clients` is the frontend static resource directory. + +As follows: + +``` +dist +├── _client +│   ├── assets +│   │   ├── index.85bb4f15.js +│   │   ├── index.b779b14d.css +│   │   └── vendor.346bc0da.js +│   ├── index.html +│   ├── logo.png +│   └── manifest.json +├── _serve +│   └── index.js +├── book.js +├── configuration.js +├── date.js +├── midway.config.js +└── star.js +``` + +You need to upload the files in the `_client` directory to CDN. However, when you deploy the backend, the `_client/index.html` file is retained for backend hosting. + +### Static resources are deployed to CDN,HTML is hosted by a separate service (CDN / Nginx / etc.), and the backend only provides interfaces. + +This is also the current mainstream deployment mode of the front end. + +Advantages: + +- The backend only provides API interfaces and does not need to process & send files +- Static resources are hosted by CDN to ensure user access speed. +- HTML is hosted by a separate service, ensuring that the access page is the latest version, and the backend service downtime does not affect the page display. +- The architecture can be expanded to add more nodes to cope with unexpected situations, such as adding gateway nodes in front of the back-end, switching to standby services when the back-end service is down, etc. + +Weaknesses: + +- Complex, high requirements for CI / CD assembly line and infrastructure. + +The overall access architecture is shown in the figure + +![](https://img.alicdn.com/imgextra/i1/O1CN01i78JiC1yinvfLq84b_!!6000000006613-55-tps-323-367.svg) + +The deployment workflow is as follows: + +![](https://img.alicdn.com/imgextra/i2/O1CN018oAQf71h1QxHtRHYY_!!6000000004217-2-tps-1728-1680.png) + +#### Full stack suite deployment guide + +The index.html hosting capability of the full-stack kit needs to be disabled by default. In this case, the full-stack kit will not generate the hosting function of `index.html` during construction, and only provide Api services. + +```ts +import { defineConfig } from '@midwayjs/hooks-kit'; + +export default defineConfig({ + static: false, +}); +``` + +In your CI / CD workflow, the following files need to be processed separately. + +- index.html: Deploy to a separate managed service, such as Nginx / CDN, which is only responsible for static page rendering. +- Static resources: Deploy to CDN, such as Aliyun OSS. This service can provide CDN acceleration for static resources. +- Api service: deploy to your server + +The final domain name may be as follows: + +- Index.html: https://example.com +- Static resources: https://cdn.example.com +- Api service: https://api.example.com or https://example.com/api (reverse proxy needs to be set) diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/file-route.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/file-route.md new file mode 100644 index 000000000000..0af7bb3def8f --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/file-route.md @@ -0,0 +1,226 @@ +# Easy Mode & Filesystem Routing + +## Simple mode + +In Midway Hooks, we provide a simple mode that can use pure functions to quickly create interfaces. + +📢Note: + +- In the simple mode, you must enable the file routing system. You must enable the `routes` configuration in `midway.config.js`. +- Routes automatically generated by pure functions only support `GET` and `POST` methods, and in full-stack applications, passing `Query / Params / Header` parameters is not supported +- In simple mode, `Api()` can still be used to define routes, and manual path definition is supported. `basePath` will be added to the spliced path automatically. + +### Get request + +```ts +import { useContext } from '@midwayjs/hooks'; + +export async function getPath() { + // Get HTTP request context by Hooks + const ctx = useContext(); + return ctx.path; +} +``` + +Integrated call: + +```ts +import { getPath } from './api/lambda'; +const path = await getPath(); +console.log(path); // /api/getPath +``` + +Manual call: + +```ts +fetcher + .get('/api/getPath') + .then((res) => { + console.log(res.data); // /api/getPath + }); +``` + +### Post request + +```ts +import { useContext } from '@midwayjs/hooks'; + +export async function post ( + name: string +) { + const ctx = useContext(); + + return { + message: 'Hello ${name}!', + method: ctx.method + }; +} +``` + +Integrated call: + +```ts +import { post } from './api/lambda'; +const response = await post('Midway'); +console.log(response.data); // { message: 'Hello Midway!', method: 'POST'} +``` + +Manual call: + +```ts +fetch('/api/post', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + args: ['Midway'] + }), +}).then((res) => { + console.log(res.data); // { message: 'Hello Midway!', method: 'POST'} +}); +``` + +### Create a route by using `Api()` + +In simple mode, you can still use `Api()` to create routes. + +Invalid example: `Api(Get('/specify_path'))`. Manual path specification is not supported in simple mode. + +A valid example, two routes are exported. + +```ts +import { + Api + Get +} from '@midwayjs/hooks'; +import { useContext } from '@midwayjs/hooks'; + +export async function getPath() { + // Get HTTP request context by Hooks + const ctx = useContext(); + return ctx.path; +} + +export default Api(Get(), async () => { + return 'Hello Midway!'; +}); +``` + +## File system routing + +Enable the `routes` configuration in `midway.config.js` to enable the file routing system and easy mode. + +The configuration example is as follows: + +```ts +import { defineConfig } from '@midwayjs/hooks'; + +export default defineConfig({ + source: './src/apis', + routes: [ + { + baseDir: 'lambda', + basePath: '/api', + }, + ], +}); +``` + +Field explanation: + +- source: The backend directory. Default value: `./src/apis`. You can also specify custom directories such as `./src/functions`. +- routes: routing configuration. the default is an array. + - baseDir: Function folder, asynchronous functions exported from any `.ts` file under the folder will be generated as Api interface + - basePath: generated Api address prefix + +### Index routing + +The `index.ts` file in the directory is used as the root route. + +- `/lambda/index.ts` →`/` +- `/lambda/about/index.ts` → `/about` + +### Nested routing + +Nested files will also generate nested routes
+ +- `/lambda/about.ts` → `/about` +- `/lambda/blog/index.ts` → `/blog` +- `/lambda/about/contact.ts` → `/about/contact` + +### Export method and corresponding route + +The default exported method is generated as the root path, while the named method splices the function name on the path. + +The following example uses `/lambda/about.ts` + +- `export default () => {}` → `/about` +- `export function contact ()` → `/about/contact` + +### wildwith routing + +If you need to generate a wildcard route, such as `/api/*`, it is used to match/api,/api/about,/api/about/a/B/c. The file name is `[...file]`. + +📢It is recommended that only `export default` is used to export functions in wildcard routes to avoid unnecessary route conflicts. + +Example: + +- `/lambda/[...index].ts` → `/api/*` +- `/lambda/[...user].ts` → `/api/user/*` +- `/lambda/about/[...contact].ts` → `/api/about/contact/*` + +### Path parameters + +If you need to generate dynamic path parameters, the file name can be named in the `[file]` format. + +Examples: + +- `/lambda/[name]/project.ts` → `/api/about/:name/project` + - `/about/midwayjs/project` -> `{ name: 'midwayjs' }` +- `/lambda/[type]/[page].ts` → `/api/about/:type/:page` + - `/blog/1` -> `{ type: 'blog', page: '1' }` + - `/articles/3` -> `{ type: 'articles', page: '3' }` + +When you use the path parameters, you can only use `Api()` to develop backend interfaces and use `Params` to mark types. + +Take `/lambda/[name]/Project. ts` as an example: + +```ts +// lambda/[name]/project.ts +import { + Api + Get + Params + useContext +} from '@midwayjs/hooks'; + +export default Api ( + Get() + Params<{ name: string }>() + async () => { + const ctx = useContext(); + return { + name: ctx.params.name + }; + } +); +``` + +Integrated call: + +```ts +import getProject from './api/[name]/project'; +const response = await getProject({ + params: { name: 'midwayjs'} +}); +console.log(response); // { name: 'midwayjs'} +``` + +Manual call: + +```ts +fetch('/api/about/midwayjs/project') + .then((res) => res.json()) + .then((res) => console.log(res)); // { name: 'midwayjs'} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/fullstack.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/fullstack.md new file mode 100644 index 000000000000..ca26abeedcb5 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/fullstack.md @@ -0,0 +1,37 @@ +# Full stack kit + +In Midway Hooks, we provide `@midwayjs/hooks-kit` to quickly develop full stack applications. At present, we provide the following templates that can be used directly: + +- [react](https://github.com/midwayjs/hooks/blob/main/examples/react) +- [vue](https://github.com/midwayjs/hooks/blob/main/examples/vue) +- [prisma](https://github.com/midwayjs/hooks/blob/main/examples/prisma) + +## Command line interface + +In projects that use `@midwayjs/hooks-kit`, hooks executables can be used in npm scripts or run through `npx hooks`. The following is the default npm scripts in Midway full stack projects created through scaffolding: + +```json +{ + "scripts": { + "dev": "hooks dev", // start the development server + "start": "hooks start", // start the production server, please make sure you have run' npm run build' before using it' + "build": "hooks build" // Build the product for production + } +} +``` + +When using a command line, you can pass in options through command line parameters, and specific options can be referenced through -- help. + +For example, `hooks build -- help` + +Output: + +``` +Usage: + $hooks build [root] + +Options: + --outDir [string] output directory (default: dist) + --clean [boolean] clean output directory before build (default: false) + -h, --help Display this message +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/intro.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/intro.md new file mode 100644 index 000000000000..1b715b0aa8ba --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/intro.md @@ -0,0 +1,147 @@ +# Introduction + +:::caution + +The integrated solution will be gradually stopped for maintenance. Existing projects can continue to be used. Please choose carefully for new projects. + +::: + + + +Midway's integrated solution is a full-stack framework based on Midway Hooks as the main function, supporting four core features: "zero" Api & type safety & full-stack suite & powerful backend. + + + +## Differences from standard projects + + + +The integrated solution is based on the standard project, and a front-end adaptation layer is extended on top of it. While reusing all the standard project capabilities, it can also seamlessly cooperate with the front-end development, that is, in the project, both the front-end code, which in turn has Node code. + + + +## Feature introduction + +### Zero APIs + +The back-end interface functions developed in the Midway Hooks full-stack application can be directly imported and called, without the need for handwritten Ajax glue layers at the front and back ends. Here is a simple example: + +Backend code: + +```ts +import { + APIs, + Post, +} from '@midwayjs/hooks'; + +export default Api( + Post(), // Http Path: /api/say, + async (name: string) => { + return `Hello ${name}!`; + } +); +```` + +Front-end call: + +```ts +import say from './api'; + +const response = await say('Midway'); +console.log(response); // Hello Midway! +```` + +### Type Safety and Runtime Safety + +Using the [Validate](./validate.md) validator provided by `@midwayjs/hooks`, you can achieve type-safe + runtime-safe links from front-end to back-end. Here is a simple example: + +Backend code: + +```ts +import { + APIs, + Post, + Validate, +} from '@midwayjs/hooks'; +import { z } from 'zod'; + +export default Api( + Post('/hello'), + Validate(z.string(), z.number()), + async (name: string, age: number) => { + return `Hello ${name}, you are ${age} years old.`; + } +); +```` + +All-in-one call: + +```ts +import hello from './api'; + +try { + await hello(null, null); +} catch (error) { + console.log(error.message); // 'name must be a string' + console.log(error.status); // 422 +} +```` + +throughout the process. + +- Front-end: Based on type, statically verify input parameters and get type hints +- Backend: Verify the incoming parameters of the frontend +- Business logic such as database: use the correct data + +In this way, we can achieve static type safety + runtime safety at low cost. + +### Full stack kit + +In Midway Hooks, we provide `@midwayjs/hooks-kit` to quickly develop full stack applications. + +You can use `hooks dev` to start full-stack applications, `hooks build` to package full-stack applications, and on the server side, you can also use `hooks start` to start the application with one click. + +Solve your worries when using full-stack applications. + +### Powerful backend + +Midway Hooks is developed based on Midway. + +Midway is an 8-year-old Node.js framework with powerful back-end functions, including Cache / Redis / Mongodb / Task / Config and other commonly used components under the Web. + +And all of this you can enjoy seamlessly when using Midway Hooks. + +## create application + +Midway Hooks currently provides the following templates: + +- Full stack application + - [react](https://github.com/midwayjs/hooks/blob/main/examples/react) + - [vue](https://github.com/midwayjs/hooks/blob/main/examples/vue) + - [prisma](https://github.com/midwayjs/hooks/blob/main/examples/prisma) +- API Server + - [api](https://github.com/midwayjs/hooks/blob/main/examples/api) + +The command to create an application based on the specification is as follows: + +```bash +npx degit https://github.com/midwayjs/hooks/examples/ +```` + +The full stack application command to create a react template is as follows: + +```bash +npx degit https://github.com/midwayjs/hooks/examples/react ./hooks-app +```` + +The application command to create an api template is as follows: + +```bash +npx degit https://github.com/midwayjs/hooks/examples/api ./hooks-app +```` + +## Next step + +- Learn how to develop interfaces and provide them for front-end calls: [Interface Development](./api.md) +- How to use and create reusable Hooks: [Hooks](./builtin-hooks.md) +- How to validate user parameters at runtime: [validate](./validate.md) \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/middleware.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/middleware.md new file mode 100644 index 000000000000..ccce0c087aed --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/middleware.md @@ -0,0 +1,175 @@ +# Web Middleware + +Midway Hooks supports defining Web middleware through function + `useContext()`. + +## Grammar + +The middleware has only one parameter `next`. `ctx` needs to be obtained by `useContext`. You can also use any `Hooks` in the middleware. + +### Basic example + +Take recording request logs as an example: + +```typescript +import { Context } from '@midwayjs/koa'; +import { useContext } from '@midwayjs/hooks'; + +const logger = async (next: any) => { + const ctx = useContext(); + + console.log ( + '<-- [${ctx.method}] ${ctx.url}' + ); + + const start = Date.now(); + await next(); + const cost = Date.now() - start; + + console.log ( + '--> [${ctx.method}] ${ctx.url} ${cost}ms' + ); +}; +``` + +## Global middleware + +Global middleware is defined in `configuration.ts`, and the middleware defined here takes effect for all interfaces. + +```typescript +import { + hooks + createConfiguration +} from '@midwayjs/hooks'; +import logger from './logger'; + +// Global Middleware +export default createConfiguration({ + imports: [ + // highlight-start + hooks({ + middleware: [logger], + }), + // highlight-end + ], +}); +``` + +## File-level middleware + +File-level middleware is defined in the Api file. Through the exported `config.middleware`, the middleware takes effect on all Api functions in the file. + +```typescript +import { + ApiConfig + Api + Get +} from '@midwayjs/hooks'; +import logger from './logger'; + +// File Level Middleware +// highlight-start +export const config: ApiConfig = { + middleware: [logger] +}; +// highlight-end + +export default Api(Get(), async () => { + return 'Hello World!'; +}); +``` + +## Single function middleware + +Middleware defined by `Middleware(... Middlewares: HooksMiddleware[])` takes effect only for a single function + +```ts +import { + Api + Get + Middleware +} from '@midwayjs/hooks'; +import logger from './logger'; + +export default Api ( + Get() + // highlight-start + Middleware(logger) + // highlight-end + async () => { + return 'Hello World!'; + } +); +``` + +## Using Koa middleware + +You can pass in the Koa middleware directly in the above example. + +Take [@koa/cors](https://www.npmjs.com/package/@koa/cors) as an example. + +Global Enabled: + +```ts +import { + hooks + createConfiguration +} from '@midwayjs/hooks'; +import logger from './logger'; +import cors from '@koa/cors'; + +// Global Middleware +export default createConfiguration({ + imports: [ + hooks({ + // highlight-start + middleware: [logger, cors()] + // highlight-end + }), + ], +}); +``` + +File level enabled: + +```ts +import { + ApiConfig + Api + Get +} from '@midwayjs/hooks'; +import logger from './logger'; +import cors from '@koa/cors'; + +// File Level Middleware +// highlight-start +export const config: ApiConfig = { + middleware: [logger, cors] +}; +// highlight-end + +export default Api(Get(), async () => { + return 'Hello World!'; +}); +``` + +Function level enabled: + +```ts +import { + Api + Get + Middleware +} from '@midwayjs/hooks'; +import logger from './logger'; +import cors from '@koa/cors'; + +export default Api ( + Get() + // highlight-start + Middleware(logger, cors) + // highlight-end + async () => { + return 'Hello World!'; + } +); +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/prisma.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/prisma.md new file mode 100644 index 000000000000..1db54714ea5d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/prisma.md @@ -0,0 +1,149 @@ +# Prisma ORM + +In Midway Hooks, we recommend that you use [Prisma](https://prisma.io/) to build databases and achieve the goal of static type security. + +[Prsima](https://www.prisma.io/) is an ORM designed for Node.js & TypeScript. It provides a series of friendly functions (Schema definition, client generation, full TypeScript support), which can help users build applications quickly. + +## Example + +We provide a simple example of [hooks-prisma-starter](https://github.com/midwayjs/hooks/blob/main/examples/prisma/README.md) to demonstrate how to use Prisma in Midway Hooks. + +I will also briefly introduce how simple it will be for Midway Hooks to develop applications with Prisma. + +### Database Schema + +The example is based on sqlite, and the database schema is as follows: + +```prisma +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + content String? + published Boolean @default(false) + viewCount Int @default(0) + author User? @relation(fields: [authorId], references: [id]) + authorId Int? +} +``` + +For specific database settings & initial data filling, refer to the [hooks-prisma-starter](https://github.com/midwayjs/hooks/blob/main/examples/prisma/README.md) document. + +### Initialize Prisma + +Create a prisma file under the src/api of the project, and use the following code to initialize the Client. + +```ts +import { PrismaClient } from '@prisma/client'; + +export const prisma = + new PrismaClient(); +``` + +#### Use proxy mirroring + +Prisma will download executable files dynamically according to the platform during installation. If your network environment is not good, you can set the image through environment variables. + +```bash +PRISMA_ENGINES_MIRROR = https://registry.npmmirror.com/-/binary/prisma/ +``` + +Related issues: [mirror prisma](https://github.com/cnpm/mirrors/issues/248) + +### Query data + +Taking all published articles as an example, you can quickly complete the operation through the generated Prisma Client. + +Back-end code: + +```ts +import { + Api + Get +} from '@midwayjs/hooks'; +import { prisma } from './prisma'; + +export default Api(Get(), async () => { + const posts = + await prisma.post.findMany({ + where: { published: true} + include: { author: true} + }); + return posts; +}); +``` + +Integrated call: + +```ts +import fetchFeeds from '../api/feeds'; + +fetchFeeds().then((feeds) => { + console.log(feeds); +}); +``` + +### Add data + +Take the registration login as an example, the client generated based on the integrated call + Prisma can complete all the work in a few simple lines of code. + +Contains: + +- Front end type prompt +- Back-end parameter verification +- Database operation + +```ts +import { + Api + Post + Validate +} from '@midwayjs/hooks'; +import { z } from 'zod'; +import { prisma } from './prisma'; + +export const signUp = Api ( + Post() + Validate ( + z.string() + z.string().email() + ), + async ( + name: string + email: string + ) => { + const result = + await prisma.user.create({ + data: { + name + email + }, + }); + return result; + } +); +``` + +Integrated call: + +```ts +import { signUp } from '../api/feeds'; + +signUp('John', 'test@test.com').then( + (user) => { + console.log(user); + } +); +``` + +### More examples + +For more examples of Prisma, see [Prisma documentation](https://www.prisma.io/). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/safe.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/safe.md new file mode 100644 index 000000000000..c7c75db045d1 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/safe.md @@ -0,0 +1,65 @@ +# Static type safety + runtime safety + +Using the [Validate](./validate.md) checker provided by [Prisma](./prisma.md) and `@midwayjs/hooks`, a type security + runtime security link from front-end to back-end to database can be realized. + +Take the `POST /api/post` interface in [hooks-prisma-starter](https://github.com/midwayjs/hooks/blob/main/examples/fullstack/prisma/README.md) as an example. The code is as follows: + +```ts +import { + Api + Post + Validate +} from '@midwayjs/hooks'; +import { prisma } from './prisma'; +import { z } from 'zod'; + +const PostSchema = z.object({ + title: z.string().min(1) + content: z.string().min(1) + authorEmail: z.string().email() +}); + +export const createPost = Api( + Post('/api/post') + Validate(PostSchema) + async ( + post: z.infer + ) => { + const result = + await prisma.post.create({ + data: { + title: post.title + content: post.content + author: { + connect: { + email: post.authorEmail + }, + }, + }, + }); + return result; + } +); +``` + +Front-end call: + +```ts +import { createPost } from '../api/post'; + +await createPost({ + title: 'Hello Midway', + content: 'Hello Prisma', + authorEmail: 'test@test.com', +}); +``` + +At this time, the front end obtains the type prompt based on the schema of Zod, and the back end uses the `Validate` checker to check the type, and finally calls the `prisma.post.create` method to create the user. + +In the whole process. + +- Front end: based on type, static check input parameters, and get type prompt +- Backend: Check the front-end incoming parameters +- Database: Use Correct Data + +In this way, we can achieve static type security and runtime security at low cost. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/test.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/test.md new file mode 100644 index 000000000000..e285c9d9b957 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/test.md @@ -0,0 +1,212 @@ +# Testing + +In Midway Hooks, we can quickly test the Http interface. + +## Interface test + +Take Hello World as an example. In `src/hello.ts`, we exported an interface with the following code. + +```ts +import { Api, Get } from '@midwayjs/hooks'; + +export default Api(Get('/hello'), async () => { + return 'Hello World!'; +}); +``` + +In the test, you can start the application through `@midwayjs/mock` and call the interface to complete the test. + +### Call through `@midwayjs/hooks` + +`@midwayjs/hooks` provides a `getApiTrigger (API: ApiFunction)` method that can be used to get triggers. + +Take the above `hello` interface as an example, the `getApiTrigger(hello)` returns: + +```json +{ + "type": "HTTP ", + "method": "GET ", + "path": "/hello" +} +``` + +Here, we use the `createHttpRequest` method provided by `@midwayjs/mock` to call the interface. For `createHttpRequest` usage documents, please refer to [supertest](https://github.com/visionmedia/supertest). + +```ts +// src/hello.test.ts +import { + close + createApp + createHttpRequest +} from '@midwayjs/mock'; +import { + Framework + IMidwayKoaApplication +} from '@midwayjs/koa'; +import { getApiTrigger, HttpTriger } from '@midwayjs/hooks'; +import hello from './hello'; + +describe('test koa with api router', () => { + let app: IMidwayKoaApplication; + + beforeAll(async () => { + app = await createApp(); + }); + + afterAll(async () => { + await close(app); + }); + + test('Hello World', async () => { + const trigger = getApiTrigger(hello); + const response = await createHttpRequest(app) + .get(trigger.path) + .expect(200); + expect(response.text).toBe('Hello World!'); + }); +}); +``` + +### Manual call + +If you call this operation manually, you must specify parameters such as `Path`. + +```ts +test('Hello World', async () => { + const response = await createHttpRequest(app) + .get('/hello') + .expect(200); + expect(response.text).toBe('Hello World!'); +}); +``` + +### Request parameter Data + +Back-end code: + +```ts +import { Api, Post } from '@midwayjs/hooks'; + +export default Api ( + Post(), // Http Path: /api/say + async (name: string) => { + return 'Hello ${name}!'; + } +); +``` + +Test code: + +```ts +test('Hello World', async () => { + const trigger = getApiTrigger(say); + const response = await createHttpRequest(app) + .post(trigger.path) + .send({ args: ['Midway'] }) + .expect(200); + expect(response.text).toBe('Hello Midway!'); +}); +``` + +### Query parameter Query + +Back-end code: + +```ts +import { + Api + Get + Query + useContext +} from '@midwayjs/hooks'; + +export default Api ( + Get('/hello') + Query<{ name: string }>() + async () => { + const ctx = useContext(); + return 'Hello ${ctx.query.name}!'; + } +); +``` + +Test code: + +```ts +test('Hello World', async () => { + const trigger = getApiTrigger(hello); + const response = await createHttpRequest(app) + .get(trigger.path) + .query({ name: 'Midway' }) + .expect(200); + expect(response.text).toBe('Hello Midway!'); +}); +``` + +### Path parameter Params + +Back-end code: + +```ts +import { Api, Get, Params, useContext } from '@midwayjs/hooks' + +export default Api ( + Get('/article/:id') + Params<{ id: string }> (, + async () => { + const ctx = useContext() + return { + article: ctx.params.id + } + } +) +``` + +Test code: + +```ts +test('Get Article', async () => { + const response = await createHttpRequest(app) + .get('/article/1') + .expect(200); + + expect(response.body).toEqual({ article: '1' }); +}); +``` + +### Request header Headers + +Back-end code: + +```ts +import { + Api + Get + Headers + useContext +} from '@midwayjs/hooks'; + +export default Api ( + Get('/auth') + Headers<{ token: string }>() + async () => { + const ctx = useContext(); + return { + token: ctx.headers.token + }; + } +); +``` + +Test code: + +```ts +test('Auth', async () => { + const response = await createHttpRequest(app) + .get('/auth') + .set('token', '123456') + .expect(200); + + expect(response.body).toEqual({ token: '123456' }); +}); +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/upload.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/upload.md new file mode 100644 index 000000000000..705ec802023b --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/upload.md @@ -0,0 +1,157 @@ +# File Upload + +Midway Hooks provides `@midwayjs/hooks-upload` and cooperates with `@midwayjs/upload` to realize file upload function in pure function + integrated project. + +## Start + +Installation dependency: + +```bash +npm install @midwayjs/upload @midwayjs/hooks-upload +``` + +## Use + +### Enable upload components + +Enable the `@midwayjs/upload` component in the `configuration.ts` of the backend directory. For more information about the supported configuration items, see [file upload](/docs/extensions/upload). + +```diff +import { createConfiguration, hooks } from '@midwayjs/hooks'; +import * as Koa from '@midwayjs/koa'; ++ import * as upload from '@midwayjs/upload'; + +/** + * setup midway server + */ +export default createConfiguration({ + imports: [ + Koa + hooks() ++ upload + ], + importConfigs: [{ default: { keys: 'session_keys' } }] +}); +``` + +### Create interface + +In the backend directory, create a new interface file. + +```ts +import { Api } from '@midwayjs/hooks'; +import { + Upload + useFiles +} from '@midwayjs/hooks-upload'; + +export default Api ( + Upload('/api/upload') + async () => { + const files = useFiles(); + return files; + } +); +``` + +> Integrated call + +```tsx +import upload from './api/upload'; + +function Form() { + const [file, setFile] = + React.useState(null); + + const handleSubmit = async ( + e: React.FormEvent + ) => { + e.preventDefault(); + const files = { images: file }; + const response = await upload({ + files + }); + console.log(response); + }; + + const handleOnChange = ( + e: React.ChangeEvent + ) => { + console.log(e.target.files); + setFile(e.target.files); + }; + + return ( +
+

Hooks File Upload

+ + +
+ ); +} +``` + +> Manual call (uploaded via FormData) + +```ts +const input = + document.getElementById('file'); + +const formdata = new FormData(); +formdata.append('file', input.files[0]); + +fetch('/api/upload', { + method: 'POST', + body: formdata +}) + .then((res) => res.json()) + .then((res) => console.log(res)); +``` + +## Api + +### Upload(path?: string) + +Declare the upload interface, which can specify the path. By default, the `POST` interface supports only requests of the `multipart/form-data` type. + +### useFiles() + +Use the `useFiles()` in the function to get the uploaded file. The return value is Object, and the key is the field name at the time of upload. When multiple file field names are the same, Value is Array. + +```ts +// frontend +await upload({ pdf }); + +// backend +const files = useFiles(); +{ + pdf: { + filename: 'test.pdf', // file original name + Data: '/var/tmp/xxx.pdf', // temporary file address of the server when mode is file + fieldname: 'test1', // form field name + mimeType: 'application/pdf', // mime + } +} +``` + +### useFields() + +Returns fields FormData non-files. + +```ts +// frontend +const formdata = new FormData(); +formdata.append('name', 'test'); + +post(formdata); + +// backend +const fields = useFields(); +// { name: 'test'} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/validate.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/validate.md new file mode 100644 index 000000000000..40579b51605d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/hooks/validate.md @@ -0,0 +1,248 @@ +## # Parameter Validation + +## check + +Midway Hooks uses [zod@3](https://www.npmjs.com/package/zod) as a validator, and provides `Validate(...schemas: any[])` to validate user input parameters, ` ValidateHttp(options)` function to validate Http structure. + +Please install [zod](https://www.npmjs.com/package/zod) before use. + +```` +npm install zod +```` + +##Validate + +The order of schemas passed in `Validate` matches the order of user input parameters. + +### Basic example + +```ts +import { + APIs, + Post, + Validate, +} from '@midwayjs/hooks'; +import { z } from 'zod'; + +export default Api( + Post('/hello'), + Validate(z.string(), z.number()), + async (name: string, age: number) => { + return `Hello ${name}, you are ${age} years old.`; + } +); +```` + +All-in-one call: + +```ts +import hello from './api'; + +try { + await hello(null, null); +} catch (error) { + console.log( + JSON.parse(error.data.message) + ); + console.log(error.status); // 422 +} +```` + +Manual call: + +```ts +fetcher + .post('/hello', { + args: [null, null], + }) + .catch((error) => { + console.log( + JSON.parse(error.data.message) + ); + console.log(error.status); // 422 + }); +```` + +### Error handling + +Validation failure errors can be caught with Try/Catch. + +```ts +try { + // call the interface +} catch (error) { + console.log(error.data.code); // VALIDATION_FAILED + console.log( + JSON.parse(error.data.message) + ); +} +```` + +`error.data.message` contains the complete [error message](https://zod.js.org/docs/errors/), you need to use `JSON.parse` to parse, the parsed example is as follows: + +```ts +[ + { + code: 'invalid_type', + expected: 'string', + received: 'number', + path: [0, 'name'], + message: + 'Expected string, received number', + }, +]; +```` + +in: + +- `message`: error message +- The `path` parameter represents the error path. For example, `0` represents the first parameter validation error, and `name` represents the `name` field validation error. + +You can manually parse the error message and display it to the user. + +###ValidateHttp + +ValidateHttp(options) supports passing in `options` parameters, the types are as follows. + +```ts +type ValidateHttpOption = { + query?: z.Schema; + params?: z.Schema; + headers?: z.Schema; + data?: z.Schema[]; +}; +```` + +Take validating the `Query` parameter as an example. + +Backend code: + +```ts +import { + APIs, + Get, + Query, + useContext, + ValidateHttp, +} from '@midwayjs/hooks'; +import { z } from 'zod'; + +const QuerySchema = z. object({ + searchString: z.string().min(5), +}); + +export const filterPosts = Api( + Get('/api/filterPosts'), + Query>(), + ValidateHttp({ query: QuerySchema }), + async() => { + const ctx = useContext(); + return ctx.query.searchString; + } +); +```` + +All-in-one call: + +```ts +import filterPosts from './api'; + +try { + await filterPosts({ + query: { searchString: '' }, + }); +} catch (error) { + console.log( + JSON.parse(error.data.message) + ); + console.log(error.status); // 422 +} +```` + +Manual call: + +```ts +fetcher + .get( + '/api/filterPosts?searchString=1' + ) + .catch((error) => { + console.log( + JSON.parse(error.data.message) + ); + console.log(error.status); // 422 + }); +```` + +## TypeScript support + +You can use the built-in TypeScript function of zod to deduce and verify complex types. + +An example is as follows: + +```ts +import { + APIs, + Post, + Validate, +} from '@midwayjs/hooks'; +import { z } from 'zod'; + +const Project = z.object({ + name: z.string(), + description: z.string(), + owner: z.string(), + members: z.array(z.string()), +}); + +export default Api( + Post('/project'), + Validate(Project), + async ( + // { name: string, description: string, owner: string, members: string[] } + project: z.infer + ) => { + return project; + } +); +```` + +All-in-one call: + +```ts +import createProject from './api'; + +try { + await createProject({ + name: 1, + description: 'test project', + owner: 'test', + members: ['test'], + }); +} catch (error) { + console.log(error.message); + console.log(error.status); // 422 +} +```` + +Manual call: + +```ts +fetcher + .post('/project', { + args: [ + { + name: 1, + description: 'test project', + owner: 'test', + members: ['test'], + }, + ], + }) + .catch((error) => { + console.log( + JSON.parse(error.data.message) + ); + console.log(error.status); // 422 + }); +```` \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/how_to_install_nodejs.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/how_to_install_nodejs.md new file mode 100644 index 000000000000..0e1deb5f7016 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/how_to_install_nodejs.md @@ -0,0 +1,134 @@ +# How to install Node.js environment + +## Use scenario + +Generally, you can download the corresponding installation package from the [Node.js official website](https://nodejs.org/) to complete the environment configuration. + +However, when you **develop on-premises**, you often need to quickly update or switch the version. + +The community has solutions such as [nvm](https://github.com/creationix/nvm), [n](https://github.com/tj/n), etc. We recommend cross-platform [nvs](https:/ /github.com/jasongin/nvs). + +- nvs is cross-platform. +- Nvs is written based on Node, and we can participate in maintenance. + + + +> Friendly reminder: both Node 12.x and 14.x run into EOL. please upgrade to 16 or 18 as soon as possible. +> [https://github.com/nodejs/Release](https://github.com/nodejs/Release) + + + +**PS:nvs is generally only used for local development. For more information, see** [Popular text: What should I do if O & M does not upgrade the Node version?](https://zhuanlan.zhihu.com/p/39226941) + +--- + +## How to install + +### Linux / macOS environment + +The project corresponding to Git Clone can be used. + +```bash +$ export NVS_HOME="$HOME/.nvs" +$ git clone https://github.com/jasongin/nvs --depth=1 "$NVS_HOME" +$ . "$NVS_HOME/nvs.sh" install +``` + +### Windows environment + +Due to the complexity of Windows environment configuration, it is recommended to use `msi` file to complete initialization. +Visit [nvs/releases](https://github.com/jasongin/nvs/releases) to download the latest version of `nvs.msi`, and then double-click to install. + +--- + +## Configure mirror address +In China, due to reasons that everyone knows, it is necessary to modify the corresponding mirror address: +```bash +$ nvs remote node https://npmmirror.com/mirrors/node/ +$ nvs remote +default node +chakracore https://github.com/nodejs/node-chakracore/releases/ +chakracore-nightly https://nodejs.org/download/chakracore-nightly/ +nightly https://nodejs.org/download/nightly/ +node https://nodejs.org/dist/ +``` + +--- + +## Guidelines for Use +With the following command, you can easily install the latest LTS version of Node.js. +```bash +# Install the latest LTS version +$ nvs add lts +# Configure as default version +$ nvs link lts +``` +Install other versions: +```bash +# Install other versions and try them +$ nvs add 12 +# View installed versions +$ nvs ls +# Switch version at current Shell +$ nvs use 12 +``` +For more information, see `nvs --help`. + +--- + +## Common npm global module +If you use `nvs`, the default `prefix` is the installation path of the currently activated Node.js version. +One problem is that after switching versions, the previous installation of the global command module needs to be reinstalled, which is very inconvenient. +The solution is to configure a unified global module installation path to `~/.npm-global`, as follows: +```bash +$ mkdir -p ~/.npm-global +$ npm config set prefix ~/.npm-global +``` +You must also configure environment variables in the `~/.bashrc` or `~/.zshrc` file: +```bash +$ echo "export PATH=~/.npm-global/bin:$PATH" >> ~/.zshrc +$ source ~/.zshrc +``` + +--- + + + +## Mac Silicon chips use lower versions of Node.js + +If you are using an Apple chip, since there is no chip support build for arm64 below Node.js 16, it cannot be installed directly. + +Fortunately, there are workarounds to get Node.js 14 to work with Mac Silicon. Apple offers Rosetta, a translation app that allows apps built for Intel chips (or previous-generation Macs) to run under Apple Silicon. + +There are two steps: + +* 1. Install Rosetta +* 2. Switch to the intel environment and install a lower version of Node.js + + + +**Install Rosetta** + +Open the terminal and execute + +```bash +$ /usr/sbin/softwareupdate --install-rosetta --agree-to-license +``` + + + +**Switch the environment and install a lower version of Node.js** + +* 1. Open the terminal, execute `arch`, and confirm that the running is `arm64` +* 2. Execute `arch -x86_64 zsh` to open a new terminal +* 3. Execute `arch` to confirm that the running is `i386` +* 4. Install a lower version of Node.js, you can use the nvs or nvm mentioned above to install + + + +## Related reading + +- [Popular text: Node.js security attack and defense-how to forge and obtain a user's real IP address?](https://zhuanlan.zhihu.com/p/62265144) +- [Popular text: What if O & M does not upgrade the Node version?](https://zhuanlan.zhihu.com/p/39226941) +- [Popular Science: Why can't you use npm install on the server?](https://zhuanlan.zhihu.com/p/39209596) +- [Using NodeJs 14 with Mac Silicon (M1)](https://devzilla.io/using-nodejs-14-with-mac-silicon-m1) diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/how_to_update_midway.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/how_to_update_midway.md new file mode 100644 index 000000000000..64148620161b --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/how_to_update_midway.md @@ -0,0 +1,248 @@ +# How to update Midway + + + +## When will Midway be updated + +In general, you may need to update in the following cases: + +- 1. After Midway has sent a new version, when you want to use the new functions +- 2. When you install a new component with lock file +- 3. When there is an error that cannot be found in the method +- ... and so on + +For example, when the following error occurs + +1. Generally, it is a new package with components installed, but the old @midwayjs/core does not include this method, thus reporting an error. + +![](https://img.alicdn.com/imgextra/i3/O1CN01dDNRZr1MBPewPo7Xg_!!6000000001396-2-tps-1196-317.png) + +2. The general reason is that the version of @midwayjs/core that mock depends on does not have this method, indicating that the version is incorrect. It may be that the version is incorrectly referenced, or the version may be too low. + +![](https://img.alicdn.com/imgextra/i3/O1CN01HVMJKP1xNuFO2Wv73_!!6000000006432-2-tps-1055-135.png) + +3. When a new component is installed, we find that there is more than one version instance of a package + +![](https://img.alicdn.com/imgextra/i3/O1CN01jZxQu91YBCs0N9S9Y_!!6000000003020-2-tps-1133-43.png) + +## Update considerations + +:::danger + +**Do not**: + + +- 1. Upgrade a @midwayjs/* package separately +- 2. Remove the symbol from the version number in package.json + +::: + + + +## Check package version exception + +You can use the following command to execute in the project root directory to check. + +```bash +# Community user +$ npx midway-version +# Internal user +$ tnpx @ali/midway-version +``` + +If the project is a dependency of pnpm installation, please use the following command. + +```bash +# Community user +$ pnpx midway-version +# Internal user +$ pnpx @ali/midway-version +``` + + + +## Update version using tools + +You can use the following command to execute the update prompt in the project root directory. + +```bash +# Community user +$ npx midway-version -u +# Internal user +$ tnpx @ali/midway-version -u +``` + +If the project is a dependency of pnpm installation, please use the following command. + +```bash +# Community user +$ pnpx midway-version -u +# Internal user +$ pnpx @ali/midway-version -u +``` + +If you want to write updates to `package.json`, please use the following command. + +```bash +# Community user +$ npx midway-version -u -w +# Internal user +$ tnpx @ali/midway-version -u -w +``` + +If the project is a dependency of pnpm installation, please use the following command. + +```bash +# Community user +$ pnpx midway-version -u -w +# Internal user +$ pnpx @ali/midway-version -u -w +``` + +:::tip + +Newer versions will be written to `package.json` and `package-lock.json` and require reinstallation of dependencies. + +::: + + + +## Manually update the version + + + + +### General item update + + +For projects that normally use npm/yarn, please follow the following procedure for upgrading + + +- 1. delete package-lock.json or yarn.lock +- 2. completely delete node_modules (such as rm -rf node_modules) +- 3. Reinstall dependency (npm install or yarn) + + + +**We do not guarantee the effect of using other tools and cli separate upgrade packages.** + + + + +### Lerna project update + + +If you use lerna to develop a project, due to the existence of hoist mode, please follow the following procedure (take lerna3 as an example) + + + +- 1. Clean up the node_modules of subpackages, such as (lerna clean -- yes) +- 2. Delete the node_modules of the main package (such as rm -rf node_modules) +- 3. delete package-lock.json or yarn.lock +- 4. Reinstall Dependency (npm install & & lerna bootstrap) + + + +**We do not guarantee the effect of using other tools and cli separate upgrade packages.** + + +## Major version update + + +You must manually change the version number, for example, from `^ 1.0.0` to `^ 2.0.0`. + + + +## View current package version + + +The Midway package is managed and released using the standard Semver version. The version specified in `package.json` usually starts with `^`, indicating that it is compatible in a wide range of versions. + + +For example, if `@midwayjs/core` is set to `^ 2.3.0` in `package.json`, the latest version of `2.x` is installed according to npm installation rules. + + +Therefore, it is normal that the actual installed version is higher than the version specified in `package.json`. + + +You can use `npm ls package name` to view the specific version, such as `npm ls @midwayjs/core` to view the version of `@midwayjs/core`. + + +## Version matching query + + +Because lerna packages have certain dependencies, for example, the modified package will only be updated. **The version of the package under the midway may not be the same.** + + +For example, it is normal that the version of `@midwayjs/Web` is higher than that of `@midwayjs/core`. + + +Midway submits a [@midwayjs/version](https://www.npmjs.com/package/@midwayjs/version) package at each conference, which contains each of our versions and all the package versions matched by that version. Please [visit here](https://github.com/midwayjs/midway/tree/2.x/packages/version/versions) to view it. + + +The file names in the directory are created according to the `@midwayjs/decorator version-@midwayjs/core version. json` rule. Each version corresponds to a JSON file. + + +The file content uses the package name as the key and the compatible matching version name as the value. + + +For example, the egg-layer package versions compatible with the current file decorator(v2.10.18) and core(v2.10.18) are v2.10.18 and v2.10.19. + + +If the file name of the combination of decorator and core is not found, or the versions in the file do not match, there may be a **version problem**. + + +Examples of content are as follows: +```json +{ + "@midwayjs/egg-layer": [ + "2.10.18", + "2.10.19" + ], + "@midwayjs/express-layer": "2.10.18", + "@midwayjs/faas-typings": "2.10.7", + "@midwayjs/koa-layer": "2.10.18", + "@midwayjs/runtime-engine": "2.10.14", + "@midwayjs/runtime-mock": "2.10.14", + "@midwayjs/serverless-app": "2.10.18", + "@midwayjs/serverless-aws-starter": "2.10.14", + "@midwayjs/serverless-fc-starter": "2.10.18", + "@midwayjs/serverless-fc-trigger": "2.10.18", + "@midwayjs/serverless-http-parser": "2.10.7", + "@midwayjs/serverless-scf-starter": "2.10.14", + "@midwayjs/serverless-scf-trigger": "2.10.18", + "@midwayjs/static-layer": "2.10.18", + "@midwayjs/bootstrap": "2.10.18", + "@midwayjs/cache": "2.10.18", + "@midwayjs/consul": "2.10.18", + "@midwayjs/core": "2.10.18", + "@midwayjs/decorator": "2.10.18", + "@midwayjs/faas": "2.10.18", + "@midwayjs/grpc": "2.10.18", + "@midwayjs/logger": "2.10.18", + "midway-schedule": "2.10.18", + "midway": [ + "2.10.18", + "2.10.19" + ], + "@midwayjs/mock": "2.10.18", + "@midwayjs/prometheus": "2.10.18", + "@midwayjs/rabbitmq": "2.10.18", + "@midwayjs/socketio": "2.10.18", + "@midwayjs/task": [ + "2.10.18", + "2.10.19" + ], + "@midwayjs/typegoose": "2.10.18", + "@midwayjs/version": [ + "2.10.18", + "2.10.19" + ], + "@midwayjs/express": "2.10.18", + "@midwayjs/koa": "2.10.18", + "@midwayjs/web": [ + "2.10.18", + "2.10.19" + ] +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/intro.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/intro.md new file mode 100644 index 000000000000..dc2826b4c31d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/intro.md @@ -0,0 +1,130 @@ +# Introduction + +Midway is a Node.js framework developed by Alibaba-Taobao front-end architecture team based on the concept of gradual development. Through self-developed dependency injection containers and various upper-level modules, Midway combines solutions suitable for different scenarios. + +Midway is based on TypeScript development, combines two programming paradigms: `object-oriented (OOP + Class + IoC)` and `functional (FP + Function + Hooks)`, and supports various scenarios such as Web/full stack/microservice/RPC / Socket / Serverless, and is committed to providing users with a simple, easy-to-use and reliable Node.js server research and development experience. + + + +## Why Midway + +There are many similar frameworks in the community, so why do you need Midway? + +There are three reasons: + +1. Midway is a framework that has been continuously developed in Ali. Before, egg was used as the underlying framework and needed an application-oriented framework to interface with group scenarios. +2. Full use of TypeScript is the trend in the future, and future-oriented iteration and research and development are the requirements of architecture group innovation. +3. Although the community already has a framework like nest, the maintenance, collaboration, and modification of these products will be restricted by commercial products, and it is impossible to achieve rapid iteration of requirements and security guarantees. The overall research and development concept is also different from ours. For this reason, we need a self-developed framework system + + + +## Our strengths + +1. Midway framework is a Node.js framework that has been used internally for more than 5 years, backed by a team with long-term investment and continuous maintenance. +2. Has been tested in the annual promotion scene, stability need not worry +3. Rich components and scalability, such as database, cache, timing tasks, process model, deployment and support for new scenarios such as Web,Socket and even Serverless +4. The integrated calling scheme can be conveniently and quickly developed with front-end pages. +5. Good TypeScript definition support +6. Localized documentation and communication are easy and simple. + + + +## Multi-programming paradigm + +Midway supports two programming paradigms: object-oriented and functional. You can choose different programming paradigms to develop applications according to actual research and development needs. + + + +### Object-oriented (OOP + Class + IoC) + +Midway supports the object-oriented programming paradigm and provides a more elegant architecture for applications. + +The following is an example of developing routes based on object-oriented. +```typescript +// src/controller/home.ts +import { Controller, Get } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context + + @Get('/') + async home() { + return { + message: 'Hello Midwayjs!', + query: this.ctx.ip + } + } +} +``` + + + +### Functional formula (FP + Function + Hooks) + +Midway also supports a functional programming paradigm that provides higher R & D efficiency for applications. + + +The following is an example of developing a routing interface based on a function. +```typescript +// src/api/index.ts + +import { useContext } from '@midwayjs/hooks' +import { Context } from '@midwayjs/koa'; + +export default async function home () { + const ctx = useContext() + + return { + message: 'Hello Midwayjs!', + query: ctx.ip + } +} +``` + + + +## Environmental preparation + + +Please install Node.js environment and npm in advance to run Midway. cnpm can be used in China. + + +- Operating system: supports macOS,Linux,Windows +- Running environment: We recommend that you select [LTS](http://nodejs.org/). The minimum requirement is **12.11.0**. + +After continuous iteration, Midway's version requirements are as follows: + +| Midway Version | Development environment Node.js version requirements | Deployment environment Node.js version requirements | +| -------------- | ---------------------------------------------------- | --------------------------------------------------- | +| >=v3.9.0 | >= v14, LTS version recommended | >= v12.11.0 | +| 3.0.0 ~ 3.9.0 | >= v12, LTS version recommended | >= v12.0.0 | +| 2.x | >= v12, LTS version recommended | >= v10.0.0 | + +For more information, see [How to install the Node.js environment](how_to_install_nodejs). + + + +## Correct questions + +- ✅Ask a question on [github issue](https://github.com/midwayjs/midway/issues), which can be traced, precipitated, and Star + - 1. Describe your problem and provide as detailed a reproduction method as possible, framework version, scenario (Serverless or application) + - 2. Provide error screenshots, stack information and minimum reproduction repo as much as possible. + + + +## Q & A sharing group + +There will be enthusiastic friends in the group and new versions will be released and pushed. + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01LyI8r91S91RsKsD29_!!6000000002203-0-tps-3916-2480.jpg) + + + +## Official publicity channels + +- [Bilibili](https://space.bilibili.com/1746017680) provides updated information and tutorials. + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/mongodb.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/mongodb.md new file mode 100644 index 000000000000..da2f3518550d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/mongodb.md @@ -0,0 +1,547 @@ +# MongoDB + + +:::tip +This document is obsolete from v3.4.0. +::: + +In this chapter, we choose [Typegoose](https://github.com/typegoose/typegoose) as the MongoDB ORM library based on it. As he described, "Define Mongoose models using TypeScript classes" is very well combined with TypeScript. + +Simply put, Typegoose using TypeScript "wrappers" to write Mongoose models, most of its capabilities are provided by [mongoose](https://www.npmjs.com/package/mongoose) libraries. + +You can also directly select the [mongoose](https://www.npmjs.com/package/mongoose) library to use, and we will describe it separately. + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | + + + +## Mongoose version dependency + + +The mongoose is also related to the version of MongoDB Server used by your server, as follows, please note. + + +- MongoDB Server 2.4.x: mongoose ^3.8 or 4.x +- MongoDB Server 2.6.x: mongoose ^3.8.8 or 4.x +- MongoDB Server 3.0.x: mongoose ^3.8.22, 4.x, or 5.x +- MongoDB Server 3.2.x: mongoose ^4.3.0 or 5.x +- MongoDB Server 3.4.x: mongoose ^4.7.3 or 5.x +- MongoDB Server 3.6.x: mongoose 5.x +- MongoDB Server 4.0.x: mongoose ^5.2.0 +- MongoDB Server 4.2.x: mongoose ^5.7.0 +- MongoDB Server 4.4.x: mongoose ^5.10.0 +- MongoDB Server 5.x: mongoose ^6.0.0 + + +**mongoose related dependencies are complex and correspond to different versions. At this stage, we mainly use mongoose v5 and v6.** + + +:::info +From mongoose@v5.11.0 on, mongoose the definition is officially supported, there is no need to install the @types/mongoose dependency package. +::: + + +The installation package depends on the following version: + +**Support MongoDB Server 5.x** + +```json + "dependencies": { + "mongoose": "^6.0.7 ", + "@typegoose/typegoose": "9.0.0", // This dependency needs to be installed using typegoose + }, +``` + + +**Support MongoDB Server 4.4.x** + + +The following versions do not require additional definition packages to be installed. +```json + "dependencies": { + "mongoose": "^5.13.3 ", + "@typegoose/typegoose": "^8.0.0", // This dependency needs to be installed using typegoose + }, +``` + + +The following versions require additional definition packages to be installed (not recommended). +```json + "dependencies": { + "mongodb": "3.6.3", // The version is written inside the mongoose + "mongoose": "~5.10.18 ", + "@typegoose/typegoose": "7.0.0", // This dependency needs to be installed using typegoose + }, + "devDependencies": { + "@types/mongodb": "3.6.3", // this version can only be used + "@types/mongoose": "~5.10.3 ", + } +``` + + +The remaining MongoDB installation modules are similar and have not been tested. + + +## Use Typegoose + + +### 1. Install components + + +Install Typegoose components to provide access to MongoDB. + + +**Please note that please check the first section to write/install mongoose and other related dependency packages in advance.** +```bash +$ npm i @midwayjs/typegoose@3 --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + // Components + "@midwayjs/typegoose": "^3.0.0", + // mongoose dependency in the previous section + }, + "devDependencies": { + // mongoose dependency in the previous section + // ... + } +} +``` + + + +After installation, you need to manually configure `src/configuration.ts`. The code is as follows. + +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as typegoose from '@midwayjs/typegoose'; + +@Configuration({ + imports: [ + typegoose // Load typegoose Components + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + + +:::info +In this component, midway just makes a simple configuration regularization and injects it into the initialization process. +::: + + +### 2. Configure connection information + + +Add the configuration of the connection to `src/config/config.default.ts`. + +```typescript +export default { + // ... + mongoose: { + client: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '***********' + } + } + }, +} +``` + + +### 3. Simple directory structure + + +Let's take a simple project as an example. Please refer to other structures yourself. + + +``` +MyProject +├── src // TS root directory +│ ├── config +│ │ └── config.default.ts // Application Profile +│ ├── entity // entity (database Model) directory +│ │ └── user.ts // entity file +│ ├── configuration.ts // Midway configuration file +│ └── service // Other Service directory +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + +Here, our database entities are mainly located in the `entity` directory (non-mandatory). This is a simple convention. + + +### 3. Create an entity file + + +```typescript +import { prop } from '@typegoose/typegoose'; +import { EntityModel } from '@midwayjs/typegoose'; + +@EntityModel() +export class User { + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} +``` + +Equivalent to the following code that uses the Mongoose + +```typescript +const userSchema = new mongoose.Schema({ + name: String + jobs: [{ type: String }] +}); + +const User = mongoose.model('User', userSchema); +``` + +:::info +Therefore, typegoose just simplify the process of creating model. +::: + +### 4, reference entities, call the database. + + +The sample code is as follows: + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typegoose'; +import { ReturnModelType } from '@typegoose/typegoose'; +import { User } from '../entity/user'; + +@Provide() +export class TestService { + + @InjectEntityModel(User) + userModel: ReturnModelType; + + async getTest() { + // create data + const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an "as" assertion, to have types for all properties + + // find data + const user = await this.userModel.findById(id).exec(); + console.log(user) + } +} +``` + + +### 5, multi-Library situation + + +First configure multiple connections. + + +Add the configuration of the connection to `src/config/config.default.ts`, `default` represents the default connection. +```typescript +export default { + // ... + mongoose: { + clients: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '***********' + } + }, + Db1: { + uri: 'mongodb://localhost:27017/test1', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '***********' + } + } + } + }, +} +``` + + +Use fixed connections when defining instances, such: +```typescript +@EntityModel() // default connection is used by default +class User { + + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} + +@EntityModel({ + connectionName: 'db1' // db1 connection is used here +}) +class User2 { + + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} +``` + + +When in use, inject specific connections +```typescript +@Provide() +export class TestService { + + @InjectEntityModel(User) + userModel: ReturnModelType; + + @InjectEntityModel(User2) + user2Model: ReturnModelType; + + async getTest() { + const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an "as" assertion, to have types for all properties + const user = await this.userModel.findById(id).exec(); + console.log(user) + + const { _id: id2 } = await this.user2Model.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User2); // an "as" assertion, to have types for all properties + const user2 = await this.user2Model.findById(id2).exec(); + console.log(user2) + } +} + +``` + + +## Direct use of mongoose + +mongoose component is the basic component of typegoose, sometimes we can use it directly. + + +### 1. Install components + +**Please note that please check the first section to write/install mongoose and other related dependency packages in advance.** + +```bash +$ npm i @midwayjs/mongoose --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + // Components + "@midwayjs/mongoose": "^3.0.0", + // mongoose dependency in the previous section + }, + "devDependencies": { + // mongoose dependency in the previous section + // ... + } +} +``` + + + +### 2. Open the components + + +After installation, you need to manually configure `src/configuration.ts`. The code is as follows. +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as mongoose from '@midwayjs/mongoose'; + +@Configuration({ + imports: [ + mongoose // Load mongoose Components + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + + + + +### 2. Configuration + + +Same as typegoose, or typegoose use mongoose configuration. + + +Single library: +```typescript +export default { + // ... + mongoose: { + client: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '**********' + } + } + }, +} +``` +Multi-library: +```typescript +export default { + // ... + mongoose: { + clients: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '***********' + } + }, + Db1: { + uri: 'mongodb://localhost:27017/test1', + options: { + useNewUrlParser: true + useUnifiedTopology: true + user: '***********', + pass: '***********' + } + } + } + }, +} +``` + + +### 3. Use + + +When there is only one default connection or the default connection is directly used, we can directly use the encapsulated `MongooseConnectionService` object to create the model. +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { MongooseConnectionService } from '@midwayjs/mongoose'; +import { Schema, Document } from 'mongoose'; + +interface User extends Document { + name: string; + email: string; + avatar: string; +} + +@Provide() +export class TestService { + + @Inject() + conn: MongooseConnectionService; + + async invoke() { + const schema = new Schema({ + name: { type: String, required: true} + email: { type: String, required: true} + avatar: String + }); + const UserModel = this.conn.model('User', schema); + const doc = new UserModel({ + name: 'Bill', + email: 'bill@initech.com', + avatar: 'https:// I .imgur.com/dM7Thhn.png' + }); + await doc.save(); + } +} + +``` + + +If multiple other connections are configured, obtain the connection from the factory method before using it. +```typescript +import { MongooseConnectionServiceFactory } from '@midwayjs/mongoose'; +import { Schema } from 'mongoose'; + +@Provide() +export class TestService { + + @Inject() + connFactory: MongooseConnectionServiceFactory; + + async invoke() { + // get db1 connection + const conn = this.connFactory.get('db1'); + + // get default connection + const defaultConn = this.connFactory.get('default'); + + } +} + +``` + + +## Frequently Asked Questions + + +### 1. E002: You are using a NodeJS Version below 12.22.0 + + +Node version verification has been added to the new version @typegoose/typegoose (v8, v9). if your Node.js version is lower than v12.22.0, this prompt will appear. + + +Under normal circumstances, please upgrade Node.js to this version or above to solve the problem. + + +In special scenarios, such as when the Serverless cannot modify the Node.js version and the version is lower than v12.22, the v12 version can actually be subversions, which can be bypassed by temporarily modifying the process.version. + + +```typescript +// src/configuration.ts + +Object.defineProperty(process, 'version', { + value: 'v12.22.0', + writable: true, +}); + +// other code + +export class MainConfiguration {} +``` + + + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/orm.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/orm.md new file mode 100644 index 000000000000..5230969fc10a --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/orm.md @@ -0,0 +1,1526 @@ +# TypeORM + +:::tip +This document is obsolete from v3.4.0. +::: + +[TypeORM](https://github.com/typeorm/typeorm) is the most mature object relation mapper (`ORM`) in the existing community of `node.js`. Midway and TypeORM match to make development easier. + + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | + + + + +## Installation Components + + +Install orm components to provide database ORM capabilities. + + +```bash +$ npm i @midwayjs/orm@3 typeorm --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/orm": "^3.0.0", + "typeorm": "~0.3.0 ", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## Enable component + + +Introducing orm components in `src/configuration.ts`, an example is as follows. + +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as orm from '@midwayjs/orm'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + orm // load orm components + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class ContainerConfiguratin { + +} +``` + + + +## Install database Driver + + +The commonly used database drivers are as follows. Select the database type to install the corresponding connection: + +```bash +# for MySQL or MariaDB, you can also use mysql2 instead +npm install mysql --save +npm install mysql2 --save + +# for PostgreSQL or CockroachDB +npm install pg --save + +# for SQLite +npm install sqlite3 --save + +# for Microsoft SQL Server +npm install mssql --save + +# for SQL .js +npm install SQL .js --save + +# for Oracle +npm install oracledb --save + +# for MongoDB(experimental) +npm install mongodb --save +``` + +:::info +To make the** Oracle driver work**, you need to follow the installation instructions from [their](https://github.com/oracle/node-oracledb) site. +::: + + + + +## Simple directory structure + + +Let's take a simple project as an example. Please refer to other structures yourself. + + +``` +MyProject +├── src // TS root directory +│ ├── config +│ │ └── config.default.ts // Application Profile +│ ├── entity // entity (database Model) directory +│ │ └── photo.ts // entity file +│ │ └── photoMetadata.ts +│ ├── configuration.ts // Midway configuration file +│ └── service // Other service directory +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + + + +Here, our database entities are mainly located in the `entity` directory (non-mandatory). This is a simple convention. + + + +## Getting Started + +Next, we will take mysql as an example. + + + + +### 1. Create Model + + +We associate the model with the database. The model in the application is the database table. In TypeORM, the model is bound to the entity. Each Entity file is a Model and an Entity. + + +In the example, you need an entity. Let's take `photo` as an example. Create an entity directory and add the entity file `photo.ts` to the entity directory. A simple entity is as follows. + +```typescript +// entity/photo.ts +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + +Note that each attribute of the entity file here is actually one-to-one corresponding to the database table. Based on the existing database table, we add content up. + + +### 2. Add a solid model decorator + + +We use `EntityModel` to define an entity model class. + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; + +@EntityModel('photo') +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + +:::caution +Note that the EntityModel here is a special decorator packaged by midway, in order to better combine with midway. Please do not directly use Entity in the typeorm. +::: + + +If the table name is different from the current entity name, you can specify it in the parameter. + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; + +@EntityModel('photo_table_name') +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + + +These entity columns can also be generated using [typeorm_generator](/docs/tool/typeorm_generator) tools. + + +### 3. Add database columns + + +Attributes are decorated with the `@Column` decorator provided by typeorm, and each attribute corresponds to a column. + + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; +import { Column } from 'typeorm'; + +@EntityModel() +export class Photo { + + @Column() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` + +The `id`, `name`, `description`, `filename`, `views`, `isPublished` columns are added to the `photo` table. The column types in the database are inferred according to the attribute types you use, for example, number will be converted to integers, strings will be converted to varchar, boolean values will be converted to bool, and so on. However, you can use any column type supported by the database by explicitly specifying the column type in the `@Column` decorator. + + +We generated a database table with columns, but there is one thing left. Each database table must have a column with a primary key. + + +Database columns include more column options (ColumnOptions), such as modifying column names, specifying column types, and column lengths. For more options, see the [official documentation](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/entities.md#%E5%88% 97% E9%80% 89% E9%A1%B9). + + + + +### 4. Create a primary key column + + +Each entity must have at least one primary key column. To make a column a primary key, you need to use the `@PrimaryColumn` decorator. + + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryColumn } from 'typeorm'; + +@EntityModel() +export class Photo { + + @PrimaryColumn() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` + +### 5. Create a self-increasing primary key column + + +Now, if you want to set the self-increasing id column, you need to change the `@PrimaryColumn` decorator to the `@PrimaryGeneratedColumn` decorator: + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryGeneratedColumn } from 'typeorm'; + +@EntityModel() +export class Photo { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` + + +### 6. Column data type + + +Next, let's adjust the data type. By default, strings map to types similar to `varchar(255)` (depending on the database type). Number is mapped to an integer-like type (depending on the database type). However, we do not want all columns to be limited to varchars or integers. Some changes can be made at this time. + + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryGeneratedColumn } from 'typeorm'; + +@EntityModel() +export class Photo { + + @PrimaryGeneratedColumn() + id: number; + + @Column({ + length: 100 + }) + name: string; + + @Column('text') + description: string; + + @Column() + filename: string; + + @Column("double") + views: number; + + @Column() + isPublished: boolean; + +} +``` + + +Example, different column names + +```typescript +@Column({ + length: 100 + name: 'custom_name' +}) +name: string; +``` + + +In addition, there are several special column types that can be used: + + +- `@CreateDateColumn` is a special column that automatically inserts dates for entities. +- The `@UpdateDateColumn` is a special column that automatically updates the entity date each time the entity manager or save of the repository is called. +- The `@VersionColumn` is a special column that automatically increases the entity version (increment number) each time the entity manager or save of the repository is called. +- `@DeleteDateColumn` is a special column that automatically sets the deletion time of the entity when soft-delete is called. + +The column type is database-specific. You can set any column type supported by the database. For more information about supported column types, see [here](https://github.com/typeorm/typeorm/blob/master/docs/entities.md#column-types). + +:::tip + +`CreateDateColumn` and `UpdateDateColumn` rely on the insertion date function of creating the default data on the column when the table structure is synchronized for the first time. If the table is created by yourself, you need to add the default data to the column. + +::: + + + + +### 7. Configure connection information + + +Please refer to the [Configuration](/docs/env_config) chapter to add configuration files. + + +Then configure the database connection information in `config.default.ts`. + +```typescript +// src/config/config.default.ts +export default { + // ... + orm: { + /** + * Single database instance + */ + type: 'mysql', + host: '', + port: 3306 + username: '', + password: '', + database: undefined + synchronize: false, // If the table does not exist for the first time, you can write true if you need synchronization. + logging: false + }, +} +``` + +utc time is stored by default (recommended). + + +Time zone can also be configured (not recommended) + +```typescript +// src/config/config.default.ts +export default { + // ... + orm: { + // ... + timezone: '+08:00', + }, +} +``` + + +You can use other database types for this `type` field, including `mysql`, `mariadb`, `postgres`, `cockroachdb`, `sqlite`, `mssql`, `oracle`, `cordova`, `nativescript`, `react-native`, `expo`, or `mongodb` + + +For example, sqlite only needs the following information. + + +```typescript +// src/config/config.default.ts +export default { + // ... + orm: { + type: 'sqlite', + database: path.join(__dirname, '../../test.sqlite') + synchronize: true + logging: true + }, +} +``` + + +:::info +Note: synchronize fields are used to synchronize table structures. It is not safe to use `synchronize: true` for production mode synchronization. Please set this field to false after going online. +::: + + +### 8. Use Model to insert database data + + +In common Midway files, use the `@InjectEntityModel` decorator to inject our configured Model. All we need to do is: + + +- 1. Create entity objects +- 2. Execute the `save()` + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from '../entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // save + async savePhoto() { + // create a entity object + let photo = new Photo(); + photo.name = 'Me and Bears'; + photo.description = 'I am near polar bears'; + photo.filename = 'photo-with-bears.jpg'; + photo.views = 1; + photo.isPublished = true; + + // save entity + const photoResult = await this.photoModel.save(photo); + + // save success + console.log('photo id =', photoResult.id); + } +} +``` + + +### 9. Query Data + +For more information, see [find documentation](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/find-options.md). + +The query API has changed since typeorm@0.3.0. + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from '../entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhotos() { + + // find All + let allPhotos = await this.photoModel.find(); // v0.2.x + let allPhotos = await this.photoModel.find({}); // v0.3.x + console.log("All photos from the db: ", allPhotos); + + // find first + let firstPhoto = await this.photoModel.findOne(1); + let firstPhoto = await this.photoModel.findOne({ // v0.3.x + where: { + id: 1 + } + }); + console.log("First photo from the db: ", firstPhoto); + + // find one by name + // v0.2.x + let meAndBearsPhoto = await this.photoModel.findOne({ name: "Me and Bears" }); + // v0.3.x + let meAndBearsPhoto = await this.photoModel.findOne({ + where: { name: "Me and Bears"} + }); + console.log("Me and Bears photo from the db: ", meAndBearsPhoto); + + // find by views + // v0.2.x + let allViewedPhotos = await this.photoModel.find({ views: 1 }); + // v0.3.x + let allViewedPhotos = await this.photoModel.find({ + where: { views: 1} + }); + console.log("All viewed photos: ", allViewedPhotos); + + // v0.2.x + let allViewedPhotos = await this.photoModel.find({ views: 1 }); + // v0.3.x + let allPublishedPhotos = await this.photoModel.find({ + where: { isPublished: true} + }); + console.log("All published photos: ", allPublishedPhotos); + + // find and get count + // v0.2.x + let [allPhotos, photosCount] = await this.photoModel.findAndCount(); + // v0.3.x + let [allPhotos, photosCount] = await this.photoModel.findAndCount({}); + console.log("All photos: ", allPhotos); + console.log("Photos count: ", photosCount); + + } +} + +``` + + +### 10. Update the database + + +Now, let's load a photo from the database, update it and save it. + + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from '../entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + + let photoToUpdate = await this.photoModel.findOne(1); + photoToUpdate.name = "Me, my friends and polar bears"; + + await this.photoModel.save(photoToUpdate); + } +} +``` + + +### 11. Delete data + + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from '../entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + /*...*/ + let photoToRemove = await this.photoModel.findOne(1); // typeorm@0.2.x + await this.photoModel.remove(photoToRemove); + } +} +``` + +Now, Photo with ID = 1 will be deleted from the database. + + +There is also a soft deletion method. + +```typescript +await this.photoModel.softDelete(1); +``` + + +### 12. Create a one-to-one association + + +Let's create a one-to-one relationship with another class. Let's create a new class in `entity/photoMetadata.ts`. This class contains additional meta-information for photo. + + +```typescript +import { Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm'; +import { EntityModel } from '@midwayjs/orm'; +import { Photo } from "./photo"; + +@EntityModel() +export class PhotoMetadata { + + @PrimaryGeneratedColumn() + id: number; + + @Column("int") + height: number; + + @Column("int") + width: number; + + @Column() + orientation: string; + + @Column() + compressed: boolean; + + @Column() + comment: string; + + @OneToOne(type => Photo) + @JoinColumn() + photo: Photo; + +} +``` + + +Here, we use a new fitting called `@OneToOne`. It allows us to create a one-to-one relationship between two entities. `type => photo` is a function that returns the class of the entity with which we want to establish a relationship. + + +Due to the particularity of the language, we are forced to use a function that returns the class instead of using the class directly. You can also write it as `() => Photo`, but we use `type => Photo` as a convention to improve the readability of the code. The type variable itself contains nothing. + + +We also added an `@JoinColumn` decorator, which indicates that this side of the relationship will have the relationship. Relationships can be one-way or two-way. The relationship can only be owned by one party. The owner side of the relationship needs to use the @JoinColumn decorator. If you run the application, you will see a newly generated table that will contain a column containing foreign keys for the Photo relationship. + + +``` ++-------------+--------------+----------------------------+ +| photo_metadata | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| height | int(11) | | +| width | int(11) | | +| comment | varchar(255) | | +| compressed | boolean | | +| orientation | varchar(255) | | +| photoId | int(11) | FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +Next we will associate them in the code. + + +```typescript +import { Provide, Inject, Func } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; +import { PhotoMetadata } from './entity/photoMetadata'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + @InjectEntityModel(PhotoMetadata) + photoMetadataModel: Repository; + + async updatePhoto() { + + // create a photo + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.isPublished = true; + + // create a photo metadata + let metadata = new PhotoMetadata(); + metadata.height = 640; + metadata.width = 480; + metadata.compressed = true; + metadata.comment = "cybershoot"; + metadata.orientation = "portrait"; + metadata.photo = photo; // this way we connect them + + + // first we should save a photo + await this.photoModel.save(photo); + + // photo is saved. Now we need to save a photo metadata + await this.photoMetadataModel.save(metadata); + + // done + console.log("Metadata is saved, and relation between metadata and photo is created in the database too"); + } +} +``` + + +### 13. Reverse relation mapping + + +Relational mapping can be one-way or two-way. When the relationship between PhotoMetadata and Photo is one-way. The owner of the relationship is PhotoMetadata, and Photo knows nothing about PhotoMetadata. This complicates accessing PhotoMetadata from the Photo side. To solve this problem, we add a reverse relational mapping to make the PhotoMetadata and Photo a two-way association. Let's modify our entity. + + +```typescript +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm'; +import { Photo } from './photo'; + +@EntityModel() +export class PhotoMetadata { + + /* ... other columns */ + + @OneToOne(type => Photo, photo => photo.metadata) + @JoinColumn() + photo: Photo; +} +``` + +```typescript +import { EntityModel } from '@midwayjs/orm'; +import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from 'typeorm'; +import { PhotoMetadata } from './photoMetadata'; + +@EntityModel() +export class Photo { + + /* ... other columns */ + + @OneToOne(type => PhotoMetadata, photoMetadata => photoMetadata.photo) + metadata: PhotoMetadata; +} +``` + +`photo => photo.metadata` is a function that returns a reverse mapping relationship. Here, we explicitly declare the metadata property of the Photo class to associate PhotoMetadata. In addition to passing functions that return the photo property, you can also pass strings directly to the `@OneToOne` decorator, such as `"metadata"` . But we use this method of function callback to make our code writing easier. + + +Note that the `@JoinColumn` decorator will only be used on one side of the relationship map. No matter which side of this decorator you place, you are the owner of the relationship. The owner of the relationship contains columns with foreign keys in the database. + + +### 14. Load objects and their dependencies + + +Now, let's try to load out Photo and PhotoMetadata together in a single query. There are two ways to do this, using the `find *` method or using the `QueryBuilder` function. Let's use the `find *` method first. The `find*` methods allow you to specify objects using the `FindOneOptions` / `FindManyOptions` interfaces. + + +```typescript +import { Provide, Inject, Func } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhoto() { + /*...*/ + let photos = await this.photoModel.find({ relations: [ 'metadata' ] }); // typeorm@0.2.x + } +} + +``` + +Here, the value of photos is an array containing the query results for the entire database, and each photo object contains its associated metadata property. Learn more about `Find Options` in [this documentation](https://github.com/typeorm/typeorm/blob/master/docs/find-options.md). + + +`Find Options` is simple, but if you need more complex queries, you should use `QueryBuilder` instead. `QueryBuilder` allows more complex queries to be used in an elegant way. + + +```typescript +import { Provide, Inject, Func } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhoto() { + /*...*/ + let photos = await this.photoModel + .createQueryBuilder('photo') + .innerJoinAndSelect('photo.metadata', 'metadata') + .getMany(); + } +} +``` + +`QueryBuilder` allows the creation and execution of almost any complex SQL query. When using `QueryBuilder`, think like creating SQL queries. In this example, "photo" and "metadata" are aliases applied to the selected photos. You can use aliases to access the columns and properties of the selected data. + + +### 15. Use cascade operations to automatically save associated objects + + +Cascade can be set in the relationship when we want to automatically save the associated object every time we save another object. Let's slightly change the `@OneToOne` decorator of the photo. + + +```typescript +export class Photo { + /// ... other columns + + @OneToOne(type => PhotoMetadata, metadata => metadata.photo, { + cascade: true, + }) + metadata: PhotoMetadata; +} +``` + +Using `cascade` allows us to no longer save Photo and PhotoMetadata separately now. Due to the cascade option, metadata objects will be saved automatically. + + +```typescript +import { Provide, Inject, Func } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; +import { PhotoMetadata } from './entity/photoMetadata'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + + // create photo object + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.isPublished = true; + + // create photo metadata object + let metadata = new PhotoMetadata(); + metadata.height = 640; + metadata.width = 480; + metadata.compressed = true; + metadata.comment = "cybershoot"; + metadata.orientation = "portrait"; + + photo.metadata = metadata; // this way we connect them + + // save a photo also save the metadata + await this.photoModel.save(photo); + + // done + console.log("Photo is saved, photo metadata is saved too"); + } +} +``` + + +Note that we now set the metadata of Photo instead of setting the Photo attribute of metadata as before. This is only valid when you connect Photo to the PhotoMetadata from the Photo side. If set on the PhotoMetadata side, it will not be saved automatically. + + +### 16. Create many-to-one/one-to-many associations + + +Let's create a many-to-one/one-to-many relationship. Suppose a photo has an author, and each author can have many photos. First, let's create an Author class: + +```typescript +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryGeneratedColumn, OneToMany, JoinColumn } from "typeorm"; +import { Photo } from './entity/photo'; + +@EntityModel() +export class Author { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @OneToMany(type => Photo, photo => photo.author) // note: we will create author property in the Photo class below + photos: Photo[]; +} +``` + +`Author` contains a reverse relationship. `OneToMany` and `ManyToOne` need to appear in pairs. + + +Now, add the owner of the relationship to the Photo entity: + +```typescript +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryGeneratedColumn, ManyToOne } from "typeorm"; +import { PhotoMetadata } from "./photoMetadata"; +import { Author } from "./author"; + +@Entity() +export class Photo { + + /* ... other columns */ + + @ManyToOne(type => Author, author => author.photos) + author: Author; +} +``` + + +In a many-to-one/one-to-many relationship, the owner is always many-to-one. This means that the class using the `@ManyToOne` will store the ID of the related object. + + +After the application is run, ORM creates the `author` table: + + +``` ++-------------+--------------+----------------------------+ +| author | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| name | varchar(255) | | ++-------------+--------------+----------------------------+ +``` + +It also modifies the `photo` table, adds a new `author` column, and creates a foreign key for it: + +``` ++-------------+--------------+----------------------------+ +| photo | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| name | varchar(255) | | +| description | varchar(255) | | +| filename | varchar(255) | | +| isPublished | boolean | | +| authorId | int(11) | FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +### 17. Create many-to-many associations + + +Let's create a many-to-one/many-to-many relationship. Suppose a photo can be in many albums, and each album can contain many photos. Let's create an `Album` class. + + +```typescript +import { EntityModel } from '@midwayjs/orm'; +import { PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from "typeorm"; + +@EntityModel() +export class Album { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @ManyToMany(type => Photo, photo => photo.albums) + @JoinTable() + photos: Photo[]; +} +``` + + +`@JoinTable` is used to indicate that this is the owner of the relationship. + + +Now, add the reverse association to `Photo`. + + +```typescript +export class Photo { + /// ... other columns + + @ManyToMany(type => Album, album => album.photos) + albums: Album[]; +} +``` + +After running the application, ORM will create a album_photos_photo_albums join table: + + +``` ++-------------+--------------+----------------------------+ +| album_photos_photo_albums | ++-------------+--------------+----------------------------+ +| album_id | int(11) | PRIMARY KEY FOREIGN KEY | +| photo_id | int(11) | PRIMARY KEY FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +Now, let's insert albums and photos into the database: + + +```typescript +import { Provide, Inject, Func } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; +import { PhotoMetadata } from './entity/photoMetadata'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + @InjectEntityModel(Album) + albumModel: Repository + + async updatePhoto() { + + // create a few albums + let album1 = new Album(); + album1.name = "Bears"; + await this.albumModel.save(album1); + + let album2 = new Album(); + album2.name = "Me"; + await this.albumModel.save(album2); + + // create a few photos + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.albums = [album1, album2]; + await this.photoModel.save(photo); + + + // now our photo is saved and albums are attached to it + // now lets load them: + const loadedPhoto = await this.photoModel.findOne(1, { relations: ["albums"] }); // typeorm@0.2.x + } +} +``` + +The `loadedPhoto` value is: + +```json +{ + id: 1 + name: "Me and Bears ", + description: "I am near polar bears ", + filename: "photo-with-bears.jpg ", + albums: [{ + id: 1 + name: "Bears" + }, { + id: 2 + name: "Me" + }] +} +``` + +### 18. Use QueryBuilder + + +You can use QueryBuilder to build almost any complex SQL query. For example, you can do this: + + +```typescript +let photos = await this.photoModel + .createQueryBuilder("photo") // first argument is an alias. Alias is what you are selecting - photos. You must specify it. + .innerJoinAndSelect("photo.metadata", "metadata") + .leftJoinAndSelect("photo.albums", "album") + .where("photo.isPublished = true") + .andWhere("(photo.name = :photoName OR photo.name = :bearName)") + .orderBy("photo.id", "DESC") + .skip(5) + .take(10) + .setParameters({ photoName: "My", bearName: "Mishka" }) + .getMany(); +``` + +The query selects all published photos with "My" or "Mishka" names. It will return results (paging offset) from position 5, and only 10 results (paging limit) will be selected. The selection results will be sorted in descending order of ID. The photo album will be left-Joined and metadata will be automatically associated. + + +You will use query generators extensively in your application. Learn more about QueryBuilder [here](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/select-query-builder.md). + + +### 19. Event Subscriber + + +typeorm provides an event subscription mechanism to facilitate log output when doing some database operations. For this reason, midway provides a `EventSubscriberModel` decorator to label event subscription classes with the following code. + + +```typescript +import { Provide } from '@midwayjs/core'; +import { EventSubscriberModel } from '@midwayjs/orm'; +import { EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm'; + +@Provide() +@EventSubscriberModel() +export class EverythingSubscriber implements EntitySubscriberInterface { + + /** + * Called before entity insertion. + */ + beforeInsert(event: InsertEvent) { + console.log('BEFORE ENTITY INSERTED:', event.entity); + } + + /** + * Called before entity insertion. + */ + beforeUpdate(event: UpdateEvent) { + console.log('BEFORE ENTITY UPDATED:', event.entity); + } + + /** + * Called before entity insertion. + */ + beforeRemove(event: RemoveEvent) { + console.log('BEFORE ENTITY WITH ID ${event.entityId} REMOVED:', event.entity); + } + + /** + * Called after entity insertion. + */ + afterInsert(event: InsertEvent) { + console.log('AFTER ENTITY INSERTED:', event.entity); + } + + /** + * Called after entity insertion. + */ + afterUpdate(event: UpdateEvent) { + console.log('AFTER ENTITY UPDATED:', event.entity); + } + + /** + * Called after entity insertion. + */ + afterRemove(event: RemoveEvent) { + console.log('AFTER ENTITY WITH ID ${event.entityId} REMOVED:', event.entity); + } + + /** + * Called after entity is loaded. + */ + afterLoad(entity: any) { + console.log('AFTER ENTITY LOADED:', entity); + } + +} +``` + + +This subscription class provides some common interfaces to perform some things during database operations. + +### 20. OrmConnectionHook + +In versions prior to 3.4.0 (not included), the Midway package provided a Hook mechanism for monitoring database connection and disconnection events; the code is as follows. + +```typescript +import { Provide } from '@midwayjs/core'; +import { OrmConnectionHook, OrmHook } from '@midwayjs/orm'; +import { Connection, ConnectionOptions } from 'typeorm'; + +@Provide() +@OrmHook() +export class OrmConnectionListener implements OrmConnectionHook { + /** + * Called before connection create + * @param opts + * @returns + */ + async beforeCreate(opts?: ConnectionOptions): Promise { + console.log('BEFORE CONNECTION CREATE'); + return opts; + } + + /** + * Called after connection create + * @param conn + * @param opts + * @returns + */ + async afterCreate(conn?: Connection, opts?: ConnectionOptions): Promise { + console.log('AFTER CONNECTION CREATE'); + return conn; + } + + /** + * Called before connection close + * @param conn + * @param connectionName + * @returns + */ + async beforeClose(conn?: Connection, connectionName?: string): Promise { + console.log('BEFORE CONNECTION CLOSE'); + return conn; + } + + /** + * Called after connection close + * @param conn + * @returns + */ + async afterClose(conn?: Connection): Promise { + console.log('AFTER CONNECTION CLOSE'); + return conn; + } +} +``` + + + + +## Advanced features + +### Multi-database support + + +Sometimes, we have multiple database connections (Connection) in an application, and there will be multiple configurations at this time. We use the **object form** to define the configuration. + + +For example, the following defines two database connections (Connection), `default` and `test`. + + +```typescript +import {join} from 'path'; + +export default { + orm: { + default: { + type: 'sqlite', + database: join(__dirname, '../../default.sqlite') + logging: true + }, + test: { + type: 'mysql', + host: '127.0.0.1', + port: 3306 + username: '*********', + password: '*********', + database: undefined + synchronize: true + logging: false + } + } +} +``` + + +In use, you need to specify which connection (Connection) the model belongs. + +```typescript +// entity/photo.ts +import { InjectEntityModel } from '@midwayjs/orm'; +import { User } from './model/user'; + +export class XXX { + + @InjectEntityModel(User, 'test') + testUserModel: Repository; + + //... +} +``` + + +Similarly, when using the injection Model, you need to specify the connection. + + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; + +@EntityModel('photo', { + connectionName: 'test' +}) +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + + + + +### Get connection pool + +```typescript +import { Configuration } from '@midwayjs/core'; +import { getConnection } from 'typeorm'; + +@Configuration() +export class MainConfiguration { + async onReady() { + const conn = getConnection('default'); + console.log(conn.isConnected); + } +} +``` + + +### Hooks scenario support + + +For the scenario of functional programming, we provide a simplified functional writing. + + +```typescript +import { useEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; + +export async function getPhoto() { + // get model + const photoModel = useEntityModel(Photo); + + const photo = new Photo(); + // create entity + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.views = 1; + photo.isPublished = true; + + // find + const newPhoto = await photoModel.save(photo); + + return 'hello world'; +} +``` + + +### About Table Structure Synchronization + + +- If you already have a table structure, you want to automatically create an Entity and use the [Generator](../tool/typeorm_generator) +- If you already have Entity code, use the `synchronize: true` in the configuration to create a table structure. + +## Frequently Asked Questions + + +### Handshake inactivity timeout + + +Generally, it is due to network reasons. If it appears locally, you can ping but telnet is not available. You can try to execute the following command: + +```bash +$sudo sysctl -w net.inet.tcp.sack=0 +``` + +### About the current time zone display of mysql time column + + +If you use the `@UpdateDateColumn` and `@CreateDateColumn` columns, UTC time is normally saved in the database. If you want to return the time in the current time zone, you can use the following method. + + +When configuring, turn on the time-to-string option. + +```typescript +// src/config/config.default.ts +export default { + // ... + orm: { + //... + dateStrings: true + }, +} +``` + + +The time column in the entity requires a column type. + +```typescript +@EntityModel() +export class Photo { + //... + @UpdateDateColumn({ + name: "gmt_modified ", + type: 'timestamp' + }) + gmtModified: Date; + + @CreateDateColumn({ + name: "gmt_create ", + type: 'timestamp' + }) + gmtCreate: Date; +} +``` + +In this way, the output time field is the current time zone. + + +The effect is as follows: + + +**Before configuring:** + +```typescript +gmtModified: 2021-12-13T03:49:43.000Z +gmtCreate: 2021-12-13T03:49:43.000Z +``` + +**After configuration:** + +```typescript +gmtModified: '2021-12-13 11:49:43', +gmtCreate: '2021-12-13 11:49:43' +``` + + + + +### Default values for time columns + + +If the `@UpdateDateColumn` and `@CreateDateColumn` columns are used, note that the default value is typeorm automatically added to the table-building statement. if the table is self-built, the field will be written to 00:00:00 because there is no default value. + + +There are two solutions: **1. Modify the default value of a table** or **2. Modify the default value of a column in the code** + +**If you don't want to modify the table, but want to modify the code, please refer to the code below. ** + +```typescript +@Column({ + default: () => "NOW() ", + type: 'timestamp' +}) +createdOn: Date; + +@Column({ + default: () => "NOW() ", + type: 'timestamp' +}) +modifiedOn: Date; +``` + + + +### Install mysql and mysql2 at the same time + +when both mysql and mysql2 are present in the node_modules, the typeorm automatically loads mysql instead of mysql2. + +If you need to use mysql2 at this time, please specify driver. + +```typescript +// src/config/config.default.ts +export default { + // ... + orm: { + //... + type: 'mysql', + driver: require('mysql2') + }, +} +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/sequelize.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/sequelize.md new file mode 100644 index 000000000000..33d2671a276a --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/sequelize.md @@ -0,0 +1,283 @@ +# Sequelize + +:::tip +This document is obsolete from v3.4.0. +::: + +This document describes how to use Sequelize modules in Midway. + +Related information: + +| Description | | +| ----------------- | --- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ✅ | +| Can be used for integration | ✅ | + +## Usage: + +```bash +$ npm i @midwayjs/sequelize@3 sequelize --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/sequelize": "^3.0.0", + "sequelize": "^6.13.0" + // ... + }, + "devDependencies": { + // ... + } +} +``` + +## Install database Driver + +The commonly used database drivers are as follows. Select the database type to install the corresponding connection: + +```bash +# for MySQL or MariaDB, you can also use mysql2 instead +npm install mysql --save +npm install mysql2 --save + +# for PostgreSQL or CockroachDB +npm install pg --save + +# for SQLite +npm install sqlite3 --save + +# for Microsoft SQL Server +npm install mssql --save + +# for SQL .js +npm install SQL .js --save + +# for Oracle +npm install oracledb --save + +# for MongoDB(experimental) +npm install mongodb --save +``` + +## Introduction module + +In the configuration.ts file + +```typescript +import { App, Configuration, ILifeCycle } from '@midwayjs/core'; +import { Application } from '@midwayjs/web'; +import { join } from 'path'; +import * as sequelize from '@midwayjs/sequelize'; + +@Configuration({ + imports: [sequelize] + importConfigs: [join(__dirname, './config')] +}) +export class MainConfiguration implements ILifeCycle { + @App() + app: Application; + + async onReady() {} +} +``` + +## Configuration + +Configure in config.default.ts: + +```typescript +// src/config/config.default.ts +export default { + // ... + sequelize: { + dataSource: { + default: { + database: 'test4', + username: 'root', + password: '123456', + Host: '127.0.0.1', // here supports the way key is vipserver above idb, and aliyun's address is also supported. + port: 3306 + encrypt: false + dialect: 'mysql', + define: { charset: 'utf8'} + timezone: '+08:00', + logging: console.log + }, + }, + sync: false, // local, you can directly createTable it through sync: true + }, +}; +``` + +## Business layer + +### Define Entity + +```typescript +import { Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript'; +import { BaseTable } from '@midwayjs/sequelize'; +import { User } from './User'; + +@BaseTable +export class Photo extends Model { + @ForeignKey(() => User) + @Column({ + comment: 'User Id', + }) + userId: number; + @BelongsTo(() => User) user: User; + + @Column({ + comment: 'name', + }) + name: string; +} +``` + +```typescript +import { Model, Column, HasMany } from 'sequelize-typescript'; +import { BaseTable } from '@midwayjs/sequelize'; +import { Photo } from './Photo'; + +@BaseTable +export class User extends Model { + @Column name! : string; + @HasMany(() => Photo) Photo: Photo[]; +} +``` + +### Use Entity: + +#### Query list + +```typescript +import { Config, Controller, Get, Provide } from '@midwayjs/core'; +import { Photo } from '../entity/Photo'; + +@Provide() +@Controller('/') +export class HomeController { + @Get('/') + async home() { + let result = await Photo.findAll(); + console.log(result); + return 'hello world'; + } +} +``` + +Add data: + +```typescript +import { Controller, Post, Provide } from '@midwayjs/core'; +import { Photo } from '../entity/Photo'; + +@Provide() +@Controller('/') +export class HomeController { + @Post('/add') + async home() { + let result = await Photo.create({ + name: '123', + }); + console.log(result); + return 'hello world'; + } +} +``` + +#### Delete: + +```typescript +import { Controller, Post, Provide } from '@midwayjs/core'; +import { Photo } from '../entity/Photo'; + +@Provide() +@Controller('/') +export class HomeController { + @Post('/delete') + async home() { + await Photo.destroy({ + where: { + name: '123', + }, + }); + return 'hello world'; + } +} +``` + +#### Find individual: + +```typescript +import { Controller, Post, Provide } from '@midwayjs/core'; +import { Photo } from '../entity/Photo'; + +@Provide() +@Controller('/') +export class HomeController { + @Post('/delete') + async home() { + let result = await Photo.findOne({ + where: { + name: '123', + }, + }); + return 'hello world'; + } +} +``` + +#### Joint enquiries: + +```typescript +import { Controller, Get, Provide } from '@midwayjs/core'; +import { Photo } from '../entity/Photo'; +import { Op } from 'sequelize'; + +@Provide() +@Controller('/') +export class HomeController { + @Get('/') + async home() { + // SELECT * FROM photo WHERE name = "23" OR name = "34"; + let result = await Photo.findAll({ + where: { + [Op.or]: [{ name: '23' }, { name: '34' }], + }, + }); + console.log(result); + return 'hello world'; + } +} +``` + +#### table query + +```typescript +import { Controller, Get, Provide } from '@midwayjs/core'; +import { User } from '../entity/User'; +import { Photo } from '../entity/Photo'; + +@Provide() +@Controller('/users') +export class HomeController { + @Get('/') + async home() { + let result = await User.findAll({ include: [Photo] }); + console.log(result); + return 'hello world'; + } +} +``` + +More ways to use OP: [https:// sequelize.org/v5/manual/querying.html](https://sequelize.org/v5/manual/querying.html) + +Midway + sequelize Complete Use Case [https:// github.com/ddzyan/midway-practice](https://github.com/ddzyan/midway-practice) + +If you encounter more complicated ones, you can use the raw query method: +[https://sequelize.org/v5/manual/raw-queries.html](https://sequelize.org/v5/manual/raw-queries.html) diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/task.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/task.md new file mode 100644 index 000000000000..3ec7378eeb7c --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/legacy/task.md @@ -0,0 +1,580 @@ +# Task scheduling + +:::tip +This document is obsolete from v3.6.0. +::: + +@midwayjs/task is a module to solve task series, such as distributed scheduled tasks and delayed task scheduling. For example, daily regular report mail delivery, order failure after 2 hours, etc. + +Distributed scheduled tasks depend on bull, which is implemented through redis. Therefore, additional Redis needs to be configured in the configuration. Local scheduled tasks are based on Cron module and do not need additional configuration. + +Related information: + +| Description | | +| ----------------- | ---- | +| Can be used for standard projects | ✅ | +| Can be used for Serverless | ❌ | +| Can be used for integration | ✅ | + +**Other** + +| Description | | +| -------------------- | ---- | +| Can be used independently as the main frame | ✅ | +| Contains custom logs | ✅ | +| Middleware can be added independently | ❌ | + + + +## Installation dependency + +First install the task components provided by Midway: + +```bash +$ npm install @midwayjs/task@3 @types/bull --save +``` + +Or reinstall the following dependencies in `package.json`. + +```json +{ + "dependencies": { + "@midwayjs/task": "^3.0.0", + // ... + }, + "devDependencies": { + "@types/bull": "^3.15.8 ", + // ... + } +} +``` + + + +## Introducing components + +In `configuration.ts`, introduce this component: + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as task from '@midwayjs/task'; //Import module +import { join } from 'path'; + +@Configuration({ + imports: [task], + importConfigs: [join(__dirname, 'config')] +}) +export class MainConfiguration { +} +``` + + + +## Distributed timing task + +This is our most common way of timing tasks. + +Distributed timed tasks can be distributed across multiple processes and multiple machines can execute a single timed task. + +The distributed definition task depends on the Redis service and needs to be applied in advance. + + + +### Configuration + +Configure the corresponding module information in the `config.default.ts` file: + +```typescript +// src/config/config.default.ts +export default { + // ... + task: { + redis: 'redis:// 127.0.0.1:32768', // the task depends on redis, so a redis needs to be added here. + prefix: 'midway-task', // the keys stored in these tasks start with midway-task to distinguish the configurations in the user's original redis. + defaultJobOptions: { + repeat: { + Tz: "Asia/Shanghai" // Task and other parameters such as (0 0 0 * * *) were originally set for 0 o'clock, but because the time zone is not correct, the time zone for domestic users is set. + }, + }, + }, +} +``` + +Account password: + +```typescript +// src/config/config.default.ts +export default { + // ... + task: { + // ioredis configuration https://www.npmjs.com/package/ioredis + redis: { + port: 6379 + host: '127.0.0.1', + password: 'foobared', + }, + prefix: 'midway-task', // the keys stored in these tasks start with midway-task to distinguish the configurations in the user's original redis. + defaultJobOptions: { + repeat: { + Tz: "Asia/Shanghai" // Task and other parameters such as (0 0 0 * * *) were originally set for 0 o'clock, but because the time zone is not correct, the time zone for domestic users is set. + }, + }, + }, +} +``` + + + +### Code usage + +```typescript +import { Provide, Inject, Task, FORMAT } from '@midwayjs/core'; + +@Provide() +export class UserService { + @Inject() + helloService: HelloService; + + // For example, the following is executed every minute and is a distributed task + @Task({ + repeat: { cron: FORMAT.CRONTAB.EVERY_MINUTE} + }) + async test() { + console.log(this.helloService.getName()) + } +} +``` + +### Setting progress + +For example, when we do audio and video or publish such time-consuming tasks, we hope to set the progress. + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01WPYaAz21NgV3VNzjV_!!6000000006973-2-tps-576-454.png) + +equivalent to the second parameter, the job of bull is passed to the user. Users can set the progress through the `job.progress`. + + +Then query the progress: + +```typescript +import { QueueService } from '@midwayjs/task'; +import { Provide, Controller, Get } from '@midwayjs/core'; + +@Controller() +export class HelloController { + @Inject() + queueService: QueueService; + + @Get("/get-queue") + async getQueue(@Query() id: string) { + return await this.queueService.getClassQueue(TestJob).getJob(id); + } +} +``` + +### The relevant content of the task + +```typescript +let job = await this.queueService.getClassQueue(TestJob).getJob(id) +``` + +Then there is a similar way to stop or check the progress on the job. + + + +### Triggered when started + + +Some friends hope to perform the corresponding timing tasks immediately after restarting because there is only one machine. + +```typescript +import { Configuration, Context, ILifeCycle, IMidwayBaseApplication, IMidwayContainer } from '@midwayjs/core'; +import { Queue } from 'bull'; +import { join } from 'path'; +import * as task from '@midwayjs/task'; +import { QueueService } from '@midwayjs/task'; + +@Configuration({ + imports: [ + task + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration implements ILifeCycle { + + async onServerReady(container: IMidwayContainer, app?: IMidwayBaseApplication): Promise { + + // Task will be executed immediately after it is started. + let result: QueueService = await container.getAsync(QueueService); + // Here the first one is the class name of your task, and the second one is the function name of the decorator Task + let job: Queue = result.getQueueTask('HelloTask', 'task') + // Indicates immediate execution. + job.add({}, {delay: 0, repeat: null}) + + // The LocalTask will be executed immediately after it is started. + const result = await container.getAsync(QueueService); + let job = result.getLocalTask('HelloTask', 'task'); //Parameter 1: Class Name Parameter 2: Function Name TaskLocal by Decorator + job(); // indicates immediate execution + } +} + +``` + + + +## Common Cron expressions + +About Task Task Task Configuration: + +```text +* * * * * * +┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ | +│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) +│ │ │ │ └───── month (1 - 12) +│ │ │ └────────── day of month (1 - 31) +│ │ └─────────────── hour (0 - 23) +│ └──────────────────── minute (0 - 59) +└───────────────────────── second (0 - 59, optional) +``` + +Common expressions: + + +- run every 5 seconds: `*/5 * * * *` +- run every 1 minute:`0 */1 * * * *` +- run every 20 minutes per hour: `0 20 * * *` +- run every day at 0 o'clock: `0 0 0 * *` +- Execute every day at 2:35: `0 35 2 * *` + +You can use the [online tool](https://cron.qqe2.com/) to confirm the time of the next execution. + + + +Midway provides some commonly used expressions on the frame side for everyone to use in `@midwayjs/core`. + +```typescript +import { FORMAT } from '@midwayjs/core'; + +// cron expressions executed per minute +FORMAT.CRONTAB.EVERY_MINUTE +``` + +There are some other expressions built in. + +| Expression | corresponding time | +| ------------------------------ | --------------- | +| CRONTAB.EVERY_SECOND | Every second | +| CRONTAB.EVERY_MINUTE | Every minute | +| CRONTAB.EVERY_HOUR | Every hour on the hour | +| CRONTAB.EVERY_DAY | 0 o'clock every day | +| CRONTAB.EVERY_DAY_ZERO_FIFTEEN | At 0:15 every day | +| CRONTAB.EVERY_DAY_ONE_FIFTEEN | At 1:15 every day | +| CRONTAB.EVERY_PER_5_SECOND | Every 5 seconds | +| CRONTAB.EVERY_PER_10_SECOND | Every 10 seconds | +| CRONTAB.EVERY_PER_30_SECOND | Every 30 seconds | +| CRONTAB.EVERY_PER_5_MINUTE | Every 5 minutes | +| CRONTAB.EVERY_PER_10_MINUTE | Every 10 minutes | +| CRONTAB.EVERY_PER_30_MINUTE | Every 30 minutes | + + + +## Manually trigger tasks + +The definition of a task, through the `@Queue` decorator, defines a task class, which must contain an `async execute()` method. +```typescript +import { Provide, Inject, Queue } from '@midwayjs/core'; + +@Queue() +export class HelloTask { + async execute(params) { + console.log(params); + } +} +``` + + +Trigger: +```typescript +import { QueueService } from '@midwayjs/task'; +import { Provide, Inject } from '@midwayjs/core'; + +@Provide() +export class UserTask { + @Inject() + queueService: QueueService; + + async execute(params = {}) { + // Triggers distributed task scheduling after 3 seconds. + const xxx = await this.queueService.execute(HelloTask, params, {delay: 3000}); + } +} +``` +After 3 seconds, the HelloTask task will be triggered. + +:::tip + +Note that if it is not triggered, please check the params above to ensure that it is not empty. + +::: + + + +## Operation and Maintenance + +### Log +On the Midway Task Component, two logs have been added: + +- midway-task.log +- midway-task-error.log + + +The corresponding logs are printed when the task, localTask, and queue trigger starts and ends respectively. + +Basic configuration of task log: +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; +export default { + midwayLogger: { + default: { + // ... + }, + clients: { + coreLogger: { + // ... + }, + appLogger: { + // ... + }, + taskLog: { + disableConsole: false, // whether to disable printing to the console, disabled by default + level: 'warn', + consoleLevel: 'warn', + }, + } + }, +} as MidwayConfig; +``` +Distributed Task Trigger Log: +```typescript +logger.info('task start.') + +// Exception: +logger.error(err.stack) + +logger.info('task end.') +``` +Non-distributed LocalTask trigger logs: +```typescript +logger.info('local task start.') + +// Exception: +// logger.error('${e.stack}') + +logger.info('local task end.') +``` + + +Trigger log for task queue: +```typescript +logger.info('queue process start.') + +// Exception: +// logger.error('${e.stack}') + +logger.info('queue process end.') +``` + + +### Troubleshoot problem links: +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01xL1mQE25kMZnB5ygb_!!6000000007564-2-tps-1614-847.png) +you can search for the same id to find the log of the same request. +In order to facilitate users to concatenate the corresponding logs in their business codes, I hung traceId variables on ctx. + +For example, abnormal situation: when abnormal, the **local can see this error-related situation in the console and midway-task.log bar:** + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01WYBjbL1lGKHmsdSnH_!!6000000004791-2-tps-1964-324.png) + + + +### traceId + +The localTask generates a UUID ID as a traceId. + + +Task and queue use the ID of the job as the traceId. + + + +### Code within the business + +In the service, you can inject logger through inject or inject ctx to get logger variables. +```typescript +import { App, Inject, Provide, Queue } from '@midwayjs/core'; +import { Application } from "@midwayjs/koa"; + +@Queue() +export class QueueTask { + + @App() + app: Application; + + @Inject() + logger; + + async execute(params) { + this.logger.info('====>QueueTask execute') + this.app.getApplicationContext().registerObject('queueConfig', JSON.stringify(params)); + } +} + +``` +or +```typescript +import { App, Inject, Provide, Queue } from '@midwayjs/core'; +import { Application } from "@midwayjs/koa"; + +@Queue() +export class QueueTask { + + @App() + app: Application; + + @Inject() + ctx; + + async execute(params) { + this.ctx.logger.info('====>QueueTask execute') + this.app.getApplicationContext().registerObject('queueConfig', JSON.stringify(params)); + } +} + +``` + + +Printed log +```typescript +2021-07-30 13:00:13,101 INFO 5577 [Queue][12][QueueTask] queue process start. +2021-07-30 13:00:13,102 INFO 5577 [Queue][12][QueueTask] ====>QueueTask execute +2021-07-30 13:00:13,102 INFO 5577 [Queue][12][QueueTask] queue process end. +``` + + + +## Local timed task + +Unlike distributed tasks, local timed tasks do not need to rely on and configure Redis, and can only do single-process tasks, that is, each process of each machine will be executed. + +```typescript +import { Provide, Inject, TaskLocal, FORMAT } from '@midwayjs/core'; + +@Provide() +export class UserService { + @Inject() + helloService: HelloService; + + // For example, the following is executed every minute + @TaskLocal(FORMAT.CRONTAB.EVERY_MINUTE) + async test() { + console.log(this.helloService.getName()) + } +} +``` + + + + + +## Frequently Asked Questions + + + +### 1. EVALSHA error + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01KfjCKT1yypmNPDkIL_!!6000000006648-2-tps-3540-102.png) + +This problem is basically clear. The problem will appear on the cluster version of redis. The reason is that redis will hash the key to determine the stored slot. In this step of the cluster @midwayjs/task, the key hit a different slot. The temporary solution is to use the prefix configuration in the task to include {}, and force redis to calculate only the hash in {}, for example, `prefix: '{midway-task}'`. + + + +### 2. Delete historical log + +When Redis is executed every time, he will have a log, so how to delete it after completion: +```typescript +import { Provide, Task } from '@midwayjs/core'; +import { IUserOptions } from '../interface'; + +@Provide() +export class UserService { + async getUser(options: IUserOptions) { + return { + uid: options.uid + username: 'mockedName', + phone: '12345678901', + email: 'xxx.xxx@xxx.com', + }; + } + + @Task({ + repeat: { cron: '* * * * * *'} + removeOnComplete: true // added a line of this + }) + async test() { + console.log('====') + } +} + +``` +Whether it is deleted by default, you need to communicate with the user. + + + +### 3. Configure the Redis cluster + +You can use the `createClient` method provided by bull to access the custom redis instance, so that you can access the Redis cluster. + +For example: + +```typescript +// src/config/config.default +import Redis from 'ioredis'; + +const clusterOptions = { + enableReadyCheck: false, // must be false + retryDelayOnClusterDown: 300 + retryDelayOnFailover: 1000 + retryDelayOnTryAgain: 3000 + slotsRefreshTimeout: 10000 + maxRetriesPerRequest: null // must be null +} + +const redisClientInstance = new Redis.Cluster ([ + { + port: 7000 + host: '127.0.0.1' + }, + { + port: 7002 + host: '127.0.0.1' + }, +], clusterOptions); + +export default { + task: { + createClient: (type, opts) => { + return redisClientInstance; + }, + prefix: '{midway-task}', // the keys stored in these tasks are all at the same beginning, so as to distinguish the configurations in the user's original redis. + defaultJobOptions: { + repeat: { + Tz: "Asia/Shanghai" // Task and other parameters such as (0 0 0 * * *) were originally set for 0 o'clock, but because the time zone is not correct, the time zone for domestic users is set. + } + } + } +} +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/lifecycle.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/lifecycle.md new file mode 100644 index 000000000000..9f08ba3f80e9 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/lifecycle.md @@ -0,0 +1,443 @@ +# Life cycle + +Under normal circumstances, we want to do some initialization or other pre-processing things when the application starts, such as creating a database connection and pre-generating some configuration, instead of processing it when requesting a response. + + + +## Project life cycle + +The framework provides these lifecycle functions for developers to handle: + +- Configuration file loading, we can modify the configuration here (`onConfigLoad`) +- When the dependent injection container is ready, most things can be done at this stage (`onReady`) +- After the service is started, you can get the server( `onServerReady`) +- The application is about to be shut down. Here, clean up the resources (`onStop` ). + + +Midway's life cycle is to implement the ILifeCycle interface through the `src/configuration.ts` file, which can be automatically loaded when the project starts. + + +The interface is defined as follows. + + +```typescript +interface ILifeCycle { + /** + * Execute after the application configuration is loaded + */ + onConfigLoad?(container: IMidwayContainer, app: IMidwayApplication): Promise; + + /** + * Execute when relying on the injection container ready + */ + onReady(container: IMidwayContainer, app: IMidwayApplication): Promise; + + /** + * Execute after the application service is started + */ + onServerReady?(container: IMidwayContainer, app: IMidwayApplication): Promise; + + /** + * Execute when the application stops + */ + onStop?(container: IMidwayContainer, app: IMidwayApplication): Promise; +} +``` + + + +### onConfigLoad + +Generally used to modify the configuration file of the project. + +For example. + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onConfigLoad(): Promise { + // The data returned directly will be automatically merged into the configuration. + return { + test: 1 + } + } +} +``` + +In this case, the `@Config` configuration contains the returned data. For more information, see [Asynchronous initialization configuration](./env_config# Asynchronous Initialization Configuration). + + + +### onReady + +onReady is a life cycle that is used in most scenarios. + +:::info +Note that ready here refers to the dependency injection container ready, not the application ready, so you can make any extension to the application, such as adding middleware, connecting databases, etc. +::: + + +We need to connect a database in advance during initialization. Since it is in the class, we can also inject the connection tool class of a database such as db through the `@Inject` decorator. This instance contains two functions, connect and close: + + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + @Inject() + db: any; + + async onReady(container: IMidwayContainer): Promise { + // Establish a database connection + await this.db.connect(); + } + + async onStop(): Promise { + // Close database connection + await this.db.close(); + } +} +``` + + +In this way, we can establish the database connection when the application starts, rather than creating it when the response is requested. At the same time, when the application is stopped, the database connection can also be closed gracefully. + + +In addition, in this way, the default injected objects can be expanded. + + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer } from '@midwayjs/core'; +import * as sequelize from 'sequelize'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onReady(container: IMidwayContainer): Promise { + // Three-party package object + container.registerObject('sequelize', sequelize); + } +} +``` + + +It can be directly injected into other classes. + + +```typescript +export class IndexHandler { + + @Inject() + sequelize; + + async handler() { + console.log(this.sequelize); + } +} +``` + + + +### onServerReady + +This lifecycle is needed when you want to get information about the framework's service objects, ports, and so on. + +Let's take `@midwayjs/koa` as an example to get its Server at startup. + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + imports: [koa] +}) +export class MainConfiguration implements ILifeCycle { + + async onServerReady(container: IMidwayContainer): Promise { + // Obtain the exposed Framework in koa + const framework = await container.getAsync(koa.Framework); + const server = framework.getServer(); + // ... + + } +} +``` + + + +### onStop + +We can clean up some resources at this stage, such as closing the connection. + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + imports: [koa] +}) +export class MainConfiguration implements ILifeCycle { + @Inject() + db: any; + + async onReady(container: IMidwayContainer): Promise { + // Establish a database connection + await this.db.connect(); + } + + async onStop(): Promise { + // Close database connection + await this.db.close(); + } +} +``` + + + +### onHealthCheck + +When the built-in health check service calls the status retrieval API, this method is automatically executed for all components. + +The following simulates a db health check method. + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, HealthResult } from '@midwayjs/core'; + +@Configuration({ + namespace: 'db' +}) +export class MainConfiguration implements ILifeCycle { + @Inject() + db: any; + + async onReady(container: IMidwayContainer): Promise { + await this.db.connect(); + } + + async onHealthCheck(): Promise { + try { + const result = await this.db.isConnect(); + if (result) { + return { + status: true, + }; + } else { + return { + status: false, + reason: 'db is disconnected', + }; + } + } catch (err) { + return { + status: false, + reason: err.message, + }; + } + } +} +``` + +In the above `onHealthCheck`, a status check of `isConnect` is called, and a fixed `HealthResult` type format is returned based on the result. + +Note that external calls to `onHealthCheck` may be very frequent. Please keep the check logic as reliable and efficient as possible to ensure that there is no greater pressure on check dependencies. At the same time, please handle the logic of resource release after the check timeout by yourself to avoid the risk of memory leaks caused by frequent resource requests without returning results. + + + +## Global Object Lifecycle + +The so-called object life cycle refers to the event that each object is created and destroyed in the dependency injection container. Through these life cycles, we can do some operations when the object is created and destroyed. + +```typescript +export interface IObjectLifeCycle { + onBeforeObjectCreated(/**...**/); + onObjectCreated(/**...**/); + onObjectInit(/**...**/); + onBeforeObjectDestroy(/**...**/); +} +``` + +These stages are already included in the `ILifeCycle` definition. + +:::caution + +Note that the object lifecycle API will affect the entire dependency injection container and the use of the business. Please operate with caution. + +::: + +### onBeforeObjectCreated + +Before the business object instance is created, some objects inside the framework cannot be intercepted because they have been initialized. + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectBeforeCreatedOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onBeforeObjectCreated(Clzz: new (...args), options: ObjectBeforeCreatedOptions): Promise { + // ... + } +} +``` + +There are two parameters in the entry parameter: + +- `Clzz` is the prototype class of the object to be created. +- `options` some parameters + +The parameters are as follows: + +| Property | Type | Description | +| ----------------------- | ----------------- | ---------------- | +| options.context | IMidwayContainer | Dependent injection container itself | +| options.definition | IObjectDefinition | Object definition | +| options.constructorArgs | any[] | Constructor input parameter | + + + +### onObjectCreated + +Execute after the object instance is created, this stage can replace the created object. + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectCreatedOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onObjectCreated(ins: any, options: ObjectCreatedOptions): Promise { + // ... + } +} +``` + +There are two parameters in the entry parameter: + +- `ins` is the object created by the builder. +- `options` some parameters + +The parameters are as follows: + +| Property | Type | Description | +| ----------------------- | ------------------ | ------------------ | +| options.context | IMidwayContainer | Dependent injection container itself | +| options.definition | IObjectDefinition | Object definition | +| options.replaceCallback | (ins: any) => void | Callback method for object replacement | + +**Example: dynamically add attributes** + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectInitOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onObjectCreated(ins: any, options: ObjectInitOptions): Promise { + // Each created object will add a_name attribute + ins._name = 'xxxx'; + // ... + } +} +``` + +**Example: Replace an object** + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectInitOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onObjectCreated(ins: any, options: ObjectInitOptions): Promise { + // Each created object will be replaced with {bbb: 'aaa'} + options.replaceCallback({ + bbb: 'aaa' + }); + + // ... + } +} +``` + + + +### onObjectInit + +Execute after the asynchronous initialization method is executed after the object instance is created. + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectInitOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onObjectInit(ins: any, options: ObjectInitOptions): Promise { + // ... + } +} +``` + +There are two parameters in the entry parameter: + +- `ins` is the object created by the builder. +- `options` some parameters + +The parameters are as follows: + +| Property | Type | Description | +| ------------------ | ----------------- | ---------------- | +| options.context | IMidwayContainer | Dependent injection container itself | +| options.definition | IObjectDefinition | Object definition | + +:::info + +At this stage, you can also dynamically attach attributes, methods, etc. to objects. The difference with `onObjectCreated` is that this stage is after the initialization method is executed. + +::: + + + +### onBeforeObjectDestroy + +Execute before the object instance is destroyed. + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectBeforeDestroyOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onBeforeObjectDestroy(ins: any, options: ObjectBeforeDestroyOptions): Promise { + // ... + } +} +``` + +There are two parameters in the entry parameter: + +- `ins` is the object created by the builder. +- `options` some parameters + +The parameters are as follows: + +| Property | Type | Description | +| ------------------ | ----------------- | ---------------- | +| options.context | IMidwayContainer | Dependent injection container itself | +| options.definition | IObjectDefinition | Object definition | + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/logger.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/logger.md new file mode 100644 index 000000000000..44c9e0e65755 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/logger.md @@ -0,0 +1,869 @@ +# Logger + +:::tip + +This document is for `@midwayjs/logger` v2.0 version. + +::: + +Midway provides a unified log access method for different scenarios. The `@midwayjs/logger` package export method allows you to easily access log systems in different scenarios. + +Midway's log system is based on the [winston](https://github.com/winstonjs/winston) of the community and is now a very popular log library in the community. + +The functions realized are: + +- Log classification +- Automatic cutting by size and time +- Custom output format +- Unified error log + + + +## Loger path and file + +Midway creates some default files in the log root directory. + + +- `midway-core.log` logs of printed information of the framework and components, corresponding to the `coreLogger`. +- `midway-app.log` applies the log of printing information, corresponding to the `appLogger` +- `common-error.log` The log of all errors (all logs created by Midway will repeatedly print errors to this file) + +The **Log Path** and **Log Level** of local development and server deployment are different. For more information, see [Configure Log Root](# Configure the log root directory) and [Default Level](# The default level of the framework). + + + +## Default loger object + +Midway provides three different logs in the framework by default, corresponding to three different behaviors. + +| Log | Interpretation | Description | Common use | +| ----------------------------------- | -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| coreLogger | Framework, component-level logs | By default, the console log and text log `midway-core.log` are output, and the error log is sent to `common-error.log` by default. | Frames and component errors are generally printed into them. | +| appLogger | Logs at the business level | The `midway-app.log` of the console log and text log is output by default, and the error log is sent to `common-error.log` by default. | The log used by the business. Generally, the business log will be printed in it. | +| Context Logger (Configuration of Multiplexing appLogger) | Log of request link | By default, `appLogger` is used for output. In addition to sending error logs to `common-error.log`, context information is added. | Modify the label (Label) of log output. Different frameworks have different request labels. For example, under HTTP, routing information will be output. | + + + +## Use log + +Midway's common log usage method. + +### Context log + +The context log is the log associated with the framework context object (Context). + +You can [obtain the ctx object](./req_res_app) and then use the `ctx.logger` object to print and output logs. + +For example: + +```typescript +ctx.logger.info("hello world"); +ctx.logger.debug('debug info'); +ctx.logger.warn('WARNNING!!!!'); + +// Error log recording will directly record the complete stack information of the error log and output it to the errorLog +// In order to ensure that exceptions can be traced, all exceptions thrown must be of Error type, because only Error type will bring stack information to locate the problem. +ctx.logger.error(new Error('custom error')); +``` + +After execution, we can see the log output in two places: + + +- The console sees the output. +- In the midway-app.log file of the log directory + + +Output result: +```text +2021-07-22 14:50:59,388 INFO 7739 [-/::ffff:127.0.0.1/-/0ms GET /api/get_user] hello world +``` + +In the injection form, you can also use `@Inject() logger` to inject `ctx.logger`, which is equivalent to calling `ctx.logger` directly. + +For example: + +```typescript +import { Get, Inject, Controller, Provide } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Controller() +export class HelloController { + + @Inject() + logger: ILogger; + + @Inject() + ctx; + + @Get("/") + async hello() { + // ... + + // this.logger === ctx.logger + } +} +``` + + + +### App Logger + +If we want to do some application-level logging, such as recording some data information during the startup phase, we can do it through App Logger. + +```typescript +import { Configuration, Logger } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + @Logger() + logger: ILogger; + + async onReady(container: IMidwayContainer): Promise { + this.logger.debug('debug info'); + This.logger.info ('startup takes% d ms', Date.now() - start); + this.logger.warn('warning!'); + + this.logger.error(someErrorObj); + } + +} +``` + +Note that the `@Logger()` decorator is used here. + + + +### CoreLogger + +In research and development at the component or framework level, we will use coreLogger to log. + +```typescript + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + @Logger('coreLogger') + logger: ILogger; + + async onReady(container: IMidwayContainer): Promise { + this.logger.debug('debug info'); + This.logger.info ('startup takes% D MS', Date.now() -Start); + this.logger.warn('warning!'); + + this.logger.error(someErrorObj); + } + +} +``` + + + + + + +## Output method and format + + +The log object of Midway inherits the log object of the winston. In general, only four methods are provided: `error()`, `war ()`, `info()`, and `debug`. + + +An example is as follows. +```typescript +logger.debug('debug info'); +logger.info('startup takes% d ms', Date.now() - start); +logger.warn('warning!'); +logger.error(new Error('my error')); +``` + + +### Default output behavior + + +In most common types, the logstore works well. + + +For example: +```typescript +logger.info('hello world'); // Output string +logger.info(123); //Output Number +logger.info(['B', 'c']); // Output array +logger.info(new Set([2, 3, 4])); // Output Set +logger.info(new Map([['key1', 'value1'], ['key2', 'value2']])); // Output Map +``` +> Midway has specially customized the `Array`, `Set`, and `Map` types that winston cannot output to enable them to output normally. + + +However, it should be noted that under normal circumstances, the log object can only pass in one parameter, and its second parameter has other functions. +```typescript +logger.info('plain error message', 321); // 321 will be ignored +``` + + +### Error output + + +For the wrong object, Midway has also customized the winston so that it can be easily combined with ordinary text for output. +```typescript +// Output error object +logger.error(new Error('error instance')); + +// Output custom error object +const error = new Error('named error instance'); +error.name = 'NamedError'; +logger.error(error); + +// Text before, plus error instance +logger.info('text before error', new Error('error instance after text')); +``` +:::caution +Note that the error object can only be placed at the end, and there is only one, and all parameters after it will be ignored. +::: + + + + +### Format content +The format method based on `util.format`. +```typescript +logger.info('%s %d', 'aaa', 222); +``` +Commonly used are + + +- The `%s` string is occupied. +- `%d` digit occupancy +- `%j` json placeholder + +For more information, see the [util.format](https://nodejs.org/dist/latest-v14.x/docs/api/util.html#util_util_format_format_args) method of Node. js. + + + +### Output custom objects or complex types + + +Based on performance considerations, Midway(winston) only outputs basic types most of the time, so when the output parameter is an advanced object, the user **needs to manually convert it to a string** that needs to be printed. + + +The following example will not get the desired result. +```typescript +const obj = {a: 1}; +logger.info(obj); // By default, output [object Object] +``` +You need to manually output what you want to print. +```typescript +const obj = {a: 1}; +logger.info(JSON.stringify(obj)); // formatted text can be output +logger.info(obj.a); // Direct output attribute value +logger.info('%j', a); // Direct placeholder output entire json +``` + + + +### Pure output content + + +In special scenarios, we need to simply output content, and do not want to output timestamps, labels and other format-related information. For this requirement, we can use the `write` method. + +The `write` method is a very low-level method, and no matter what level of logs are written to the file. + + +Although the `write` method is available on every logger, we only provide it in the `IMidwayLogger` definition, and we hope you can clearly know that you want to call it. +```typescript +(logger as IMidwayLogger).write('hello world'); // There will only be hello world in the file +``` + + + +## Log type definition + + +By default, users should use the simplest `ILogger` definition. +```typescript +import { Provide, Logger } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Provide() +export class UserService { + + @Inject() + logger: ILogger; // Get context log + + async getUser() { + this.logger.info('hello user'); + } + +} +``` + + +The `ILogger` definition provides only the simplest `debug`, `info`, `WARN`, and `error` methods. + + +In some scenarios, we need more complex definitions, such as modifying log attributes or dynamically adjusting. At this time, we need to use more complex `IMidwayLogger` definitions. + + +```typescript +import { Provide, Logger } from '@midwayjs/core'; +import { IMidwayLogger } from '@midwayjs/logger'; + +@Provide() +export class UserService { + + @Inject() + logger: IMidwayLogger; // Get context log + + async getUser() { + This. Logger. disableConsole(); // Prohibit console output + this.logger.info('hello user'); // This sentence is not visible in the console + This. Logger. enableConsole(); // Turn on console output + this.logger.info('hello user'); // This sentence can be seen in the console + } + +} +``` +The definition of the `IMidwayLogger` can refer to the description in the interface or view the [code](https://github.com/midwayjs/logger/blob/main/src/interface.ts). + + + +## Basic log configuration + +We can configure various behaviors of the log in the configuration file. + +The log configuration in Midway includes **Global Configuration** and **Single Log Configuration**. The two configurations are merged and overwritten. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + // ... + }, + clients: { + coreLogger: { + // ... + }, + appLogger: { + // ... + } + } + }, +} as MidwayConfig; +``` + +As mentioned above, each object in the `clients` configuration segment is an independent log configuration item, and its configuration will be merged with the `default` segment to create a logger instance. + + + +## Configure log level + + +The winston log levels are divided into the following categories, and the log levels decrease in turn (the larger the number, the lower the level): +```typescript +const levels = { + none: 0 + error: 1 + trace: 2 + warn: 3 + info: 4 + verbose: 5 + debug: 6 + silly: 7 + all: 8 +} +``` +In order to simplify the Midway, we usually use only four levels: `error`, `war`, `info`, and `debug`. + +The log level represents the lowest level that can currently output logs. For example, if your log level is set to `WARN`, only logs of the `WARN` and higher `error` level can be output. + +In Midway, different log levels can be configured for different output behaviors. + +- `Level` Log Level of Text Written +- `consoleLevel` the log level output from the console + + + +### The default level of the framework + + +In Midway, it has its own default log level. + + +- In the development environment (local,test,unittest), the text and console log levels are unified to `info`. +- In the server environment (except the development environment), in order to reduce the number of logs, the log level of `coreLogger` is `warn`, while other logs are `info`. + + + +### Adjust log level + +In general, we do not recommend adjusting the global default log level, but adjust the log level of a specific logger, for example: + +Adjust `coreLogger` or `appLogger`. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + clients: { + coreLogger: { + level: 'warn', + consoleLevel: 'warn' + // ... + }, + appLogger: { + level: 'warn', + consoleLevel: 'warn' + // ... + } + } + }, +} as MidwayConfig; +``` + +In special scenarios, you can also temporarily adjust the global log level. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + level: 'info', + consoleLevel: 'warn' + }, + // ... + }, +} as MidwayConfig; +``` + + + +## Configure the log root directory + +By default, Midway outputs logs to the **root directory** during local development and server deployment. + + +- The root directory of the local log is `${app.appDir}/logs/project name`. +- The log root directory of the server is under the user directory `${process.env.HOME}/logs/project_name` (Linux/Mac) and `${process.env.USERPROFILE}/logs/project_name` (Windows), for example `/home/admin/logs/example-app`. + +We can configure the root directory where the log is located. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + dir: '/home/admin/logs', + }, + // ... + }, +} as MidwayConfig; +``` + + + +## Configure log cutting (rotation) + + +By default, the same log object **generates two files**. + +Take `midway-core.log` as an example. When the application is started, a `midway-core with the timestamp of the day is generated. YYYY-MM files in-DD` format and a soft chain file of `midway-core.log` without timestamp. + +> Soft chain will not be generated under windows + + +To facilitate log collection and viewing, the soft chain file always points to the latest log file. + + +At `00:00` in the morning, a new file of the form `midway-core.log.YYYY-MM-DD` is generated at the end of the day's log. + +At the same time, when a single log file exceeds 200M, it will be automatically cut to generate a new log file. + +You can adjust the cutting behavior by configuration. + +```typescript +export default { + midwayLogger: { + default: { + maxSize: '100m', + }, + // ... + }, +} as MidwayConfig; +``` + + + +## Configure log cleanup + +By default, the log will exist for 31 days. + +This behavior can be adjusted by configuration, such as saving for 3 days instead. + +```typescript +} as MidwayConfig;export default { + midwayLogger: { + default: { + maxFiles: '3d', + }, + // ... + }, +} as MidwayConfig; +``` + + + + + + +## Advanced configuration + +If you are not satisfied with the default log object, you can create and modify it yourself. + + + +### Add custom log + +It can be configured as follows: + +```typescript +export default { + midwayLogger: { + clients: { + abcLogger: { + fileLogName: 'abc.log' + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +You can call `@Logger('abcLogger')` to obtain custom logs. + +For more log options, please refer to the [LoggerOptions description](https://github.com/midwayjs/logger/blob/main/src/interface.ts) in the interface. + + + +### Configure log output format + + +The display format refers to the string structure of a single line of text when the log is output. Midway has customized Winston logs and provided some default objects. + +For each logger object, you can configure an output format. The display format is a method that returns a string structure with the [info object](https://github.com/winstonjs/logform#info-objects) parameter of the Winston. + +```typescript +export default { + midwayLogger: { + clients: { + appLogger: { + format: info => { + return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.labelText}${info.message}`; + } + // ... + }, + customOtherLogger: { + format: info => { + return 'xxxx'; + } + } + } + // ... + }, +} as MidwayConfig; +``` + +The default properties of the info object are as follows: + +| **Attribute Name** | **Description** | **Example** | +| ----------- | ------------------------------------------------ | ------------------------------------------------------------ | +| timestamp | The timestamp. Default value: `'YYYY-MM-DD HH:mm:ss,SSS`. | 2020-12-30 07:50:10,453 | +| level | Lowercase log level | info | +| LEVEL | Uppercase log level | INFO | +| pid | current process pid | 3847 | +| labelText | Aggregate text for labels | [abcde] | +| message | Combination of normal messages + error messages + error stacks | 1. plain text, such as `123456`, `hello world`
2, error text (error name + stack) error: another test error at object. anonymous (/home/runner/work/midway/packages/logger/test/index.test.ts:224:18)
3, plain text + error text hello world error: another test error at object. anonymous (/home/runner/work/midway/midway/packages/logger/test/index.test.ts:224:18) | +| stack | Error stack | | +| originError | Original error object | The error instance itself | +| originArgs | Original user input parameters | ['a', 'B', 'c'] | + + + +### Get a custom context log + +Context logs are typed based on **raw log objects**. All formats of the original logs are reused. The relationship between them is as follows. + +```typescript +// Pseudocode +const contextLogger = customLogger.createContextLogger(ctx); +``` + +`@Inject` can only inject the default context logs. You can use the `ctx.getLogger` method to obtain the **context logs** corresponding to other **custom logs**. the context log is associated with ctx, and the same key in the same context will obtain the same log object. when ctx is destroyed, the log object will also be recycled. + +```typescript +import { Provide } from '@midwayjs/core'; +import { IMidwayLogger } from '@midwayjs/logger'; +import { Context } from '@midwayjs/koa'; + +@Provide() +export class UserService { + + @Inject() + ctx: Context; + + async getUser() { + // The context log object corresponding to the customLogger is obtained here. + const customLogger = this.ctx.getLogger('customLogger'); + customLogger.info('hello world'); + } + +} +``` + + + + +### Configure the context log output format + +Context logs are typed based on the **original log object**. All formats of the original log are reused. However, you can configure the corresponding context log format of the log object separately. + +There are more ctx objects in the info object of the context log. Let's take the context log of the `customLogger` as an example. + +```typescript +export default { + midwayLogger: { + clients: { + customLogger: { + contextFormat: info => { + const ctx = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} [${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`; + } + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +Then when you use the context log output, it will default to your format. + +```typescript +ctx.getLogger('customLogger').info('hello world'); +// 2021-01-28 11:10:19,334 INFO 9223 [2ms POST] hello world +``` + +Note that because `App Logger` is the default log object for all frameworks, it is relatively special. Some existing frameworks have their context formats configured by default, resulting in invalid configuration in `midwayLogger` fields. + +For this, you need to modify the context log format configuration of a framework separately, please jump to a different framework to view. + +- [Modify the koa context log format](./extensions/koa# Modify Context Log) +- [Modify the context log format of the egg](./extensions/egg# Modify Context Log) +- [Modify express context log format](./extensions/express# Modify Context Log) + + + +### Log default Transport + +Each log contains several default Transport. + +| Name | Default behavior | Description | +| ----------------- | -------- | ------------------------------ | +| Console Transport | Open | For output to console | +| File Transport | Open | For output to a text file | +| Error Transport | Open | Used to output errors to specific error logs | +| JSON Transport | Close | Text used to output JSON format | + +It can be modified through configuration. + +**Example: Only enable console output** + +```typescript +export default { + midwayLogger: { + clients: { + abcLogger: { + enableFile: false + enableError: false + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +**Example: Disable Console Output** + +```typescript +export default { + midwayLogger: { + clients: { + abcLogger: { + enableConsole: false + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +**Example: Enable text and JSON synchronization and disable error output** + +```typescript +export default { + midwayLogger: { + clients: { + abcLogger: { + enableConsole: false + enableFile: true + enableError: false + enableJSON: true + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + + + +### Custom Transport + +The framework provides extended Transport functions, for example, you can write a Transport to transfer logs and upload them to other log libraries. + +For example, in the following example, we will transfer the log to another local file. + +```typescript +import { EmptyTransport } from '@midwayjs/logger'; + +class CustomTransport extends EmptyTransport { + log(info, callback) { + const levelLowerCase = info.level; + if (levelLowerCase === 'error' || levelLowerCase === 'warn') { + writeFileSync(join(logsDir, 'test.log'), info.message); + } + callback(); + } +} +``` + +We can initialize, add it to logger, or set level for Transport separately. + +```typescript +const customTransport = new CustomTransport({ + level: 'warn', +}); + +logger.add(customTransport); +``` + +In this way, the original logger will automatically execute the Transport when printing logs. + +All Transport are attached to the original logger instance (not context logger). If ctx data is required, it can be obtained from info. Note that it is empty. + + +```typescript +class CustomTransport extends EmptyTransport { + log(info, callback) { + if (info.ctx) { + // ... + } else { + // ... + } + callback(); + } +} +``` + + +We can also use dependency injection to define Transport. + +```typescript +import { EmptyTransport, IMidwayLogger } from '@midwayjs/logger'; +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; +import { MidwayLoggerService } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum) +export class CustomTransport extends EmptyTransport { + log(info, callback) { + // ... + callback(); + } +} + +// src/configuration.ts +@Configuration(/*...*/) +export class MainConfiguration { + + @Inject() + loggerService: MidwayLoggerService; + + @Inject() + customTransport: CustomTransport; + + async onReady() { + const appLogger = this.loggerService.getLogger('customLogger') as IMidwayLogger; + appLogger.add(this.customTransport); + } +} +``` + + + +### Lazy initialization + +The log can be initialized lazily using the `lazyLoad` configuration. + +for example: + +```typescript +export default { + midwayLogger: { + clients: { + customLoggerA: { + level: 'DEBUG', + }, + customLoggerB: { + lazyLoad: true, + }, + } + //... + }, +} as MidwayConfig; +``` + +`customLoggerA` will be initialized immediately when the framework starts, and `customLoggerB` will be initialized when the business actually uses `getLogger` or `@Logger` injection for the first time. + +This feature is very suitable for dynamically creating logs, but configurations want to be merged together. + + + +## Frequently Asked Questions + + + +### 1. The server environment log is not output + +For the server environment, the default log level is warn, that is, `logger.warn` will print out. please check the "log level" section. + +We do not recommend printing too many logs in the server environment, only printing the necessary content, too much log output affects performance, but also affects the rapid positioning problem. + + + +### 2. The server does not have a console log + +Generally speaking, the server console log (console) is closed and will only be output to the file. If there are special requirements, it can be adjusted separately. + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/logger_v3.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/logger_v3.md new file mode 100644 index 000000000000..ca9017b3a2ad --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/logger_v3.md @@ -0,0 +1,1052 @@ +# Logger + +Midway provides a unified log access method for different scenarios. Through the `@midwayjs/logger` package export method, you can easily access the logging system in different scenarios. + +The functions implemented are: + +- Log classification +- Automatic cutting by size and time +- Custom output format +- Unified error log + +:::tip + +The current version of the log SDK documentation is 3.0. If you need version 2.0, please check [this document](/docs/logger). + +::: + + + +## Upgrade from 2.0 to 3.0 + +Starting from midway v3.13.0, the 3.0 version of `@midwayjs/logger` is supported. + +Upgrade the dependency versions in `package.json`, pay attention to the `dependencies` dependencies. + +```diff +{ + "dependencies": { +- "@midwayjs/logger": "2.0.0", ++ "@midwayjs/logger": "^3.0.0" + } +} +``` + +If there is no type hint for midwayLogger in the configuration, you need to add a reference to the log library in `src/interface.ts`. + +```diff +// src/interface.ts ++ import type {} from '@midwayjs/logger'; +``` + +In most scenarios, the two versions are compatible, but since it is a major version upgrade, there will definitely be some differences. For the complete Breaking Change, please view the [Change Document](https://github.com/midwayjs /logger/blob/main/BREAKING-3.md). + + + +## Logger path and file + +Midway will create some default files in the log root directory. + + +- `midway-core.log` is the log of information printed by the framework and components, corresponding to `coreLogger`. +- `midway-app.log` is the log of application printing information, corresponding to `appLogger`. In `@midawyjs/web`, the file is `midway-web.log` +- `common-error.log` All error logs (all logs created by Midway will repeatedly print errors to this file) + +The **log path** and **log level** are different between local development and server deployment. For details, please refer to [Configuration log root directory](#Configuration log root directory) and [Framework’s default level](#Framework’s default grade). + + + +##Default log object + +Midway provides three different logs in the framework by default, corresponding to three different behaviors. + +| Log | Definition | Description | Common Usage | +| ---------------------------------------------- | ------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| coreLogger | Framework, component level logs | Console logs and text logs `midway-core.log` will be output by default, and error logs will be sent to `common-error.log` by default. | Errors in frameworks and components are generally printed to it. | +| appLogger | Business-level logs | Console logs and text logs `midway-app.log` will be output by default, and error logs will be sent to `common-error.log` by default, in `@midawyjs/web`, The file is `midway-web.log`. | Log used by business, generally business logs will be printed into it. | +| Context logger (reuse appLogger configuration) | Request link log | By default, `appLogger` is used for output. In addition to sending the error log to `common-error.log`, context information is also added. | Different protocols have different request log formats. For example, routing information will be output under HTTP. | + + + +## Usage logger + +Common log usage methods for Midway. + +### Context logger + +The context log is a log associated with the framework context object (Context). + +We can use the `ctx.logger` object to print logs after [obtaining the ctx object](./req_res_app). + +for example: + +```typescript +ctx.logger.info("hello world"); +ctx.logger.debug('debug info'); +ctx.logger.warn('WARNNING!!!!'); + +// Error logging will directly record the complete stack information of the error log and output it to errorLog. +// In order to ensure that exceptions are traceable, it must be ensured that all thrown exceptions are of type Error, because only type Error will bring stack information and locate the problem. +ctx.logger.error(new Error('custom error')); +``` + +After execution, we can see log output in two places: + + +- The console sees the output. +- midway-app.log file in the log directory + + +Output result: + +```text +2021-07-22 14:50:59,388 INFO 7739 [-/::ffff:127.0.0.1/-/0ms GET /api/get_user] hello world +``` + +In the form of injection, we can also directly use the form of `@Inject() logger` to inject `ctx.logger`, which is equivalent to directly calling `ctx.logger`. + +for example: + +```typescript +import { Get, Inject, Controller, Provide } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Controller() +export class HelloController { + + @Inject() + logger: ILogger; + + @Inject() + ctx; + + @Get("/") + async hello(){ + // ... + + // this.logger === ctx.logger + } +} +``` + + + +### App Logger + +If we want to do some application-level logging, such as recording some data information during the startup phase, we can do it through App Logger. + +```typescript +import { Configuration, Logger } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + @Logger() + logger: ILogger; + + async onReady(container: IMidwayContainer): Promise { + this.logger.debug('debug info'); + this.logger.info('Startup took %d ms', Date.now() - start); + this.logger.warn('warning!'); + + this.logger.error(someErrorObj); + } + +} +``` + +Note that the `@Logger()` decorator is used here. + + + +### CoreLogger + +In component or framework level development, we will use coreLogger to record logs. + +```typescript +@Configuration() +export class MainConfiguration implements ILifeCycle { + + @Logger('coreLogger') + logger: ILogger; + + async onReady(container: IMidwayContainer): Promise { + this.logger.debug('debug info'); + this.logger.info('Startup took %d ms', Date.now() - start); + this.logger.warn('warning!'); + + this.logger.error(someErrorObj); + } + +} +``` + + + + +## Output method and format + + +Midway's log object provides five methods: `error()`, `warn()`, `info()`, `debug()`, and `write()`. + + +Examples are as follows. + +```typescript +logger.debug('debug info'); +logger.info('Startup takes %d ms', Date.now() - start); +logger.warn('warning!'); +logger.error(new Error('my error')); +logger.write('abcdef'); +``` + +:::tip + +The `write` method is used to output the user's original format log. + +::: + + + +Formatting method based on `util.format`. + +```typescript +logger.info('%s %d', 'aaa', 222); +``` + +Commonly used ones include + + +- `%s` string placeholder +- `%d` digital placeholder +- `%j` json placeholder + +For more placeholders and details, please refer to the [util.format](https://nodejs.org/dist/latest-v14.x/docs/api/util.html#util_util_format_format_args) method of node.js. + + + +## Logger type definition + + +In most cases, users should use the simplest `ILogger` definition in `@midwayjs/core`. + +```typescript +import { Provide, Logger, ILogger } from '@midwayjs/core'; + +@Provide() +export class UserService { + + @Inject() + logger: ILogger; + + async getUser() { + this.logger.info('hello user'); + } +} +``` + +The `ILogger` definition only provides the simplest `debug`, `info`, `warn` and `error` methods. + + +In some scenarios, we need more complex definitions. In this case, we need to use the `ILogger` definition provided by `@midwayjs/logger`. + + +```typescript +import { Provide, Logger } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Provide() +export class UserService { + + @Inject() + logger: ILogger; + + async getUser() { + // ... + } + +} +``` + +`ILogger`The definition can refer to the description in interface, or view [code](https://github.com/midwayjs/logger/blob/main/src/interface.ts). + + + +## Logger configuration + + + +### Basic configuration structure + +We can configure various log behaviors in the configuration file. + +The log configuration in Midway includes two parts: **global configuration** and **individual log configuration**. The two configurations will be merged and overwritten. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + // ... + }, + clients: { + coreLogger: { + // ... + }, + appLogger: { + // ... + } + } + }, +} as MidwayConfig; +``` + +As mentioned above, each object in the `clients` configuration section is an independent log configuration item, and its configuration will be merged with the `default` section to create a logger instance. + + + +### Default Transport + +In logger module, four Transports `console`, `file`, `error` and `json` are built-in by default. Among them, Midway enables `console`, `file` and `error` by default. More information can be configured through to modify. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + transports: { + console: { + // console transport configuration + }, + file: { + // file transport configuration + }, + error: { + // error transport configuration + }, + } + }, + // ... + }, +} as MidwayConfig; +``` + +If a transport is not required, it can be set to `false`. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + transports: { + console: false, + } + }, + // ... + }, +} as MidwayConfig; +``` + + + +### Configure log level + +In Midway, under normal circumstances, we only use four levels: `error`, `warn`, `info`, and `debug`. + +The log level indicates the lowest level that can currently output logs. For example, when your log level is set to `warn`, only `warn` and higher `error` level logs can be output. + + +Midway has its own default log level. + + +- In the development environment (local, test, unittest), the text and console log levels are unified to `info`. +- In a server environment, in order to reduce the number of logs, the log level of `coreLogger` is `warn`, while other logs are `info`. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + level: 'info', + }, + // ... + }, +} as MidwayConfig; +``` + + + +The level of the logger and the level of the Transport can be set separately. The level of the Transport has a higher priority than the level of the logger. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + // level of logger + level: 'info', + transports: { + file: { + //level of file transport + level: 'warn' + } + } + }, + // ... + }, +} as MidwayConfig; +``` + + + +We can also adjust the log level of a specific logger, such as: + +Adjust `coreLogger` or `appLogger`. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + clients: { + coreLogger: { + level: 'warn', + // ... + }, + appLogger: { + level: 'warn', + // ... + } + } + }, +} as MidwayConfig; +``` + +In special scenarios, the global log level can also be temporarily adjusted. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + level: 'info', + transports: { + console: { + level: 'warn' + } + } + }, + // ... + }, +} as MidwayConfig; +``` + + + +### Configure log root directory + +By default, Midway will output logs to the **log root** during local development and server deployment. + + +- The local log root directory is under the `${app.appDir}/logs/project name` directory +- The server's log root directory is under the user directory `${process.env.HOME}/logs/project name` (Linux/Mac) and `${process.env.USERPROFILE}/logs/project name` (Windows), For example `/home/admin/logs/example-app`. + +We can configure the root directory where the log is located. Note that all Transport paths must be modified. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + transports: { + file: { + dir: '/home/admin/logs', + }, + error: { + dir: '/home/admin/logs', + }, + } + }, + // ... + }, +} as MidwayConfig; +``` + + + +### Configure log cutting (rotation) + + +Under the default behavior, the same log object **will generate two files**. + +Taking `midway-core.log` as an example, when the application starts, it will generate a file in the format of `midway-core.YYYY-MM-DD` with a timestamp of the day, and a `midway-core.log` without a timestamp. soft link file. + +> Soft links will not be generated under windows + + +To facilitate the configuration of log collection and viewing, the soft link file always points to the latest log file. + + +When `00:00` is reached in the morning, a new file will be generated in the form of `midway-core.log.YYYY-MM-DD` ending with the current day's log. + +At the same time, when a single log file exceeds 200M, it will be automatically cut and a new log file will be generated. + +Cutting by size behavior can be adjusted through configuration. + +```typescript +export default { + midwayLogger: { + default: { + transports: { + file: { + maxSize: '100m', + }, + error: { + maxSize: '100m', + }, + } + }, + // ... + }, +} as MidwayConfig; +``` + + + +### Configure log cleaning + +By default, logs exist for 7 days. + +This behavior can be adjusted through configuration, such as saving for 3 days instead. + +```typescript +export default { + midwayLogger: { + default: { + transports: { + file: { + maxFiles: '3d', + }, + error: { + maxFiles: '3d', + }, + } + }, + // ... + }, +} as MidwayConfig; +``` + +You can also configure a number to indicate the maximum number of log files to retain. + +```typescript +export default { + midwayLogger: { + default: { + transports: { + file: { + maxFiles: '3', + }, + error: { + maxFiles: '3d', + }, + } + }, + // ... + }, +} as MidwayConfig; +``` + +### Configure custom logs + +It can be configured as follows: + +```typescript +export default { + midwayLogger: { + clients: { + abcLogger: { + fileLogName: 'abc.log' + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +Customized logs can be obtained through `@Logger('abcLogger')`. + +For more logging options, please refer to [LoggerOptions Description](https://github.com/midwayjs/logger/blob/main/src/interface.ts) in interface. + + + +### Configure log output format + + +The display format refers to the string structure of a single line of text when outputting logs. + +Each logger object can be configured with an output format. The display format is a method that returns a string structure, and the parameter is an info object. + +```typescript +import { LoggerInfo } from '@midwayjs/logger'; + +export default { + midwayLogger: { + clients: { + appLogger: { + format: (info: LoggerInfo) => { + return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.labelText}${info.message}`; + } + // ... + }, + customOtherLogger: { + format: (info: LoggerInfo) => { + return 'xxxx'; + } + } + } + // ... + }, +} as MidwayConfig; +``` + +The default properties of the info object are as follows: + +| **Attribute name** | **Description** | **Example** | +| ------------------ | ------------------------------------------------------------ | ----------------------- | +| timestamp | Timestamp, default is `'YYYY-MM-DD HH:mm:ss,SSS` format. | 2020-12-30 07:50:10,453 | +| level | Lowercase log level | info | +| LEVEL | uppercase log level | INFO | +| pid | current process pid | 3847 | +| message | result of util.format | | +| args | Original user input parameters | [ 'a', 'b', 'c' ] | +| ctx | Context object associated when using ContextLogger | | +| originError | Original error object, obtained after traversing parameters, poor performance | error instance itself | +| originArgs | Same as args, only compatible with older versions | | + + + + + +### Get custom context log + +Context log is logged based on **original log object** and will reuse all formats of the original log. Their relationship is as follows. + +```typescript +// pseudocode +const contextLogger = customLogger.createContextLogger(ctx); +``` + +`@Inject` can only inject the default context log. We can obtain the **context log** corresponding to other **custom log** through the `ctx.getLogger` method. The context log is associated with ctx. The same context and the same key will obtain the same log object. When ctx is destroyed, the log object will also be recycled. + +```typescript +import { Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Provide() +export class UserService { + + @Inject() + ctx: Context; + + async getUser() { + // What is obtained here is the context log object corresponding to customLogger + const customLogger = this.ctx.getLogger('customLogger'); + customLogger.info('hello world'); + } + +} +``` + + + + +### Configure context log output format + +The context log is based on the **original log object** and will reuse all the formats of the original log, but we can separately configure the corresponding context log format of the log object. + +There is an additional ctx object in the info object of the context log. Let's take modifying the context log of `customLogger` as an example. + +```typescript +export default { + midwayLogger: { + clients: { + customLogger: { + contextFormat: info => { + const ctx = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} [${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`; + } + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +Then when you use context log output, it will become your format by default. + +```typescript +ctx.getLogger('customLogger').info('hello world'); +// 2021-01-28 11:10:19,334 INFO 9223 [2ms POST] hello world +``` + +Note that since `App Logger` is the default log object of all frameworks and is quite special, some existing frameworks configure its context format by default, causing the configuration in the `midwayLogger` field to be invalid. + +To do this, you need to modify the context log format configuration of a certain framework separately. Please jump to a different framework to view it. + +- [Modify koa's context log format](./extensions/koa#Modify context log) +- [Modify egg's context log format](./extensions/egg#Modify context log) +- [Modify the context log format of express](./extensions/express#Modify the context log) + + + +### Configure delayed initialization + +The log can be initialized lazily using the `lazyLoad` configuration. + +for example: + +```typescript +export default { + midwayLogger: { + clients: { + customLoggerA: { + // .. + }, + customLoggerB: { + lazyLoad: true, + }, + } + // ... + }, +} as MidwayConfig; +``` + +`customLoggerA` will be initialized immediately when the framework starts, while `customLoggerB` will be initialized when the business actually uses `getLogger` or `@Logger` injection for the first time. + +This function is very suitable for scenarios where logs are dynamically created, but the configurations are expected to be merged together. + + + +### Configure associated logs + +The log object can be configured with an associated log object name. + +for example: + +```typescript +export default { + midwayLogger: { + clients: { + customLoggerA: { + aliasName: 'customLoggerB', + // ... + }, + } + // ... + }, +} as MidwayConfig; +``` + +When using the API to retrieve, the same log object will be retrieved with different names. + +```typescript +app.getLogger('customLoggerA') => customLoggerA +app.getLogger('customLoggerB') => customLoggerA +``` + + + +### Configure console output color + +When outputting to the console, if the command line supports color output, different colors will be output for different log levels. If color is not supported, it will not be displayed. + +You can turn off color output directly through configuration. + +```typescript +export default { + midwayLogger: { + default: { + transports: { + console: { + autoColors: false, + } + } + } + // ... + }, +} as MidwayConfig; +``` + + + +### Configure JSON output + +By enabling the `json` Transport, the logs can be output in JSON format. + +For example, all loggers are turned on. + +```typescript +export default { + midwayLogger: { + default: { + transports: { + file: false, + json: { + fileLogName: 'midway-app.json.log' + } + } + } + // ... + }, +} as MidwayConfig; +``` + +Or a single logger is enabled. + +```typescript +export default { + midwayLogger: { + default: { + // ... + }, + clients: { + appLogger: { + transports: { + json: { + // ... + } + } + } + } + }, +} as MidwayConfig; +``` + +The configuration format of `json` Transport is the same as `file`, but the output is slightly different. + +For example, we can modify the output content in `format`. By default, the output will contain at least the `level` and `pid` fields. + +```typescript +export default { + midwayLogger: { + default: { + transports: { + json: { + format: (info: LoggerInfo & {data: string}) => { + info.data = 'custom data'; + return info; + } + } + } + } + // ... + }, +} as MidwayConfig; +``` + +The output is: + +```text +{"data":"custom data","level":"info","pid":89925} +{"data":"custom data","level":"debug","pid":89925} +``` + + + +## Custom Transport + +The framework provides the function of extending Transport. For example, you can write a Transport to transfer logs and upload them to other log libraries. + + + +### Inherit existing Transport + +If writing to a new file, this can be achieved by using `FileTransport`. + +```typescript +import { FileTransport, isEnableLevel, LoggerLevel, LogMeta } from '@midwayjs/logger'; + +// Transport configuration +interface CustomOptions { + // ... +} + +class CustomTransport extends FileTransport { + log(level: LoggerLevel | false, meta: LogMeta, ...args) { + // Determine whether level satisfies the current Transport + if (!isEnableLevel(level, this.options.level)) { + return; + } + + // Format the message using built-in formatting methods + let buf = this.format(level, meta, args) as string; + //Add newline character + buf += this.options.eol; + + //Write the log you want to write + if (this.options.bufferWrite) { + this.bufSize += buf.length; + this.buf.push(buf); + if (this.buf.length > this.options.bufferMaxLength) { + this.flush(); + } + } else { + // If caching is not enabled, write directly + this.logStream.write(buf); + } + } +} +``` + +Before use, it needs to be registered in the log library. + +```typescript +import { TransportManager } from '@midwayjs/logger'; + +TransportManager.set('custom', CustomTransport); +``` + +You can then use this Transport in your configuration. + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + transports: { + custom: { + dir: 'xxxx', + fileLogName: 'xxx', + // ... + } + } + } + }, +} as MidwayConfig; +``` + +In this way, the original logger will automatically execute the Transport when printing logs. + + + +### Fully customized Transport + +In addition to writing files, logs can also be delivered to remote services. For example, in the following example, the logs are forwarded to another service. + +Note that Transport is an operation that can be executed asynchronously, but the logger itself will not wait for Transport to execute and return. + +```typescript +import { Transport, ITransport, LoggerLevel, LogMeta } from '@midwayjs/logger'; + + +// Transport configuration +interface CustomOptions { + // ... +} + +class CustomTransport extends Transport implements ITransport { + log(level: LoggerLevel | false, meta: LogMeta, ...args) { + // Format the message using built-in formatting methods + let msg = this.format(level, meta, args) as string; + + //Asynchronously write to the log library + remoteSdk.send(msg).catch(err => { + // Log the error or ignore it + console.error(err); + }); + } +} +``` + + + +## Dynamic API + +Dynamically obtain the log object through the `getLogger` method. + +```typescript +// Get coreLogger +const coreLogger = app.getLogger('coreLogger'); +// Get the default contextLogger +const contextLogger = ctx.getLogger(); +// Get the contextLogger created by a specific logger, equivalent to customALogger.createContextLogger(ctx) +const customAContextLogger = ctx.getLogger('customA'); +``` + +The framework's built-in `MidwayLoggerService` also has the above API. + +```typescript +import { MidwayLoggerService } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Provide() +export class MainConfiguration { + + @Inject() + loggerService: MidwayLoggerService; + + @Inject() + ctx: Context; + + async getUser() { + // get custom logger + const customLogger = this.loggerService.getLogger('customLogger'); + + //Create context logger + const customContextLogger = this.loggerService.createContextLogger(this.ctx, customLogger); + } +} +``` + + + +## Common Problem + + + +### 1. The server environment log is not output + +We do not recommend printing too many logs in the server environment. Only print necessary content. Excessive log output affects performance and quickly locates problems. + +To adjust the log level, see the "Configuring Log Level" section. + + + +### 2. The server has no console log + +Generally speaking, the server console log (console) is closed and will only be output to a file. If there are special needs, it can be adjusted individually. + + + +### 3. Some Docker environments fail to start + +Check whether the user who started the current application in the directory where the log is written has permissions. + + + +### 4. How to convert if there is an old configuration? + +The new version of the log library is already compatible with the old configuration. Generally, no additional processing is required. There is a priority relationship between the old configuration and the new configuration when merging. Please check the [Change Document](https://github.com/midwayjs/logger/blob/ main/BREAKING-3.md). + +In order to reduce troubleshooting problems, please use the new configuration format when using the new version of the log library. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/middleware.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/middleware.md new file mode 100644 index 000000000000..90df9928cb29 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/middleware.md @@ -0,0 +1,721 @@ +# Web middleware + +Web middleware is a function called **before** and after (partially). Middleware functions can access request and response objects. +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01h6hYvW1ogNexjJ3Nl_!!6000000005254-2-tps-2196-438.png) + + +Different upper-layer web frameworks have different middleware forms. Midway standard middleware is based on the [onion ring model](https://eggjs.org/zh-cn/intro/egg-and-koa.html#midlleware). Express, on the other hand, is a traditional queue model. + + +Koa and EggJs can be executed** before and after the **controller. In Express, the middleware can **only be called before** the controller, which will be introduced separately in Express chapters. + +For the following code, we will take `@midwayjs/koa` as an example. + + + + +## Writing middleware + + +In general, we will write Web middleware in the `src/middleware` folder. + + +Create a `src/middleware/report.middleware.ts` . In this web middleware, we print the time when the controller (Controller) executes. +``` +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ ├── user.controller.ts +│ │ └── home.controller.ts +│ ├── interface.ts +│ ├── middleware ## middleare directory +│ │ └── report.middleware.ts +│ └── service +│ └── user.service.ts +├── test +├── package.json +└── tsconfig.json +``` + + +Midway uses the `@Middleware` decorator to identify the middleware. The complete middleware sample code is as follows. + + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + // Logic executed before the controller + const startTime = Date.now(); + // Execute the next Web middleware and finally execute to the controller. + // Here you can get the return value of the next middleware or controller. + const result = await next(); + // Logic executed after the controller + console.log(Date.now() - startTime); + // Returns the result to the previous middleware + return result; + }; + } + + static getName(): string { + return 'report'; + } +} +``` + + +In short, `await next()` represents the next logic to be executed, which generally represents the controller execution. Before and after execution, we can perform some printing and assignment operations, which is also the biggest advantage of the onion ring model. + +Note that Midway finishes the traditional onion model so that it can obtain the return value of the next middleware. At the same time, you can also return the result of this middleware to the previous middleware by using the `return` method. + +The static `getName` method here is used to specify the name of the middleware to facilitate troubleshooting. + + +## Use middleware + + +After the Web middleware is written, it needs to be applied to the request process. + + +According to the location of the application, there are two types: + + +- 1. Global middleware, middleware that all routes will execute, such as cookie, session, etc. +- 2. Routing middleware, middleware that a single/partial route will execute, such as pre-check of a route, data processing, etc. + + + +The relationship between them is generally: + + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01oQZ5Rk1jReqck6YMn_!!6000000004545-2-tps-2350-584.png) + + + +### Routing middleware + + +After writing the middleware, we need to apply it to each controller route. `@Controller` the second parameter of the decorator, which allows us to easily add middleware to a routing group. +```typescript +import { Controller } from '@midwayjs/core'; +import { ReportMiddleware } from '../middleware/report.middlweare'; + +@Controller('/', { middleware: [ ReportMiddleware ] }) +export class HomeController { + +} +``` + + +Midway also provides middleware parameters on route decorators such as `@Get` and `@Post` to facilitate middleware interception of a single route. +```typescript +import { Controller, Get } from '@midwayjs/core'; +import { ReportMiddleware } from '../middleware/report.middlweare'; + +@Controller('/') +export class HomeController { + + @Get('/', { middleware: [ ReportMiddleware ]}) + async home() { + } +} +``` + + + +### Global middleware + + +The so-called global middleware is the Web middleware that takes effect on all routes. + + +We need to add middleware to the middleware list of the current framework before the application starts. `useMiddleware` method, we can add middleware to the middleware list. + +```typescript +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { ReportMiddleware } from './middleware/user.middleware'; + +@Configuration({ + imports: [koa] + // ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + this.app.useMiddleware(ReportMiddleware); + } +} + +``` +You can add multiple middleware at the same time. + +```typescript +async onReady() { + this.app.useMiddleware([ReportMiddleware1, ReportMiddleware2]); +} +``` + + + +## Ignore and match routes + +When middleware is executed, we can add logic that routes ignore. + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + // ... + }; + } + + ignore(ctx: Context): boolean { + // The following route will ignore this middleware + return ctx.path === '/' + || ctx.path === '/api/auth' + || ctx.path === '/api/login'; + } + + static getName(): string { + return 'report'; + } +} +``` + +Similarly, you can also add matching routes. Only matching routes will execute the middleware. The `ignore` and `match` only take effect. + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + // ... + }; + } + + match(ctx: Context): boolean { + // The following matching route will execute this middleware + if (ctx.path === '/api/index') { + return true; + } + } + + static getName(): string { + return 'report'; + } +} +``` + +In addition, `match` and `ignore` can also be ordinary strings or regular expressions, and their array forms. + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + // string + match = '/api/index'; + + // regular + match = /^\/api/; + + // array + match = ['/api/index', '/api/user', /^\/openapi/, ctx => { + if (ctx.path === '/api/index') { + return true; + } + }]; +} +``` + +We can also modify properties during the initialization phase, such as: + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + // Configuration of a middleware + @Config('report') + reportConfig; + + @Init() + async init() { + // merge some rules dynamically + if (this. reportConfig. match) { + this.match = ['/api/index', '/api/user'].concat(this.reportConfig.match); + } else if (this. reportConfig. ignore) { + this.match = [].concat(this.reportConfig.ignore); + } + } +} +``` + + + +## Reuse middleware + +The essence of middleware is function. Function can pass different configurations to reuse middleware, but it is difficult to implement in class scenario. Midway provides `createMiddleware` method to help create different middleware functions in class scenario. + +You can use `createMiddleare` to reuse in `useMiddleware` phase. + +```typescript +// src/configuration.ts +import { App, Configuration, createMiddleare } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { ReportMiddleware } from './middleware/user.middleware'; + +@Configuration({ +imports: [koa] +// ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + // Add ReportMiddleware middleware + this.app.useMiddleware(ReportMiddleware); + // Add a ReportMiddleware with different parameters + this.app.useMiddleware(createMiddleare(ReportMiddleware, { + text: 'abc' + }, 'anotherReportMiddleare')); + } +} + +``` + +We can get this parameter in the middleware to execute different logic. + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + initData = 'text1'; + + resolve(_, options?: { + text: string; + }) { + return async (ctx: Context, next: NextFunction) => { + this.ctx.setAttr('data', options?.text || this.initData); + return await next(); + }; + } +} +``` + +`createMiddleare` method is defined as follows, containing three parameters. + +```typescript +function createMiddleware(middlewareClass: new (...args) => IMiddleware, options, name?: string); +``` + +| Parameters | Description | +| --------------- | ------------------------- | +| middlewareClass | Middleware class | +| options | Passed custom parameters | +| name | Optional, middleware name | + +`options` can pass custom functions of middleware, which can be processed by the logic. + +`name` field is used for sorting and displaying middleware, and generally a string different from the original middleware name is selected. + +`createMiddleare` method can also be used in routing middleware. + +```typescript +import { Controller, Get, createMiddleware } from '@midwayjs/core'; +import { ReportMiddleware } from '../middleware/report.middlweare'; + +const anotherMiddleware = createMiddleware(ReportMiddleware, { + // ... +}); + +@Controller('/') +export class HomeController { + @Get('/', { + middleware: [anotherMiddleware], + }) + async home() {} +} + +``` + +Note that the decorator will be loaded before the framework is started. At this time, the parameters of `createMiddleware` cannot be obtained from the framework configuration and are generally fixed object values. + + + +## Function middleware + +Midway still supports the form of function middleware and can be added to the middleware list using `useMiddleware`. + +```typescript +// src/middleware/another.middleware.ts +export async function fnMiddleware(ctx, next) { + // ... + await next(); + // ... +} + + +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { ReportMiddleware } from './middleware/user.middleware'; +import { fnMiddleware } from './middleware/another.middleware'; + +@Configuration({ + imports: [koa] + // ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + // add middleware + this.app.useMiddleware([ReportMiddleware, fnMiddleware]); + } +} + + +``` + +In this way, many koa tripartite middleware in the community can be easily accessed. + + + +## Use community middleware + + +Let's take `koa-static` as an example. + + +In the `koa-static` documentation, it is written like this. + +```typescript +const Koa = require('koa'); +const app = new Koa(); +app.use(require('koa-static')(root, opts)); +``` + +Then, `require('koa-static')(root, opts)` is actually the returned middleware method, we can export it directly and call `useMiddleware`. + +```typescript +async onReady() { + // add middleware + this.app.useMiddleware(require('koa-static')(root, opts)); +} +``` + +If the middleware supports importing on routes, for example: + +```typescript +const Koa = require('koa'); +const app = new Koa(); +app.get('/controller', require('koa-static')(root, opts)); +``` + +We can also treat middleware as ordinary functions and place them in decorator parameters. + +```typescript +const static Middleware = require('koa-static')(root, opts); + +//... +class HomeController { + @Get('/controller', {middleware: [staticMiddleware]}) + async getMethod() { + //... + } +} +``` + +It can also be used as a routing method body. + +```typescript +const static Middleware = require('koa-static')(root, opts); + +//... +class HomeController { + @Get('/controller') + async getMethod(ctx, next) { + //... + return static Middleware(ctx, next); + } +} +``` + +:::tip + +There are many ways to write three-party middleware, and the above are just the most basic ways to use it. + +::: + + + +## Get the middleware name + +Each middleware should have a name. By default, the name of the class middleware will be obtained according to the following rules: + +- 1. When the static method of `getName()` exists, take its return value as the name +- 2. If there is no static method of `getName()`, the class name will be used as the middleware name. + +A well-recognized middleware name plays a big role in manually sorting or debugging code. + +```typescript +@Middleware() +export class ReportMiddleware implements IMiddleware { + + // ... + + static getName(): string { + return 'report'; // Middleware name + } +} +``` + +Function middleware is similar. The defined method name is the name of middleware, such as the following `fnMiddleware`. + +```typescript +export async function fnMiddleware(ctx, next) { + // ... + await next(); + // ... +} +``` + +If the third-party middleware exports an anonymous middleware function, you can use `_name` to add a name. + +```typescript +const fn = async (ctx, next) => { + // ... + await next(); + // ... +}; + +fn._name = 'fnMiddleware'; + +``` + +We can use `getMiddleware().getNames()` to obtain all middleware names in the current middleware list. + +```typescript +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { ReportMiddleware } from './middleware/user.middleware'; +import { fnMiddleware } from './middleware/another.middleware'; + +@Configuration({ + imports: [koa] + // ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + // add middleware + this.app.useMiddleware([ReportMiddleware, fnMiddleware]); + + // output + console.log(this.app.getMiddleware().getNames()); + // => report, fnMiddleware + } +} + + + +``` + + + +## Middleware sequence + +Sometimes, we need to modify the order of middleware in components or applications. + +Midway provides `insert` API operations to facilitate you to quickly adjust middleware. + +We need to use the `getMiddleware()` method to obtain the middleware list and then operate on it. + +```typescript +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { ReportMiddleware } from './middleware/user.middleware'; + +@Configuration({ + imports: [koa] + // ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + // Add middleware to the front + this.app.getMiddleware().insertFirst(ReportMiddleware); + // Adding middleware to the back is equivalent to useMiddleware + this.app.getMiddleware().insertLast(ReportMiddleware); + + // After adding middleware to middleware named session + this.app.getMiddleware().insertAfter(ReportMiddleware, 'session'); + // Before adding middleware to middleware named session + this.app.getMiddleware().insertBefore(ReportMiddleware, 'session'); + } +} + +``` + + + + +## Common examples + + + +### Get request scope instance in middleware + + +Due to the particularity of the life cycle of Web middleware, it will be loaded (bound) to the route before the application request, so it cannot be associated with the request. The scope of the middleware class is **fixed as a singleton (Singleton)**. + + +Because **the middleware instance is a single instance**, the instances injected in the middleware are not bound to the request, **ctx cannot be obtained**, and `@Inject()` cannot be used to inject the instance of the request scope. Only the Singleton instances can be obtained. + + +For example, **the following code is wrong.** + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + @Inject() + userService; // The instance and context injected here are not bound and ctx cannot be obtained. + + resolve() { + return async (ctx: Context, next: NextFunction) => { + // TODO + await next(); + }; + } + +} +``` + + +If you want to get an instance of the request scope, you can use the method obtained from the request scope container `ctx.requestContext`, as follows. + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + const userService = await ctx.requestContext.getAsync(UserService); + // TODO userService.xxxx + await next(); + }; + } + +} +``` + +### Unified return data structure + +For example, all data returned in the `/api` uses a unified structure to reduce duplicate code in the Controller. + +We can add a middleware code similar to the following. + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class FormatMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + const result = await next(); + return { + code: 0, + msg: 'OK', + data: result + } + }; + } + + match(ctx) { + return ctx.path.indexOf('/api') !== -1; + } +} +``` + +The preceding code is only the code that is returned with the correct logic. If you want to return an incorrect package, you can use [Filter](./error_filter). + + + +### About the case where middleware returns null + +Under koa/egg, if the middleware returns a null value, the status code will change to 204. If you need to return other status codes (such as 200), you need to explicitly assign additional status codes in the middleware. + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class FormatMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + const result = await next(); + if (result === null) { + ctx.status = 200; + } + return { + code: 0, + msg: 'OK', + data: result + } + }; + } + + match(ctx) { + return ctx.path.indexOf('/api') !== -1; + } +} +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/midway_component.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/midway_component.md new file mode 100644 index 000000000000..1a1a8dc2948b --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/midway_component.md @@ -0,0 +1,66 @@ +# Use components + +Components are Midway's extension mechanism. We will develop reusable business code, or logical and abstract common capabilities into components, so that these codes can be reused in all Midway scenarios. + + + +## Enable components + +Components are generally reused in the form of npm packages. Each component is a package of code that can be `required` directly. Let's take the `@midwayjs/validate` component as an example. + +First, add dependencies to the application. + +```json +// package.json +{ + "dependencies": { + "@midwayjs/validate": "^3.0.0" + } +} +``` + +We need to enable this component in the code. Midway's component loading capability is designed in the `src/configuration.ts` file. + +```typescript +// src/configuration.ts of application or function +import { Configuration } from '@midwayjs/core'; +import * as validate from '@midwayjs/validate'; + +@Configuration({ + imports: [validate] +}) +export class MainConfiguration {} +``` + + + +## Enable components for different environments + +Sometimes, we need to use components in special environments, such as when developing locally. `imports` attributes can be passed into an array of objects, and we can configure the environment enabled by the component in the object. + +For example, the commonly used `info` component can be enabled only in the local environment for security purposes. + +```typescript +// src/configuration.ts of application or function +import { Configuration } from '@midwayjs/core'; +import * as info from '@midwayjs/info'; + +@Configuration({ + imports: [ + { + component: info, + enabledEnvironment: ['local'] + }, + ], +}) +export class MainConfiguration {} +``` + +- `component` is used to specify a component object, which must contain a `Configuration` exported attribute +- `enabledEnvironment` the array of environments enabled by the component + + + +## Development component + +For more information, see [Component development](component_development). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/midway_slow_problem.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/midway_slow_problem.md new file mode 100644 index 000000000000..cd796e785e00 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/midway_slow_problem.md @@ -0,0 +1,65 @@ +# About the slow start of Midway + +Midway will use ts-node to scan and require modules in real time when developing locally. If there are too many ts files (such as 200 +), it may lead to slower startup, especially in the case of non-SSD hard disks under Windows, resulting in frequent fullGC of Server for ts-node type checking, and each file load may reach 1-2s. + +Generally, Mac is SSD, so there is basically no problem, but Windows will appear, and there will be no such problem after construction. + +As shown in the following figure. + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1601523014939-40121f9c-bc19-4f9e-a7e6-e744d409a9ea.png) + +## How to judge + +1. Clean up the ts-node cache first. + +There is a directory of `ts-node-*` in the temporary directory, which can be deleted (if you do not know the temporary directory, you can execute the `require('OS').tmpdir()` output view on the command line). + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1601523402032-7e9c162a-762e-4cba-82b4-8ae63fe37280.png) + +Deleted the following similar directory. + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1601523340452-7924affe-96b5-4544-85b7-e41ace4206e8.png) + +2. Start Midway with ts-node + +Execute the following startup command. + +```bash +// midway v1 +cross-env DEBUG=midway* NODE_ENV=local midway-bin dev --ts + +// midway v2 +cross-env NODE_DEBUG=midway* NODE_ENV=local midway-bin dev --ts +``` + +The require duration of each file will appear, if the time is relatively long. + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1601523470970-1812326a-39d9-4b39-af57-7723f80f6e17.png) + +## Solve the problem + +Since a Server will be started inside `TS_NODE_TYPE_CHECK`, when there are many special files, the type check will be done every require. If it causes serious startup impact, it is recommended to close it. **The cost is that the type check is not performed at the start of the runtime. Because there is a prompt in the editor, the check is not performed at the runtime.** + +Add the following two environment variables before executing the command. + +```bash +TS_NODE_TYPE_CHECK=false TS_NODE_TRANSPILE_ONLY=true +``` + +For example: + +```json +cross-env TS_NODE_TYPE_CHECK=false TS_NODE_TRANSPILE_ONLY=true NODE_DEBUG=midway* NODE_ENV=local midway-bin dev --ts +``` + +The following is the comparison effect of using the same items. + +| | First execution (no cache) | Second execution (with cache) | +| ------------ | -------------------- | -------------------- | +| No optimization parameters | About 258s | about 5.6s | +| Add optimization parameters | About 15s | About 4.7s | +| | | | + +## Other + +If you have any questions, please submit your warehouse + node_modules to us. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/mock.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/mock.md new file mode 100644 index 000000000000..16fd967c488c --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/mock.md @@ -0,0 +1,430 @@ +# Data simulation + +Midway provides the built-in ability to simulate data during development and testing. + + + +## Mock during testing + +`@midwayjs/mock` provides some more general APIs for simulation during testing. + +### Simulation context + +`mockContext` methods are used to simulate the context. + +```typescript +import { mockContext } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + const app = await createApp(); + + // Simulation context + mockContext(app, 'user', 'midway'); + + const result1 = await createHttpRequest(app).get('/'); + // ctx.user => midway + // ... +}); +``` + +If your data is complex or logical, you can also use the callback form. + +```typescript +import { mockContext } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + const app = await createApp(); + + // Simulation context + mockContext(app, (ctx) => { + ctx.user = 'midway'; + }); +}); +``` + +Note that this mock behavior is executed before all middleware. + + + +### Analog Session + +`mockSession` methods are used to simulate Session. + +```typescript +import { mockSession } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + const app = await createApp(); + + mockSession(app, 'user', 'midway'); + + const result1 = await createHttpRequest(app).get('/'); + // ctx.session.user => midway + // ... +}); +``` + +### Simulate Header + +Use `mockHeader` methods to simulate Header. + +```typescript +import { mockHeader } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + const app = await createApp(); + + mockHeader(app, 'x-abc', 'bbb'); + + const result1 = await createHttpRequest(app).get('/'); + // ctx.headers['x-abc'] => bbb + // ... +}); +``` + +### Simulation class attribute + +Use the `mockClassProperty` method to simulate the properties of the class. + +If there is the following service class. + +```typescript +@Provide() +export class UserService { + data; + + async getUser() { + return 'hello'; + } +} +``` + +We can simulate it when we use it. + +```typescript +import { mockClassProperty } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + + mockClassProperty(UserService, 'data', { + bbb: 1 + }); + // userService.data => {bbb: 1} + + // ... +}); +``` + +It is also possible to simulate the method. + +```typescript +import { mockClassProperty } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + + mockClassProperty(UserService, 'getUser', async () => { + return 'midway'; + }); + + // userService.getUser() => 'midway' + + // ... +}); +``` + + + +### Simulate common object properties + +Use the `mockProperty` method to mock object properties. + +```typescript +import { mockProperty } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + + const a = {}; + mockProperty(a, 'name', 'hello'); + + // a['name'] => 'hello' + + // ... +}); +``` + +It is also possible to simulate the method. + +```typescript +import { mockProperty } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + + const a = {}; + mockProperty(a, 'getUser', async () => { + return 'midway'; + }); + + // a.getUser() => 'midway' + + // ... +}); +``` + + + +### Grouping + +Starting from version `3.19.0`, Midway's mock functionality supports managing different mock data through grouping. You can specify a group name when creating a mock, allowing you to restore or clean up a specific group of mock data as needed. + +```typescript +import { mockContext, restoreMocks } from '@midwayjs/mock'; + +it('should test mock with groups', async () => { + const app = await createApp(); + + // Create a mock for a regular object + const a = {}; + mockProperty(a, 'getUser', async () => { + return 'midway'; + }, 'group1'); + + // Create a mock for the context + mockContext(app, 'user', 'midway', 'group1'); + mockContext(app, 'role', 'admin', 'group2'); + + // Restore a single group + restoreMocks('group1'); + + // Restore all groups + restoreAllMocks(); +}); +``` + +By using groups, you can manage and control mock data more flexibly, especially in complex testing scenarios. + + + +### Cleaning up mocks + +Every time the `close` method is called, all mock data is automatically cleared. + +If you want to clean up manually, you can also execute the `restoreAllMocks` method. + +```typescript +import { restoreAllMocks } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + restoreAllMocks(); + // ... +}); +``` + +Starting from version `3.19.0`, it supports cleaning up by specifying a group. + +```typescript +import { restoreMocks } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + restoreMocks('group1'); + // ... +}); +``` + +### Standard Mock service + +Midway provides standard MidwayMockService services for simulating data in code. + +Various simulation methods in `@midwayjs/mock` have all called this service at the bottom. + +For more information, see [Built-in services](./built_in_service#midwaymockservice). + + + +## Development Mock + +Whenever the back-end service is not online, or when the data is not prepared during the development phase, the ability to simulate during the development phase is required. + + + +### Write mock class + +Under normal circumstances, we will write the simulation data used during development in the `src/mock` folder, and our simulation behavior is actually a piece of logic code. + +:::tip + +Don't get used to mocking data in code, it's actually part of the logic. + +::: + +Let's take an example. If there is a service for obtaining Index data, but the service has not been developed yet, we can only write simulation code. + +```typescript +// src/service/indexData.service.ts +import { Singleton, makeHttpRequest, Singleton } from '@midwayjs/core'; + +@Singleton() +export class IndexDataService { + + @Config('index') + indexConfig: {indexUrl: string}; + + private indexData; + + async load() { + // get data from remote + this.indexData = await this.fetchIndex(this.indexConfig.indexUrl); + } + + public getData() { + if (!this. indexData) { + // If the data does not exist, load it once + this. load(); + } + return this. indexData; + } + + async fetchIndex(url) { + return makeHttpRequest>(url, { + method: 'GET', + dataType: 'json', + }); + } +} +``` + +:::tip + +In the above code, the `fetchIndex` method is intentionally removed to facilitate subsequent simulation behaviors. + +::: + +When the interface has not been developed, it is very difficult for us to develop locally. The common practice is to define a JSON data, + +For example, create a `src/mock/indexData.mock.ts` to mock the initial service interface. + +```typescript +// src/mock/indexData.mock.ts +import { Mock, ISimulation } from '@midwayjs/core'; + +@Mock() +export class IndexDataMock implements ISimulation { +} +``` + +`@Mock` is used to represent that it is a simulation class, which is used to simulate some business behaviors, and `ISimulation` is some interfaces that need to be implemented by the business. + +For example, we want to simulate the data of the interface. + +```typescript +// src/mock/indexData.mock.ts +import { App, IMidwayApplication, Inject, Mock, ISimulation, MidwayMockService } from '@midwayjs/core'; +import { IndexDataService } from '../service/indexData.service'; + +@Mock() +export class IndexDataMock implements ISimulation { + + @App() + app: IMidwayApplication; + + @Inject() + mockService: MidwayMockService; + + async setup(): Promise { + // Mock properties using the MidwayMockService API + this.mockService.mockClassProperty(IndexDataService, 'fetchIndex', async (url) => { + // return different data according to the logic + if (/current/.test(url)) { + return { + data: require('./resource/current.json'), + }; + } else if (/v7/.test(url)) { + return { + data: require('./resource/v7.json'), + }; + } else if (/v6/.test(url)) { + return { + data: require('./resource/v6.json'), + }; + } + }); + } + + enableCondition(): boolean | Promise { + // Conditions for the mock class to be enabled + return ['local', 'test', 'unittest']. includes(this. app. getEnv()); + } +} +``` + +In the above code, `enableCondition` is a method that must be implemented, which represents the enabling condition of the current simulation class. For example, the above code only takes effect in `local`, `test` and `unittest` environments. + + + +### Simulation Timing + +The simulation class contains some simulation opportunities, which have been defined in the `ISimulation` interface, such as: + +```typescript +export interface ISimulation { + /** + * The initial simulation timing is executed after the life cycle onConfigLoad + */ + setup?(): Promise; + /** + * Executed when the life cycle is closed, generally used for data cleaning + */ + tearDown?(): Promise; + /** + * Executed when each framework is initialized, the app of the current framework will be passed + */ + appSetup?(app: IMidwayApplication): Promise; + /** + * Executed at the beginning of each frame's request, the app and ctx of the current frame will be passed + */ + contextSetup?(ctx: IMidwayContext, app: IMidwayApplication): Promise; + /** + * Executed at the end of each frame request, after error handling + */ + contextTearDown?(ctx: IMidwayContext, app: IMidwayApplication): Promise; + /** + * Executed when each frame is stopped + */ + appTearDown?(app: IMidwayApplication): Promise; + /** + * The execution conditions of the simulation are generally a specific environment or a specific framework + */ + enableCondition(): boolean | Promise; +} +``` + +Based on the above interface, we implement very free simulation logic. + +For example, add different middleware on different frameworks. + +```typescript +import { App, IMidwayApplication, Mock, ISimulation } from '@midwayjs/core'; + +@Mock() +export class InitDataMock implements ISimulation { + + @App() + app: IMidwayApplication; + + async appSetup(app: IMidwayApplication): Promise { + // Add different test middleware for different framework types + if (app. getNamespace() === 'koa') { + app. useMiddleware(/*...*/); + app. useFilter(/*...*/); + } + + if (app. getNamespace() === 'bull') { + app. useMiddleware(/*...*/); + app. useFilter(/*...*/); + } + } + + enableCondition(): boolean | Promise { + return ['local', 'test', 'unittest']. includes(this. app. getEnv()); + } +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/ops/ecs_start_err.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/ops/ecs_start_err.md new file mode 100644 index 000000000000..48306bbf7a2a --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/ops/ecs_start_err.md @@ -0,0 +1,49 @@ +# Server startup failure troubleshooting + +Application startup failure is a very common phenomenon. Logic errors, compilation errors, configuration errors, and environmental problems may all cause your project to fail to start. + + +## Quickly locate code problems + +In most cases, the startup failure we talk about is generally a server environment startup failure. Let's take Linux as an example. + +1. Use `ps aux | grep node` to check whether processes exist and whether the number of processes is correct. + +2. Open the [project log directory](/docs/logger_v3# Configure log root directory), view the contents of the `common-error.log` file, and check the cause based on the latest stack. + +3. Console logs that are started, such as `pm2 logs` + + +Most of the problems will be found in the log. Please make the habit of logging in to the machine to view the log as much as possible. This is a necessary skill for developers. + + + +## Possible environmental problems + +In addition to the problems of the code itself, the environment may also bring some problems. These problems are more difficult to find, and are often related to the system, permissions, environment variables, startup parameters, network environment, and even the kernel itself. + +Here are some possible scenarios. + +### 1. The document is incomplete or not up to date. + +Ensure that your project has performed the following processes before deployment + +- 1. Run `npm run dev` or similar command to start locally and run successfully. +- 2. Use `npm run build` to compile the ts file into a js file, and generate the `dist` directory in the root directory. +- 3. Use `npm run start` to run the js file locally. + +Check whether the files and directory structure on the server are complete, for example: + +- 1. Whether the `node_modules` directory exists +- 2. Whether the `dist` directory and the js file in it exist or is the latest. + +### 2. The issue of starting the user's authority + +We usually use a regular account, such as an admin account, instead of using sudo to deploy. + +- 1. Check whether the user has the permission to start node by creating a directory. +- 2. Check whether the server log directory of the project has write permission + +### 3. Startup Port Conflict + +If you start multiple Node.js projects, if you use the same port, you will throw a port reuse error. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/pipe.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/pipe.md new file mode 100644 index 000000000000..567cd6889db7 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/pipe.md @@ -0,0 +1,190 @@ +# Pipeline + +The pipeline is the internal mechanism of the parameter decorator, which can execute some custom code after the parameter decorator logic, and is generally used in the following scenarios: + +- 1. Data verification +- 2. Conversion of parameters + + + +## The pipeline provided by the component + +`@midwayjs/validate` provides a validation pipeline by default, you only need to enable the component to use it. + +For example: + +```typescript +@Controller('/api/user') +export class HomeController { + + @Post('/') + async updateUser(@Body() user: UserDTO ) { + //... + } +} +``` + +The `@Body` decorator has been automatically registered with `ValidatePipe`. If `UserDTO` is a DTO that has been decorated with the `@Rule` decorator, it will be automatically validated and converted. + +If an underlying type is used, then validation and conversion can also be done through the data conversion pipeline. + +For example: + +```typescript +import { ParseIntPipe } from '@midwayjs/validate'; + +@Controller('/api/user') +export class HomeController { + + @Post('/update_age') + async updateAge(@Body('age', [ParseIntPipe]) age: number ) { + //... + } +} +``` + +The `ParseIntPipe` pipeline can convert strings and numeric data into numbers, so that the `age` field obtained from the request parameters will pass the validation of the pipeline and be converted into a numeric format. + +In addition, it also provides more data conversion pipelines such as `ParseBoolPipe`, `ParseFloatPipe`, please refer to [Validate Component](./extensions/validate) for details. + + + +## Custom pipeline + +A pipe can be a class or a method that implements the `PipeTransform` interface, and we generally put the pipe in the `src/pipe` directory. + +for example: + +```typescript +// src/pipe/validate.pipe.ts +import { Pipe, PipeTransform, TransformOptions } from '@midwayjs/core'; + +@Pipe() +export class ValidatePipe implements PipeTransform { + transform(value: T, options: TransformOptions): R { + return value; + } +} +``` + +`PipeTransform` is a generic interface that every pipeline must implement. The generic `T` indicates the type of the input `value`, and `R` indicates the return type of the `transfrom()` method. + +To implement `PipeTransfrom`, each pipe must declare a `transfrom()` method. This method has two parameters: + +- `value` +- `options` + +`value` is the currently processed parameter value, and `options` is the currently processed option, including the following attributes. + +```typescript +export TransformOptions { + metaType: TSDesignType; + metadata: Record; + target: any; + methodName: string; +} +``` + +| Parameters | Description | +| :--------- | ------------------------------------------------------------ | +| metaType | A parsed object of ts metadata type, including `name`, `originDesign`, `isBaseType` three properties. | +| metadata | Metadata object for parameter decorator | +| target | the currently decorated instance itself | +| methodName | The method name of the current parameter decorator decorator | + + + +## Bind the pipeline + +Pipes must be attached to the parameter decorator to use. + +In the options of the custom decorator, we can transparently pass the pipeline parameters to achieve the purpose of applying the pipeline. + +For example, we customize a `RegValid` parameter decorator to pass in the regex and another pipeline parameter: + +```typescript +import { PipeUnionTransform, createCustomParamDecorator } from '@midwayjs/core'; + +function RegValid(reg: RegExp, pipe: PipeUnionTransform) { + return createCustomParamDecorator('reg-valid', { + reg, + }, { + //... + pipes: [pipe] + }); +} +``` + +The third parameter of `createCustomParamDecorator` supports passing in a `pipes` attribute, we need to pass the pipeline into it, so that the pipeline will be bound to the decorator and executed automatically in subsequent runs. + +For details, please refer to the parameter decorator chapter in [Custom Decorator](./custom_decorator). + +The `RegValid` decorator is used for regular validation, and we ignore the implementation part for now. + +In addition, we define another pipeline for intercepting data. + +```typescript +@Pipe() +export class CutPipe implements PipeTransform { + transform(value: number, options: TransformOptions): string { + return String(value).slice(5); + } +} +``` + +Now we can use them. + +```typescript +class UserService { + async invoke(@RegValid(/\d{11}/, CutPipe) phoneNumber: string) { + return phoneNumber; + } +} + +invoke(13712345678) => '345678' +``` + + + +## Default bound pipes + +Suppose we want to have pipeline capabilities to an existing parameter decorator, but we don't want the decorator to have pipeline parameters. + +Just like the built-in `@Query` and other decorators, there is no pipeline parameter, but it can automatically execute the pipeline logic when the validate component is enabled. + +We use the reverse registration API provided by `decoratorService`, which is very useful when providing capabilities across components. + +Let's take the `RegValid` written above as an example. + +```typescript +@Configuration({ + //... +}) +export class MainConfiguration { + @Inject() + decoratorService: MidwayDecoratorService; + + async onReady(container: IMidwayContainer) { + // register default pipe + this.decoratorService.registerParameterPipes('reg-valid', [ + CutPipe, + ]); + } +} +``` + +The `registerParameterPipes` method is used to implicitly register some pipes with a certain parameter decorator. In the above example, `reg-valid` is the key of a custom parameter, through which we can register with this parameter decorator. + +These pipelines are executed by default before explicitly passed pipelines. + +In this way, even if we don't pass pipeline parameters, the pipeline will still be executed. + +```typescript +class UserService { + async invoke(@RegValid(/\d{11}/) phoneNumber: string) { + return phoneNumber; + } +} + +invoke(13712345678) => '345678' +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/pipeline.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/pipeline.md new file mode 100644 index 000000000000..ba5c09d3ce19 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/pipeline.md @@ -0,0 +1,445 @@ +# Code pipeline + +In some scenarios, we want to split a complete task into different stages, each stage of the execution of the logic is relatively independent, and at the same time can improve the overall execution efficiency through parallel or serial ways. In Midway, we have implemented an optimized Pipeline mode. + + + +## Pipeline + + +In the Node.js Stream implementation, you can use `a.pipe(b).pipe(c).pipe(d)` to concatenate multiple Streams. however, the implementation of pipe, which can only be executed sequentially, may not meet different business scenarios. + + +In Midway, we use the `@Pipeline` decorator to create an instance that inherits and `IPipelineHandler` interfaces and can concatenate multiple `IValveHandler` instances for execution. + + +The `IValveHandler` is the specific task phase execution unit. The whole IPipelineHandler can be executed in parallel, series, concat, Waterfall (familiar, right? We refer to the method capabilities provided by the [async](https://github.com/caolan/async) Library. + + +The context IPipelineContext of the Pipeline execution period can be used to store Pipeline input parameters, the execution results of the previous IValveHandler instance, the previous intermediate products, etc., providing great flexibility. + + + + +## Type definition + + +### IPipelineHandler + +```typescript +interface IPipelineHandler { + /** + * Parallel execution, using Promise.all + * @param opts execution parameters + */ + parallel(opts: IPipelineOptions): Promise>; + /** + * Execute in parallel, and the final result is an array. + * @param opts execution parameters + */ + concat(opts: IPipelineOptions): Promise>; + /** + * serial execution, using foreach await + * @param opts execution parameters + */ + series(opts: IPipelineOptions): Promise>; + /** + * serial execution, using foreach await, the final result is an array + * @param opts execution parameters + */ + concatSeries(opts: IPipelineOptions): Promise>; + /** + * Serially executed, but the former execution result will be taken as an input parameter and passed into the next execution. The valve result of the last execution will be returned + * @param opts execution parameters + */ + waterfall(opts: IPipelineOptions): Promise>; +} +``` + + + +- Whitelist mechanism + When using the Pipeline decorator, if the array parameters are filled in, the values input parameters in the method execution function can only be items in the decorator array parameters. Of course, valves is optional. If you do not fill in the default, the decorator array parameters shall prevail. For example, if `@Pipeline(['a', 'B', 'c'])`, the `opts. values` array of the optional parameters in the execution function such as series must be `['a', 'B', 'c']` or a subset thereof. If this parameter is not specified, it must be executed in the logical order `['a', 'B', 'c']`. + + + +### Return result + + +The types of IPipelineResult are as follows. +```typescript +/** + * pipeline execution returns results + */ +export interface IPipelineResult { + /** + * Success + */ + success: boolean; + /** + * Exception information (return if any) + */ + error ?: { + /** + * The anomaly is on that valve. + */ + valveName?: string; + /** + * Abnormal information + */ + message?: string; + /** + * Original Error + */ + error?: Error; + }; + /** + * Return results + */ + result: T; +} +``` + + +## Use examples + +1. Suppose there is such a scenario that we need to obtain the data information on the page, the current user information, and several Tab at one time. Then let's first declare the data type returned +```typescript +class VideoDto { + videoId: string; + videoUrl: string; + videoTitle: string; +} +class AccountDto { + id: string; + nick: string; + isFollow: boolean; +} +class TabDto { + tabId: string; + title: string; + index: number; +} +interface HomepageDto { + videos: VideoDto[]; + account: AccountDto; + tab: TabDto; +} + +``` + +2. Implement a TestService to encapsulate the returned data +```typescript + +@Provide() +class TestService { + // Returns the current login user information + async getAccount(args: any): Promise { + return { + id: 'test_account_id', + nick: 'test hello', + isFollow: true + }; + } + // Return to the video list + async getVideos(args: any): Promise { + return [{ + videoId: '123', + videoUrl: 'https://www.taobao.com/xxx.mp4', + videoTitle: 'test 1 video' + }, { + videoId: '234', + videoUrl: 'https://www.taobao.com/xxx234.mp4', + videoTitle: 'test 2 video' + }, { + videoId: '456', + videoUrl: 'https://www.taobao.com/xxx456.mp4', + videoTitle: 'test 3 video' + }]; + } +// return to the tab page + async getTab(args: any): Promise { + return { + title: 'test tab', + tabId: 'firstTab', + index: 0 + }; + } +} + +``` + +3. Split several task packages into different IValveHandler implementations +```typescript +// Returns the video information +@Provide() +class VideoFeeds implements IValveHandler { + alias = 'videos'; + + @Inject() + service: TestService; + + async invoke(ctx: IPipelineContext): Promise { + return this.service.getVideos(ctx.args); + } +} +// Return account information +@Provide() +class AccountMap implements IValveHandler { + alias = 'account'; + + @Inject() + service: TestService; + + async invoke(ctx: IPipelineContext): Promise { + + // Get data execution logic + return this.service.getAccount(ctx.args); + } +} +// Returns tab information +@Provide() +class CrowFeeds implements IValveHandler { + alias = 'tab'; + @Inject() + service: TestService; + + async invoke(ctx: IPipelineContext): Promise { + // Get data execution logic + return this.service.getTab(ctx.args); + } +} +// Catch the entire error exception +@Provide() +class ErrorFeeds implements IValveHandler { + alias = 'tab'; + @Inject() + service: TestService; + + async invoke(ctx: IPipelineContext): Promise { + // Get data execution logic + throw new Error('this is error feeds'); + } +} +``` +### parallel + + +The result of this method is an object object, and each IValveHandler implements alias as the key of the object return value. +```typescript +class StageTest { + // Declare a pipeline here + @Pipeline([VideoFeeds, AccountMap, CrowFeeds]) + stages: IPipelineHandler; + + async runParallel(): Promise { + // The videoFeeds, accountMap and crowFeeds are executed concurrently here. + return this.stages.parallel({ + args: {aa: 123} + }); + + // The returned result structure + /* + { + // The key with the accountMap alias account as the return object + account: { + id: 'test_account_id', + nick: 'test hello', + isFollow: true + }, + // The videoFeeds alias video is used as the key of the return object. + video: [ + { + videoId: '123', + videoUrl: 'https://www.taobao.com/xxx.mp4', + videoTitle: 'test 1 video' + }, { + videoId: '234', + videoUrl: 'https://www.taobao.com/xxx234.mp4', + videoTitle: 'test 2 video' + }, { + videoId: '456', + videoUrl: 'https://www.taobao.com/xxx456.mp4', + videoTitle: 'test 3 video' + } + ], + // The crowFeeds alias tab is used as the key of the return object. + tab: { + title: 'test tab', + tabId: 'firstTab', + index: 0 + } + } + */ + } +} +``` + + +### concat + + +The execution method is the same as the parallel, except that the final result is an array. +```typescript +class StageTest { + // Declare a pipeline here + @Pipeline([VideoFeeds, AccountMap, CrowFeeds]) + stages: IPipelineHandler; + + async runConcat(): Promise { + // The videoFeeds, accountMap and crowFeeds are executed concurrently here. + return this.stages.concat({ + args: {aa: 123} + }); + + // The result returned here is an array + /* + [ + // Take videoFeeds as the first return object + [ + { + videoId: '123', + videoUrl: 'https://www.taobao.com/xxx.mp4', + videoTitle: 'test 1 video' + }, { + videoId: '234', + videoUrl: 'https://www.taobao.com/xxx234.mp4', + videoTitle: 'test 2 video' + }, { + videoId: '456', + videoUrl: 'https://www.taobao.com/xxx456.mp4', + videoTitle: 'test 3 video' + } + ], + // Take accountMap as the second return object + { + id: 'test_account_id', + nick: 'test hello', + isFollow: true + }, + // Take crowFeeds as the third return object + { + title: 'test tab', + tabId: 'firstTab', + index: 0 + } + ] + */ + } +} +``` + + +### series + + +Here, series is executed in serial, one by one according to the sequence of Pipeline decorator parameters, and the prev in the IPipelienContext is the previous valve, the current is the current, and next is the next valve to be executed. +```typescript +class StageTest { + // Declare a pipeline here + @Pipeline([VideoFeeds, AccountMap, CrowFeeds]) + stages: IPipelineHandler; + + async runSeries(): Promise { + // Here the serial execution videoFeeds, accountMap, crowFeeds + return this.stages.series({ + args: {aa: 123} + }); + + // The result returned here is an object, and the result is the same as the object assembly rule returned by the parallel. + } +} +``` + + +### concatSeries + + +The principle is the same as series, except that the returned result is an array. +```typescript +class StageTest { + // Declare a pipeline here + @Pipeline([VideoFeeds, AccountMap, CrowFeeds]) + stages: IPipelineHandler; + + async runConcatSeries(): Promise { + // here serially execute videoFeeds, accountMap, crowdFeeds + return this.stages.concatSeries({ + args: {aa: 123} + }); + + // The result returned here is an array that is assembled with the object returned by concat. + } +} +``` + + +### waterfall + + +Serial execution, only the last valve execution result is returned. + + +```typescript +@Provide() +class StageOne implements IValveHandler { + async invoke(ctx: IPipelineContext): Promise { + if (ctx.args.aa! = = 123) { + throw new Error('args aa is undefined'); + } + ctx.set('stageone', 'this is stage one'); + ctx.set('stageone_date', Date.now()); + if (ctx.info.current! = = 'stageOne') { + throw new Error('current stage is not stageOne'); + } + if (ctx.info.next! = = 'stageTwo') { + throw new Error('next stage is not stageTwo'); + } + if (ctx.info.prev) { + throw new Error('stageOne prev stage is not undefined'); + } + + return 'stageone'; + } +} + +@Provide() +class StageTwo implements IValveHandler { + async invoke(ctx: IPipelineContext): Promise { + const keys = ctx.keys(); + if (keys.length! = = 2) { + throw new Error('keys is not equal'); + } + ctx.set('stagetwo', ctx.get('stageone') + 1); + ctx.set('stagetwo_date', Date.now()); + // Verify whether it is the result returned by the execution stageOne + if (ctx.info.prevValue! = = 'stageone') { + throw new Error('stageone result empty'); + } + if (ctx.info.current! = = 'stageTwo') { + throw new Error('current stage is not stageTwo'); + } + if (ctx.info.next) { + throw new Error('stageTwo next stage is not undefined'); + } + if (ctx.info.prev! = = 'stageOne') { + throw new Error('prev stage is not stageOne'); + } + + return 'stagetwo'; + } +} + +class StageTest { + // Declare a pipeline here + @Pipeline([StageOne, StageTwo]) + stages: IPipelineHandler; + + async runStagesWaterfall(): Promise { + // This is executed in serial mode. You can see that the verification is performed in the stageTwo, and the prevValue is the result of stageOne execution. + return this.stages.waterfall({ + args: {aa: 123} + }); + } +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/quick_guide.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/quick_guide.md new file mode 100644 index 000000000000..3f487fe40d54 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/quick_guide.md @@ -0,0 +1,606 @@ +# Quick Start + +If you haven't touched Midway, it doesn't matter. In this chapter, we will build a Midway standard application step by step from the perspective of examples to display weather information so that you can quickly get started with Midway. + + + +## Environmental preparation + +- Operating system: supports macOS,Linux,Windows +- Running environment: [Node.js environment requirements](/docs/intro#environmental-preparation). + + + +## Initialize project + +We recommend using scaffolding directly, with only a few simple instructions, you can quickly generate the project. + +```bash +$ npm init midway@latest -y +``` + +Select `koa-v3` to initialize the project. You can customize the project name, such as `weather-sample`. + +Now you can start the application to experience it. + +```bash +$ npm run dev +$ open http://localhost:7001 +``` + +At the same time, we also provide a complete example. After `npm init midway`, you can select the `quick-start` project and create it, which is convenient for comparison and learning. + + + +## Write Controller + +If you are familiar with Web development or MVC, you know that we need to write [Controller and Router](./controller) in the first step. + +Among the files created by scaffolding, we already have some files, and we temporarily ignore them. + +In the `controller` directory, create a new `src/controller/weather.controller.ts` file with the following contents. + +```typescript +import { Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class WeatherController { + // Here is the decorator, defining a route + @Get('/weather') + async getWeatherInfo(): Promise { + // This is the return of http, which can directly return strings, numbers, JSON,Buffer, etc. + return 'Hello Weather!'; + } +} +``` + +Now we can return data through the access `/weather` interface. + + + +## Add parameter processing + +In the example, we need a URL parameter to dynamically show the weather in different cities. + +You can add the `@Query` decorator to obtain the parameters on the URL. + +```typescript +import { Controller, Get, Query } from '@midwayjs/core'; + +@Controller('/') +export class WeatherController { + @Get('/weather') + async getWeatherInfo(@Query('cityId') cityId: string): Promise { + return cityId; + } +} +``` + +In addition to the `@Query` decorator, Midway also provides other request parameters. You can view the [Routing and Control](./controller) documentation. + +## Write Service + +In actual projects, Controller is generally used to receive request parameters and verify parameters. It does not include particularly complex logic, complex and reused logic, and we should encapsulate it as a Service file. + +Let's add a Service to get weather information, including an http request to get remote data. + +The code is as follows: + +```typescript +// src/service/weather.service.ts +import { Provide, makeHttpRequest } from '@midwayjs/core'; + +@Provide() +export class WeatherService { + async getWeather(cityId: string) { + return makeHttpRequest(`https://midwayjs.org/resource/${cityId}.json`, { + dataType: 'json', + }); + } +} +``` + +:::info + +- 1. The `makeHttpRequest` method is Midway's built-in http request method. Please see the [document](./extensions/axios) for more parameters. +- 2. The city weather information in the example comes from the API of China Central Meteorological Station + +::: + +Then let's add definitions. Good type definitions can help us reduce code errors. + +In the `src/interface.ts` file, we added the data definition of weather information. + +```typescript +// src/interface.ts + +// ... + +export interface WeatherInfo { + weatherinfo: { + city: string; + cityid: string; + temp: string; + WD: string; + WS: string; + SD: string; + AP: string; + njd: string; + WSE: string; + time: string; + sm: string; + isRadar: string; + Radar: string; + } +} +``` + +In this way, we can mark in the Service. + +```typescript +import { Provide, makeHttpRequest } from '@midwayjs/core'; +import { WeatherInfo } from '../interface'; + +@Provide +export class WeatherService { + async getWeather(cityId: string): Promise { + const result = await makeHttpRequest(`https://midwayjs.org/resource/${cityId}.json`, { + dataType: 'json', + }); + + if (result.status === 200) { + return result.data as WeatherInfo;; + } + } +} + +``` + +:::info + +- 1. The `@Provide` decorator is used here to modify the class, which is convenient for subsequent Controller injection. + +::: + + + +At the same time, we revised the previous Controller file. + +```typescript +import { Controller, Get, Inject, Query } from '@midwayjs/core'; +import { WeatherInfo } from '../interface'; +import { WeatherService } from '../service/weather.service'; + +@Controller('/') +export class WeatherController { + + @Inject() + weatherService: WeatherService; + + @Get('/weather') + async getWeatherInfo(@Query('cityId') cityId: string): Promise { + return this.weatherService.getWeather(cityId); + } +} +``` + +:::info + +- 1. The `@Inject` decorator is used here to inject `WeatherService`, which is the standard usage of Midway dependency injection. You can see [here](./service) for more information. +- 2. The return value type of the method is also modified synchronously here. + +::: + +At this point, we can request `http://127.0.0.1:7001/weather?cityId=101010100` to view the returned results. + +Your first Midway interface has been developed. You can call it directly in the front-end code. Next, we will use this interface to complete a server-side rendered page. + + + +## Template rendering + +From here on, we need to use some Midway's expansion capabilities. + +The expansion package corresponding to Midway is called "component" and is also a standard npm package. + +We need to use the `@midwayjs/view-nunjucks` component here. + +You can install it using the following command. + +```bash +$ npm i @midwayjs/view-nunjucks --save +``` + +After the installation is complete, we enable the components in the `src/configuration.ts` file. + +```typescript +// ... +import * as view from '@midwayjs/view-nunjucks'; + +@Configuration({ + imports: [ + koa, + // ... + view + ], + importConfigs: [join(__dirname, './config')] +}) +export class MainConfiguration { + // ... +} + +``` + +:::info + +- 1. The `configuration` file is the life cycle entry file of Midway, which plays the role of component switch, configuration loading and life cycle management. +- 2. `imports` use the method to import (open) components + +::: + +Configure components in `src/config/config.default.ts` and specify them as `nunjucks` templates. + +```typescript +import { MidwayConfig } from '@midwayjs/core'; + +export default { + // ... + view: { + defaultViewEngine: 'nunjucks', + }, +} as MidwayConfig; + +``` + +Add the `view/info.html` template to the root directory (not in src). The content is as follows: + +```html + + + + weather forecast + + + +
+
+

+ {{city}}({{WD}}{{WS}}) +

+

{{temp}}

+

+ Air pressure +

+

+ Humidity +

+
+
+ + + +``` + +At the same time, we adjust the Controller code and change the returned JSON into template rendering. + +```typescript +// src/controller/weather.controller.ts +import { Controller, Get, Inject, Query } from '@midwayjs/core'; +import { WeatherService } from '../service/weather.service'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class WeatherController { + + @Inject() + weatherService: WeatherService; + + @Inject() + ctx: Context; + + @Get('/weather') + async getWeatherInfo(@Query('cityId') cityId: string): Promise { + const result = await this.weatherService.getWeather(cityId); + if (result) { + await this.ctx.render('info', result.weatherinfo); + } + } +} +``` + +In this step, we visit `http:// 127.0.0.1:7001/weather?cityId = 101010100` The rendered template content can already be seen. + + + +## Error handling + +Don't forget, we still have some exception logic to handle. + +Generally speaking, each external call needs to be caught by exception, and the exception will be turned into an error of our own business, so as to have a better experience. + +To do this, we need to define a business error of our own, creating a `src/error/weather.error.ts` file. + +```typescript +// src/error/weather.error.ts +import { MidwayError } from '@midwayjs/core'; + +export class WeatherEmptyDataError extends MidwayError { + constructor(err?: Error) { + super('weather data is empty', { + cause: err + }); + if (err?.stack) { + this.stack = err.stack; + } + } +} +``` + +Then, we adjust the Service code to throw an exception. + +```typescript +// src/service/weather.service.ts +import { Provide, makeHttpRequest } from '@midwayjs/core'; +import { WeatherInfo } from '../interface'; +import { WeatherEmptyDataError } from '../error/weather.error'; + +@Provide() +export class WeatherService { + async getWeather(cityId: string): Promise { + if (! cityId) { + throw new WeatherEmptyDataError(); + } + + try { + const result = await makeHttpRequest(`https://midwayjs.org/resource/${cityId}.json`, { + dataType: 'json', + }); + if (result.status === 200) { + return result.data as WeatherInfo; + } + } catch (error) { + throw new WeatherEmptyDataError(error); + } + } +} +``` + +:::info + +- 1. Error capture of http call request, package the error and return a business error of our system +- 2. If necessary, we can define more errors, assign wrong Code, etc. + +::: + +At this stage, we also need to handle exceptions for business. For example, when multiple locations throw `WeatherEmptyDataError`, we need to return them in a unified format. + +The error handler can complete this function. We need to create a `src/filter/weather.filter.ts` file with the following contents: + +```typescript +//src/filter/weather.filter.ts +import { Catch } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { WeatherEmptyDataError } from '../error/weather.error'; + +@Catch(WeatherEmptyDataError) +export class WeatherErrorFilter { + async catch(err: WeatherEmptyDataError, ctx: Context) { + ctx.logger.error(err); + return '

weather data is empty

'; + } +} + +``` + +It is then applied to the current framework. + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { WeatherErrorFilter } from './filter/weather.filter'; +// ... + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App() + app: koa.Application; + + async onReady() { + // ... + + // add filter + this.app.useFilter([WeatherErrorFilter]); + } +} +``` + +In this way, when `WeatherEmptyDataError` error is obtained in each request, the same return value will be used to return to the browser, and the original error message will be recorded in the log. + +For more information about exception handling, see [Document](./error_filter). + + + +## Data Simulation + +When writing code, our interface is often still in the unusable stage. In order to minimize the impact, we can use simulated data instead. + +For example, our weather interface can be simulated locally and in the test environment. + +We need to create a `src/mock/data.mock.ts` file with the following content: + +```typescript +// src/mock/data.mock.ts +import { + Mock, + ISimulation, + apps, + Inject, + IMidwayApplication, + MidwayMockService, +} from '@midwayjs/core'; +import { WeatherService } from '../service/weather.service'; + +@Mock() +export class WeatherDataMock implements ISimulation { + @App() + app: IMidwayApplication; + + @Inject() + mockService: MidwayMockService; + + async setup(): Promise { + const originMethod = WeatherService.prototype.getWeather; + this.mockService.mockClassProperty( + WeatherService, + 'getWeather', + async cityId => { + if (cityId === '101010100') { + return { + weatherinfo: { + city: 'Beijing', + cityid: '101010100', + temp: '27.9', + WD: 'South Wind', + WS: 'Less than level 3', + SD: '28%', + AP: '1002hPa', + njd: 'No live broadcast yet', + WSE: '<3', + time: '17:55', + sm: '2.1', + isRadar: '1', + Radar: 'JC_RADAR_AZ9010_JB', + }, + }; + } else { + return originMethod.apply(this, [cityId]); + } + } + ); + } + + enableCondition(): boolean | Promise { + // Conditions for the mock class to be enabled + return ['local', 'test', 'unittest']. includes(this. app. getEnv()); + } +} + +``` + +The `WeatherDataMock` class is used to simulate weather data, and the `setup` method is used for the actual initialization simulation. Among them, we use the `mockClassProperty` method of the built-in `MidwayMockService` to simulate the `getWeather` method of `WeatherService` Lose. + +In the simulation process, we only processed the data of a single city, and the others still followed the original interface. + +`enableCondition` is used to identify the scenarios in which this mock class takes effect. For example, the code above only takes effect locally and in the test environment. + +In this way, when developing and testing locally, the data we request `101010100` will be intercepted and returned directly, and will not be affected after deployment to the server environment. + +There are more interfaces available for data mocking, please refer to [documentation](./mock). + +## Unit test + +By default, Midway uses jest as the basic test framework. Generally, our test files are placed in the `test` directory of the root directory, with the `*.test.ts` suffix. + +For example, we will test the written `/weather` interface. + +We need to test its success and failure. + +```typescript +import { createApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework, Application } from '@midwayjs/koa'; + +describe('test/controller/weather.test.ts', () => { + + let app: Application; + beforeAll(async () => { + // create app + app = await createApp(); + }); + + afterAll(async () => { + // close app + await close(app); + }); + + it('should test /weather with success request', async () => { + // make request + const result = await createHttpRequest(app).get('/weather').query({ cityId: 101010100 }); + + expect(result.status).toBe(200); + Expect (result.text).toMatch(/Beijing/); + }); + + it('should test /weather with fail request', async () => { + const result = await createHttpRequest(app).get('/weather'); + + expect(result.status).toBe(200); + expect(result.text).toMatch(/weather data is empty/); + }); +}); + +``` + +Perform tests: + +```bash +$ npm run test +``` + +For more information, see [Test](./testing). + +:::info + +- 1. During jest test, use a single file as a unit and use `beforeAll` and `afterAll` to control the start and stop of app +- 2. Use `createHttpRequest` to create a test request +- 3. Use expect to assert whether the returned results meet expectations. + +::: + + + +## Continue to learn + +Congratulations, you have some preliminary understanding of Midway. Let's review it briefly. + +- 1. We use `npm init midway` to create an example. +- 2. Use the `@Controller` decorator to define routing and controller classes +- 3. Use `@Query` to obtain the request parameters. +- 4. use `@Provide` and `@Inject` to inject service classes +- 5. Use `imports` to enable components and configure nunjucks templates +- 6. Customize the error, use the error filter to intercept the error and return the custom data +- 7. Use jest to create tests and add successful and failed test cases + +The above is only a small part of Midway. As the use deepens, more capabilities will be used. + +You can start by [creating](./quickstart) a solution for different scenarios of the Midway. You can also go to the [Routing and Controller](./controller) section and add some request methods. You can also learn about [Web middleware](./middleware) or [dependency injection](./container). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/quickstart.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/quickstart.md new file mode 100644 index 000000000000..7b258a1dd275 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/quickstart.md @@ -0,0 +1,154 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Create the first application + + +## Technical selection + +Midway has multiple sets of technical solutions to choose from. We will distinguish them by deployment: + +| Technical selection | Description | +| --------------- | ------------------------------------------------------------ | +| Pure Node.js project | Midway traditional project, pure Node.js research and development, modules represented by `@midwayjs/koa`, supports back-end projects in the most complete way, and uses **dependency injection + Class** as the technology stack. | +| Serverless project | Midway is a technology stack developed separately for Serverless scenarios. Modules represented by `@midwayjs/faas` are connected to different Serverless platforms in a lightweight way. | +| Integration project | Midway's innovative technology scheme adopts the integrated development method of front and back ends to save the time of front and back ends. For modules represented by `@midwayjs/hooks`, **functional** is used as the main coding paradigm. | + +:::tip +This chapter and subsequent documents will use the **pure Node.js project** as the basic example. If you need to use the Serverless project, please jump to the [Serverless](serverless/serverless_intro). If you need to learn about the integration project, please visit [integration](hooks/intro). +::: + + + +## Fast initialization + + +Use `npm init midway` to view the complete list of scaffolds. After a project is selected, Midway automatically creates sample directories, codes, and installation dependencies. + +```bash +$ npm init midway@latest -y +``` + +For a v3 project, select `koa-v3`, pay attention to [Node.js environment requirements](/docs/intro#environmental-preparation). + +The example will create a directory structure similar to the following, where the simplest Midway project example is as follows. + +``` +➜ my_midway_app tree +. +├── src ## midway project source code +│ └── controller ## Web Controller Directory +│ └── home.controller.ts +├── test +├── package.json +└── tsconfig.json +``` +The whole project includes some of the most basic files and directories. + + +- `src` is the source directory of the entire Midway project. +- The test directory of the `test` project. All the test files are available here. +- `package the` package management profile of the. json Node.js project Foundation +- `tsconfig.json` Compile Configuration File TypeScript + + +In addition to the entire directory, we have some other directories, such as the `controller` directory. + + +## Development habits + + +Midway has no special restrictions on directories, but we will follow some simple development habits and classify some commonly used files into some default folders. + + +The following ts source code folders are in the `src` directory. + + +Commonly used are: + + +- `controller` Web Controller directory +- `middleware` middleware directory +- `filter` +- `aspect` interceptor +- `service` service logical directory +- `entity` or `model` database entity directory +- `config` +- The directory where the `util` tool class is stored. +- `decorator` custom decorator directory +- `interface.ts` definition file for ts business + + + +With the emergence of different scenarios, directory habits will continue to increase, and the specific directory content will be reflected in different component functions. + + +## Web framework selection + + +Midway was designed to be compatible with a variety of upper-level frameworks, such as common `Express`, `Koa` and `EggJS`. + +Starting with v3, we use Koa to demonstrate the basic example. + +These upper-level frameworks are provided in Midway with component capabilities, and all of them can use the decorator capabilities provided by Midway, but Midway will not encapsulate specific capabilities, such as the plugin system of Egg.js, or the middleware capabilities of Express, If you are familiar with one of these frameworks, or want to use the capabilities of a particular framework, you can choose it as your workhorse web framework. + + +| Name | Description | +| --- | --- | +| @midwayjs/koa | By default, Koa is a Express alternative framework, which supports asynchronous middleware and other capabilities by default, and is the second most common Node.js Web framework. | +| @midwayjs/web | Egg.js is a relatively commonly used Web framework in China, including some default plug-ins. | +| @midwayjs/express | Express is a well-known node.js minimalist Web framework. It's a well-tested, productive library with a lot of community resources. | + + +If you want to replace the default Web framework, please refer to the corresponding [egg](extensions/egg) or [express](extensions/express) section. + + +## Start the project + + +```bash +$ npm run dev +$ open http://localhost:7001 +``` +Midway will start the HTTP server, open the browser, access `http:// 127.0.0.1:7001`, and the browser will print out the `Hello midwayjs!` The information. + + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01KoUxO91jydMw41Vv4_!!6000000004617-2-tps-1268-768.png) + + +If you need to modify the development startup port, you can modify it in the scripts paragraph of the `package.json`, such as 6001: + + + + + +```typescript +"scripts": { + //... + "dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js --port 6001", +}, +``` + + + + + +```typescript +"scripts": { + //... + "dev": "cross-env NODE_ENV=local midway-bin dev --ts --port=6001", +}, +``` + + + + + + +## Frequently Asked Questions + +### windows eslint error + +:::caution +Windows may encounter the problem of eslint error. Please pay attention to [the problem of line wrapping under windows](faq/git_problem#XCAgm). +::: diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/release_schedule.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/release_schedule.md new file mode 100644 index 000000000000..5f847bdeb3dc --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/release_schedule.md @@ -0,0 +1,10 @@ +# Midway maintenance plan + +The following table shows the overall maintenance rhythm and plan of Midway. + +| Release | Status | Initial Release | Active LTS Start | Maintenance LTS Start | End-of-life | +| -------------------- | ------------------- | --------------- | ---------------- | --------------------- | ----------- | +| midway v1(inner v6) | **End-of-Life** | 2018-06-14 | 2018-10 | 2020-04 | 2022-04 | +| midway v2(inner v7) | **End-of-Life** | 2020-10 | 2021-02 | 2021-10 | 2024-04 | +| midway v3(inner v8) | **Maintenance LTS** | 2022-01 | 2022-06 | 2023-10 | | +| midway v4(inner v9) | **Development** | | | | | diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/req_res_app.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/req_res_app.md new file mode 100644 index 000000000000..c1f3ba492b84 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/req_res_app.md @@ -0,0 +1,440 @@ +# Application and Context + +Midway's application will expose different protocols, such as Http,WebSocket, etc. Each protocol here is provided by an independent component for Midway. + +For example, `@midwayjs/koa` in our previous example is a component that provides Http services. We will take this component as an example to introduce built-in objects. + +Each Web framework used will provide its own unique capabilities, which will be reflected in its own **context** (Context) and **application** (Application). + + + +## Defining conventions + +In order to simplify the use, all components of the exposure protocol will export **context** (Context) and **application** (Application) definitions, and we are consistent. That is, `Context` and `Application`. + +For example: + +```typescript +import { Application, Context } from '@midwayjs/koa'; +import { Application, Context } from '@midwayjs/faas'; +import { Application, Context } from '@midwayjs/web'; +import { Application, Context } from '@midwayjs/express'; +``` + +And non-Web framework, we have also maintained the same. + +```typescript +import { Application, Context } from '@midwayjs/socketio'; +import { Application, Context } from '@midwayjs/grpc'; +import { Application, Context } from '@midwayjs/rabbitmq'; +``` + + + +## Application + +Application is the application object in a component, and may have different implementations in different components. The Application object will contain some unified methods, which are unified from the `IMidwayApplication` definition. + +```typescript +import { Application } from '@midwayjs/koa'; +``` + + + +### How to get + +In all classes that depend on injection container management, the `@App()` decorator can be used to obtain the **current most important** Application. + +For example: + +```typescript +import { App, Controller, Get } from '@midwayjs/core'; +import { Application } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @App() + app: Application; + + @Get('/') + async home() { + // this.app.getConfig() + // this.app.getEnv() + } +} +``` + + + +### Main Application + +The protocols exposed by Midway applications are brought by components, and each component will expose the Application objects corresponding to its own protocol. + +This means that there will be multiple Application in an application. By default, we agree that the first Application introduced in `src/configuration.ts` is the **Main Application** (the **main Application** ). + +For example, the **Main Application** is the Application instance in the following KOA (the **main Application** ). + +```typescript +// src/configuration.ts + +import { Configuration, ILifeCycle } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as ws from '@midwayjs/ws'; + +@Configuration({ + imports: [koa, ws] +}) +export class MainConfiguration implements ILifeCycle { + // ... +} +``` + +In fact, Application all implement interfaces with `IMidwayApplication`. If we use a common API, there is no difference. + +Being a Main Application has some advantages: + +- In most scenarios, you can use `@App()` to inject and obtain +- Priority initialization + +For example, when multiple export Application components need to load middleware, they can be simply coded. + +```typescript +// src/configuration.ts + +import { Configuration, ILifeCycle } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as ws from '@midwayjs/ws'; + +@Configuration({ + imports: [koa, ws] +}) +export class MainConfiguration implements ILifeCycle { + @App() + koaApp: koa.Application; + + @App('webSocket') + wsApp: ws.Application; + + async onReady() { + this.koaApp.useMiddleweare(...); + this.wsApp.useMiddleweare(...); + } +} +``` + +Non-primary Application need to be obtained through the parameters or [ApplicationManager](./built_in_service#midwayapplicationmanager) of the `@App()` decorator. + +The parameter of the `@App()` decorator is the `namespace` of the component. + +Common namespaces are as follows: + +| Package | Namespace | +| ------------------ | --------- | +| @midwayjs/web | egg | +| @midwayjs/koa | koa | +| @midwayjs/express | express | +| @midwayjs/grpc | gRPC | +| @midwayjs/ws | webSocket | +| @midwayjs/socketio | socketIO | +| @midwayjs/faas | faas | +| @midwayjs/kafka | kafka | +| @midwayjs/rabbitmq | rabbitMQ | +| @midwayjs/bull | bull | + + + +### getAppDir + +Used to get the project root directory path. + +```typescript +this.app.getAppDir(); +// => /my_project +``` + + + +### getBaseDir + +It is used to obtain the basic path of the project TypeScript. By default, it is the `src` directory in development and the `dist` directory after compilation. + +```typescript +this.app.getBaseDir(); +// => /my_project/src +``` + + + +### getEnv + +Gets the current project environment. + +```typescript +this.app.getEnv(); +// => production +``` + + + +### getApplicationContext + +Gets the current global dependency injection container. + +```typescript +this.app.getApplicationContext(); +``` + + + +### getConfig + +Get the configuration. + +```typescript +// Get all configurations +this.app.getConfig(); +// Get specific key configuration +this.app.getConfig('koa'); +// Obtain multi-level configuration +this.app.getConfig('midwayLoggers.default.dir'); +``` + + + +### getLogger + +Obtain a Logger, do not pass parameters, and return appLogger by default. + +```typescript +this.app.getLogger(); +// => app logger +this.app.getLogger('custom'); +// => custom logger +``` + + + +### getCoreLogger + +Get Core Logger. + +```typescript +this.app.getCoreLogger(); +``` + + + +### getProjectName + +The project name is obtained from the `package.json`. + + + +### setAttr & getAttr + +Mount an object directly on the Application can cause difficulty in defining and maintaining it. + +In most cases, what users need is a temporary global data storage method, such as temporarily accessing a data across files within an application or component, saving from one class and obtaining it from another class. + +For this reason, Midway provides an API for global data access to solve such requirements. + +```typescript +this.app.setAttr('abc', { + a: 1 + B: 2 +}); +``` + +Get it in another place. + +```typescript +const value = this.app.getAttr('abc'); +console.log(value); +// { a: 1, B: 2} +``` + + + +### getNamespace + +Through the `getNamespace` API, you can get the [framework type](#main-application) of the component to which the current app belongs (that is, the `namespace` of the component). + +For example in the `koa` component. + +```typescript +this.app.getNamespace(); +// 'koa' +``` + + + +## Context + +A Context is a **request-level object**. Each time a user request is received, the framework instantiates a Context object, + +In Http scenarios, this object encapsulates the information requested by the user this time, or other methods for obtaining request parameters and setting response information. In scenarios such as WebSocket and Rabbitmq, Context also have their own attributes, subject to the definition of the framework. + +The following API is a common attribute or interface for each context implementation. + + + +### How to get + + +In the **default request scope**, that is, in the controller (Controller) or common service (Service), we can use `@Inject` to inject the corresponding instance. + + +for example, you can obtain the corresponding ctx instance in this way. + +```typescript +import { Inject, Controller, Get } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async home() { + // ... + } +} +``` + +Since `ctx` is a framework built-in ctx instance keyword, if you want to use a different attribute name, you can also modify the decorator parameters. + +```typescript +import { Inject, Controller, Get } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject('ctx') + customContextName: Context; + + @Get('/') + async home() { + // ... + } +} +``` + +If a service can be called by multiple upper-level frameworks, since the ctx types provided by different frameworks are different, it can be solved by type combination. + +```typescript +import { Inject, Controller, Get } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { Context as BullContext } from '@midwayjs/bull'; + +@Provide() +export class UserService { + + @Inject() + ctx: Context & BullContext; + + async getUser() { + // ... + } +} +``` + +In addition to explicit declarations, when the interceptor or decorator is designed, because we cannot know whether the user has written the ctx attribute, we can also obtain it through the built-in `REQUEST_ OBJ_CTX_KEY` field. + +For example: + +```typescript +import { Inject, Controller, Get, REQUEST_OBJ_CTX_KEY } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async home() { + ctx.logger.info(this.ctx === this[REQUEST_OBJ_CTX_KEY]); + // => true + } +} +``` + + + +### requestContext + +Midway mounts a `requestContext` attribute for each Context, a dependency injection container under the request scope, which is used to create objects under the request scope. + +```typescript +const userService = await this.ctx.requestContext.getAsync(UserService); +// ... +``` + + + +### logger + +The default logger object under the request scope, which contains context data. + +```typescript +this.ctx.logger.info('xxxx'); +``` + + + +### startTime + +The time when context execution starts. + +```typescript +this.ctx.startTime +// 1642820640502 +``` + + + +### setAttr & getAttr + +The method is the same as that used in `app`. The data of these methods is stored in the request link. As the request is destroyed, you can put some temporary data of the request in it. + +```typescript +this.ctx.setAttr('abc', { + a: 1 + B: 2 +}); +``` + +Get it in another place. + +```typescript +const value = this.ctx.getAttr('abc'); +console.log(value); +// { a: 1, B: 2} +``` + + + +### getLogger + +Gets the context log of a custom Logger. + +```typescript +this.ctx.getLogger('custom'); +// => custom logger +``` + + + +### getApp + +Get the app object of the current frame type from ctx. + +```typescript +const app = this.ctx.getApp(); +// app. getConfig(); +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/retry.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/retry.md new file mode 100644 index 000000000000..4b808f5b1a33 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/retry.md @@ -0,0 +1,215 @@ +# Retryable + +Starting from Midway v3.5.0, method custom retry logic is supported. + +Many times, we need to use `try` multiple times to wrap the function and handle errors on some method calls that are prone to failure or asynchronous. + +For example: + +```typescript +// Defines an asynchronous function + +async function invoke(id) { + + // Some remote call logic + +} + + +async invokeNew() { + let error; + try { + return await invoke(1); + } catch(err) { + error = err; + } + + try { + return await invoke(2); + } catch(err) { + error = err; + } + + if (error) { + // .... + } +} +``` + +You may try to call the `invoke` operation multiple times and use the try/catch to catch exceptions, resulting in repeated and lengthy business code writing. + + + +## Define retry functions + +We can use `retryWithAsync` method to package and simplify the whole process. + +```typescript +import { retryWithAsync } from '@midwayjs/core'; + +async function invoke(id) { + // ... +} + +async function someServiceMethod() { + // The default call, plus two retries, can be executed up to three times. + const invokeNew = retryWithAsync(invoke, 2); + + try { + return await invokeNew(1); + } catch(err) { + + // err + } +} +``` + +The method parameters and return values after the package are exactly the same as the original method. + +When the call is successful within the retry period and no error is thrown, the successful return value will be returned. + +If it fails, the `MidwayRetryExceededMaxTimesError` exception will be thrown. + +If it is used in a class, you may need to pay attention to the point of `this`. + +Examples are as follows: + +```typescript +import { retryWithAsync } from '@midwayjs/core'; + +export class UserService { + + async getUserData(userId: string) { + // wrap + const getUserDataOrigin = retryWithAsync( + this.getUserDataFromRemote, + 2, + { + receiver: this + } + ); + + // invoke + return getUserDataOrigin(userId); + } + + async getUserDataFromRemote(userId: string) { + // get data from remote + } +} +``` + + + +## This binding + +Starting from Midway v3.5.1, a `receiver` parameter has been added to bind this in the scene of the class for processing: + +- 1, the method of correct this point +- 2, the correctness of the definition of the package method. + +```typescript +// wrap +const getUserDataOrigin = retryWithAsync( + this.getUserDataFromRemote, + 2, + { + receiver: this, // This parameter is used to handle this pointing + } +); +``` + +If there is no such parameter, the code needs to be written as follows to bind this, and the definition of the `getUserDataOrigin` method returned is correct. + +```typescript +// wrap +const getUserDataOrigin = retryWithAsync( + this.getUserDataFromRemote.bind(this) as typeof this.getUserDataFromRemote, + 2, + { + receiver: this + } +); + + +``` + + + + + +## Number of retries + +The `retryWithAsync` provides a second parameter to declare the additional number of retries, which defaults to 1 (only retry once). + +This value refers to the number of additional retries after the default call. + + + +## Retry of synchronization + +Similar to `retryWithAsync`, we also provide `retryWith` synchronization method, the parameters and `retryWithAsync` are almost the same, no additional description. + + + +## Retry delay + +To prevent frequent retries from putting pressure on the server, you can set the retry interval. + +For example: + +```typescript +const invokeNew = retryWithAsync(invoke, 2, { + retryInterval: 2000, //After the execution fails, continue to try again after 2s. +}); +``` + +:::tip + +The synchronization method `retryWith` does not have this attribute. + +::: + + + +## Error thrown + +By default, if the number of retries is exceeded, the `MidwayRetryExceededMaxTimesError` exception is thrown. + +The `MidwayRetryExceededMaxTimesError` is the default exception of the framework, which can be captured and combed by the error filter, or the original exception can be handled from its attributes. + +```typescript +import { retryWithAsync, MidwayRetryExceededMaxTimesError } from '@midwayjs/core'; + +async function invoke(id) { + // ... +} + +async function someServiceMethod() { + // The default call, plus two retries, can be executed up to three times. + const invokeNew = retryWithAsync(invoke, 2); + + try { + return await invokeNew(1); + } catch(err) { + // err.name === 'MidwayRetryExceededMaxTimesError' + // err.cause instanceof CustomError => true + } + +} + +async invokeNew() { + throw new CustomError('customError'); +} +``` + +If you want to throw the original error object directly, you can configure the parameters. + +For example: + +```typescript +const invokeNew = retryWithAsync(invoke, 2, { + throwOriginError: true +}); +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/router_table.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/router_table.md new file mode 100644 index 000000000000..3efad327f10d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/router_table.md @@ -0,0 +1,550 @@ +# Router Table + +Starting from v2.8.0, Midway provides built-in routing table capability, and all Web frameworks will use this routing table to register routes. + +Starting from v3.4.0, the routing service will be provided as a Midway built-in service. + + +Available at application startup, onReady lifecycle, and thereafter. + + + +## Get the routing table service + +It has been instantiated by default and can be used by direct injection. + +```typescript +import { MidwayWebRouterService, MidwayServerlessFunctionService, Configuration, Inject } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + webRouterService: MidwayWebRouterService; + + @Inject() + serverlessFunctionService: MidwayServerlessFunctionService; + + async onReady() { + // Web routing + const routes = await this.webRouterService.getFlattenRouterTable(); + + // serverless function + const routes = await this.serverlessFunctionService.getFunctionList(); + } +} +``` + +The `MidwayServerlessFunctionService` only takes effect in Serverless scenarios, and the methods and `MidwayWebRouterService` are almost the same. + + + +## Routing information definition + + +Each routing information is represented by a `RouterInfo` definition and contains some attributes. + + +The definition is as follows: +```typescript +export interface RouterInfo { + /** + * router prefix + */ + prefix: string; + /** + * router alias name + */ + routerName: string; + /** + * router path, without prefix + */ + url: string | RegExp; + /** + * request method for http, like get/post/delete + */ + requestMethod: string; + /** + * invoke function method + */ + method: string; + description: string; + summary: string; + /** + * router handler function key,for IoC container load + */ + handlerName: string; + /** + * serverless func load key + */ + funcHandlerName: string; + /** + * controller provideId + */ + controllerId: string; + /** + * router middleware + */ + middleware: any[]; + /** + * controller middleware in this router + */ + controllerMiddleware: any[]; + /** + * request args metadata + */ + requestMetadata: any[]; + /** + * response data metadata + */ + responseMetadata: any[]; +} +``` +| Property | Type | Description | +| --- | --- | --- | +| prefix | string | Routing prefix, such as/or/api, the part written by the user on the @Controller decorator | +| routerName | string | Route name | +| url | string | The part of the route that removes the route prefix is also the part that the user writes on decorators such as @Get | +| requestMethod | string | Get/post/delete/put/all, etc. | +| method | string | The method name on the class actually called | +| description | string | Description, parameters on the route decorator | +| summary | string | Summary, parameters on the routing decorator | +| handlerName | string | Equivalent to controllerId.method | +| funcHandlerName | string | handler name written in @Func | +| controllerId | string | controller dependent injection container key(providerId) | +| middleware | string [] | Routing middleware string array | +| controllerMiddleware | string [] | Controller middleware string array | +| requestMetadata | any [] | Metadata of request parameters, @Query/@Body and other metadata | +| responseMetadata | any [] | Metadata of response parameters, @SetHeader/@ContentType and other metadata | + + + +## Routing priority + + +In the past, we need to pay attention to the loading sequence of routes. For example, the `/*` configuration of the route is used after the actual `/abc`. Otherwise, the route will be loaded to the wrong route. In the new version, we have automatically sorted this situation. + + +The rules are as follows: + +- 1. Absolute path rules have the highest priority such as `/ab/cb/e` +- 2. The asterisk can only appear at the end and must be after /, such as `/ab/cb/**` +- 3. If both the absolute path and the wildcard match a path, the absolute rule has a higher priority +- 4. When there are multiple wildcards that can match a path, the longest rule matches, such as `/ab/**` and `/ab/cd/**` when matching `/ab/cd/f` `/ab/cd/**` +- 5. If both `/` and `/*` can match / , but `/` takes precedence over `/*` + + +This rule is also consistent with the routing rules of the functions under the Serverless. + + +It is simply understood as "clear routes have the highest priority, long routes have the highest priority, and general distribution has the lowest priority". + + +For example, the priority of sorting is as follows (high to low): +```text +/api/invoke/abc +/api/invoke /* +/api/abc +/api /* +/abc +/* +``` + + + +## The current matching route + +Through `getMatchedRouterInfo` method, we can know the current route and which route information (RouterInfo) to match, so as to further process it. This logic is very useful in scenarios such as authentication. + +For example, in middleware, we can judge in advance before entering the controller. + +```typescript +import { Middleware, Inject, httpError, MidwayWebRouterService } from '@midwayjs/core'; + +@Middleware() +export class AuthMiddleware { + @Inject() + webRouterService: MidwayWebRouterService; + + resolve() { + return async (ctx, next) => { + // Query whether the current route is registered in the routing table. + const routeInfo = await this.webRouterService.getMatchedRouterInfo(ctx.path, ctx.method); + if (routeInfo) { + await next(); + } else { + throw new httpError.NotFoundError(); + } + } + } +} +``` + + + +## Routing information + + +### Get a list of flat routes + + +Gets a list of all routes that can be registered to HTTP services (including @Func/@Controller and all custom decorators registered according to standard information). + + +It will be automatically sorted from high to low by priority. + + +Definition: +```typescript +async getFlattenRouterTable(): Promise +``` + + +Gets the routing table API. +```typescript +const result = await this.webRouterService.getFlattenRouterTable(); +``` +Output example: +```typescript +[ + { + "prefix": "/", + "routerName": "", + "url": "/set_header", + "requestMethod": "get", + "method": "homeSet", + "description": "", + "summary": "", + "handlerName": "apiController.homeSet", + "funcHandlerName": "apiController.homeSet", + "controllerId": "apiController", + "middleware": [], + "controllerMiddleware": [], + "requestMetadata": [], + "responseMetadata": [ + { + "type": "web:response_header", + "setHeaders": { + "ccc": "ddd" + } + }, + { + "type": "web:response_header", + "setHeaders": { + "bbb": "aaa" + } + } + ], + }, + { + "prefix": "/", + "routerName": "", + "url": "/ctx-body", + "requestMethod": "get", + "method": "getCtxBody", + "description": "", + "summary": "", + "handlerName": "apiController.getCtxBody", + "funcHandlerName": "apiController.getCtxBody", + "controllerId": "apiController", + "middleware": [], + "controllerMiddleware": [], + "requestMetadata": [], + "responseMetadata": [], + }, + // ... +] +``` + + +### Get Router information list + + +In Midway, each Controller corresponds to a Router object, and each Router will have a route prefix, in which all routes will be sorted according to the above rules. + + +Router itself will also be sorted by prefix. + + +Definition: +```typescript +export interface RouterPriority { + prefix: string; + priority: number; + middleware: any[]; + routerOptions: any; + controllerId: string; +} + +async getRoutePriorityList(): Promise +``` +Router's data is relatively simple. + + + +| Property | Type | Description | +| --- | --- | --- | +| prefix | string | Routing prefix, such as/or/API, the part written by the user on the @Controller decorator | +| priority | number | Router's priority, @Priority the value filled in by the decorator,/root Router has the lowest default priority, which is -999 | +| middleware | string [] | Controller middleware string array | +| controllerId | string | controller dependent injection container key(providerId) | +| routerOptions | any | @options of Controller decorator | + + + +Get route table API. + +```typescript +const list = await collector.getRoutePriorityList(); +``` +Output example: +```typescript +[ + { + "prefix": "/case", + "priority": 0, + "middleware": [], + "routerOptions": { + "middleware": [], + "sensitive": true + }, + "controllerId": "caseController" + }, + { + "prefix": "/user", + "priority": 0, + "middleware": [], + "routerOptions": { + "middleware": [], + "sensitive": true + }, + "controllerId": "userController" + }, + { + "prefix": "/", + "priority": -999, + "middleware": [], + "routerOptions": { + "middleware": [], + "sensitive": true + }, + "controllerId": "apiController" + } +] +``` + + +### Get hierarchical routes + + +In some cases, we need to get hierarchical routes, including which routes are under which controller (Controller), so as to better create routes. + + +Midway also provides a method for obtaining hierarchical routing tables. The hierarchy is automatically sorted from high to low by priority. + + +Definition: +```typescript +async getRouterTable(): Promise> +``` + + +Obtain the hierarchical routing table API and return a map with the key as the prefix string of the controller's routing prefix. If the route prefix is not explicitly specified (such as functions or other scenarios), it will be classified as/route prefix. +```typescript +const result = await collector.getRouterTable(); +``` +Output example: +```typescript +Map(3) { + '/' => [ + { + prefix: '/', + routerName: '', + url: '/set_header', + requestMethod: 'get', + method: 'homeSet', + description: '', + summary: '', + handlerName: 'apiController.homeSet', + funcHandlerName: 'apiController.homeSet', + controllerId: 'apiController', + middleware: [], + controllerMiddleware: [], + requestMetadata: [], + responseMetadata: [Array], + }, + { + prefix: '/', + routerName: '', + url: '/ctx-body', + requestMethod: 'get', + method: 'getCtxBody', + description: '', + summary: '', + handlerName: 'apiController.getCtxBody', + funcHandlerName: 'apiController.getCtxBody', + controllerId: 'apiController', + middleware: [], + controllerMiddleware: [], + requestMetadata: [], + responseMetadata: [], + }, + // ... + ] +} +``` + + + +### Get all function information + +Same as `getFlattenRouterTable`, except that the returned content contains more information about the function part. + +Definition: + +```typescript +async getFunctionList(): Promise +``` + + +Gets the function routing table API. + +```typescript +const result = await this.serverlessFunctionService.getFunctionList(); +``` + + + + + +## Add route + +### Dynamically add web controllers + +Sometimes we want to dynamically add a controller according to certain conditions, we can use this method. + +First, we need to have a controller class, but do not use the `@Controller` decorator. + +```typescript +import { Get, Provide } from '@midwayjs/core'; + +// Note that @Controller decoration is not used here +@Provide() +export class DataController { + @Get('/query_data') + async getData() { + return 'hello world'; + } +} +``` + +We can add it dynamically by `addController` method. + +```typescript +// src/configuration.ts +import { MidwayWebRouterService, Configuration, Inject } from '@midwayjs/core'; +import { DataController } from './controller/data.controller'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + webRouterService: MidwayWebRouterService; + + async onReady() { + if (process.env.NODE_ENV === 'test') { + this.webRouterService.addController(DataController, { + prefix: '/test', + routerOptions: { + middleware: [ + // ... + ] + } + }); + } + // ... + } +} +``` + +The `addController` method, the first parameter is the class itself, and the second parameter is the same as the `@Controller` decorator parameter. + + + +### Dynamically add web routing functions + +In some scenarios, users can add methods directly and dynamically. + +```typescript +// src/configuration.ts +import { MidwayWebRouterService, Configuration, Inject } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + webRouterService: MidwayWebRouterService; + + async onReady() { + // koa/egg format + this.webRouterService.addRouter(async (ctx) => { + return 'hello world'; + }, { + url: '/api/user', + requestMethod: 'GET', + }); + // ... + + // express format + this.webRouterService.addRouter(async (req, res) => { + return 'hello world'; + }, { + url: '/api/user', + requestMethod: 'GET', + }); + } +} +``` + +The method of `addRouter`, the first parameter is the route method body, and the second parameter is the metadata of the route. + + + +### Dynamically add Serverless functions + +Similar to adding dynamic Web routes, it is added using built-in `MidwayServerlessFunctionService` services. + +For example, add an http function. + +```typescript +// src/configuration.ts +import { MidwayServerlessFunctionService, Configuration, Inject } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + serverlessFunctionService: MidwayServerlessFunctionService; + + async onReady() { + this.serverlessFunctionService.addServerlessFunction(async (ctx, event) => { + return 'hello world'; + }, { + type: ServerlessTriggerType.HTTP, + metadata: { + method: 'get', + path: '/api/hello' + }, + functionName: 'hello', + handlerName: 'index.hello', + }); + } +} +``` + +The information `metadata` is the same as the parameters of the @ServerlessTrigger. + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/aliyun_faas.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/aliyun_faas.md new file mode 100644 index 000000000000..9e1b53abe481 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/aliyun_faas.md @@ -0,0 +1,726 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Deploy to Alibaba Cloud Function Compute + +Alibaba Cloud Serverless is one of the first teams in China to provide serverless computing services. It relies on Alibaba Cloud's powerful cloud infrastructure service capabilities to continuously achieve technological breakthroughs. At present, Taobao, Alipay, DingTalk, AutoNavi, etc. have applied Serverless to production business. Serverless products on the cloud have been successfully implemented in tens of thousands of companies such as Pumpkin Movie, NetEase Cloud Music, iQiyi Sports, and Lilith. + +Alibaba Cloud Serverless includes many products, such as Function Compute FC, Lightweight Application Engine SAE, etc. This article mainly uses its **Function Compute** part. + +The following are common methods of using, testing, and deploying function triggers. + + + +## Deployment type + +Alibaba Cloud has many types of function computing deployments, including the following types according to the different containers they run. + +| Name | Functional limitations | Description | Deployment Media | +| ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ---------------------- | +| Built-in runtime | Streaming requests and responses are not supported; requests and responses that are too large are not supported. | Only function interfaces can be deployed, no custom ports are required, zip packages are built for platform deployment | zip package deployment | +| Custom Runtime | | You can deploy standard applications, start port 9000, use the system image provided by the platform, and build a zip package for platform deployment | zip package deployment | +| Custom Container | | You can deploy standard applications, start port 9000, control all environmental dependencies yourself, and build a Dockerfile for platform deployment | Dockerfile deployment | + +There are three ways to create functions on the platform. + +![](https://img.alicdn.com/imgextra/i1/O1CN01JrlhOw1EJBZmHklbq_!!6000000000330-2-tps-1207-585.png) + + + +## Pure function development (built-in runtime) + +Below we will use the **"built-in runtime deployment"** pure function as an example. + + + +### Trigger code + + + + +Publish a function that does not contain a trigger. This is the simplest type. You can manually trigger parameters directly through event, or you can bind other triggers on the platform. + +Bind event triggers via the `@ServerlessTrigger` decorator directly in code. + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.EVENT) + async handleEvent(event: any) { + return event; + } +} +``` + + + + + +Alibaba Cloud's HTTP triggers are different from those of other platforms. They are another set of triggers independent of the API gateway that serve HTTP scenarios. This trigger is easier to use and configure than API Gateway. + +Bind HTTP triggers via the `@ServerlessTrigger` decorator directly in code. + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.HTTP, { + path: '/', + method: 'get', + }) + async handleHTTPEvent(@Query() name = 'midway') { + return `hello ${name}`; + } +} +``` + + + + + +The API gateway is special in the Alibaba Cloud function system. It is similar to creating a trigger-free function and binding it to a specific path through the platform gateway. + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.API_GATEWAY, { + path: '/api_gateway_aliyun', + method: 'post', + }) + async handleAPIGatewayEvent(@Body() name) { + return `hello ${name}`; + } +} +``` + + + + + +Scheduled task triggers are used to execute a function regularly. + +:::info +Warm reminder, please turn off trigger automatic execution in time after testing the function to avoid excessive deductions. +::: + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; +import type { TimerEvent } from '@midwayjs/fc-starter'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.TIMER) + async handleTimerEvent(event: TimerEvent) { + this.ctx.logger.info(event); + return 'hello world'; + } +} +``` + +**Event Structure** + +The structure returned by the Timer message is as follows, described in the `TimerEvent` type. + +```json +{ + triggerTime: new Date().toJSON(), + triggerName: 'timer', + payload: '', +} +``` + + + + + +OSS is used to store some resource files and is Alibaba Cloud's resource storage product. When a file is created or updated in OSS, the corresponding function will be triggered and executed. + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; +import type { OSSEvent } from '@midwayjs/fc-starter'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.OS) + async handleOSSEvent(event: OSSEvent) { + //xxx + } +} +``` + + + +**Event Structure** + +The structure returned by OSS messages is as follows, which is described in the `FC.OSSEvent` type. + +```json +{ + "events": [ + { + "eventName": "ObjectCreated:PutObject", + "eventSource": "acs:oss", + "eventTime": "2017-04-21T12:46:37.000Z", + "eventVersion": "1.0", + "oss": { + "bucket": { + "arn": "acs:oss:cn-shanghai:123456789:bucketname", + "name": "testbucket", + "ownerIdentity": "123456789", + "virtualBucket": "" + }, + "object": { + "deltaSize": 122539, + "eTag": "688A7BF4F233DC9C88A80BF985AB7329", + "key": "image/a.jpg", + "size": 122539 + }, + "ossSchemaVersion": "1.0", + "ruleId": "9adac8e253828f4f7c0466d941fa3db81161e853" + }, + "region": "cn-shanghai", + "requestParameters": { + "sourceIPAddress": "140.205.128.221" + }, + "responseElements": { + "requestId": "58F9FF2D3DF792092E12044C" + }, + "userIdentity": { + "principalId": "123456789" + } + } + ] +} +``` + + + + + +:::info + +* 1. Alibaba Cloud Message Queue will incur certain fees for Topic and Queue. +* 2. The default message queue format provided is JSON + +::: + + + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; +import type {MNSEvent} from '@midwayjs/fc-starter'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.MQ) + async handleMNSEvent(event: MNSEvent) { + // ... + } +} +``` + + + +**Event Structure** + +The structure returned by the MNS message is as follows, described in the `FC.MNSEvent` type. + +```json +{ + "Context": "user custom info", + "TopicOwner": "1186202104331798", + "Message": "hello topic", + "Subscriber": "1186202104331798", + "PublishTime": 1550216302888, + "SubscriptionName": "test-fc-subscibe", + "MessageMD5": "BA4BA9B48AC81F0F9C66F6C909C39DBB", + "TopicName": "test-topic", + "MessageId": "2F5B3C281B283D4EAC694B7425288675" +} +``` + + + + + +:::info + +More configurations of triggers are platform-related and will be written in `s.yaml`, such as the time interval of scheduled tasks, etc. For more details, please see the deployment paragraph below. + +::: + + + +### Type definition + +The definition of FC will be exported by the adapter. In order for the definition of `ctx.originContext` to remain correct, it needs to be added to `src/interface.ts`. + +```typescript +// src/interface.ts +import type {} from '@midwayjs/fc-starter'; +``` + +Additionally, definitions for various Event types are provided. + +```typescript +//Event type +import type { + OSSEvent, + MNSEvent, + SLSEEvent, + CDNEvent, + TimerEvent, + APIGatewayEvent, + TableStoreEvent, +} from '@midwayjs/fc-starter'; +// InitializeContext type +import type { InitializeContext } from '@midwayjs/fc-starter'; +``` + + + +### Local development + +HTTP triggers and API Gateway types can be developed locally through local `npm run dev` and development methods similar to traditional applications. Other types of triggers cannot be developed locally using dev and can only be tested by running `npm run test`. + + + +### Local testing + +Similar to traditional application testing, use the `createFunctionApp` method to create a function app and use the `close` method to close it. + +```typescript +import { Application, Context, Framework } from '@midwayjs/faas'; +import { mockContext } from '@midwayjs/fc-starter'; +import { createFunctionApp } from '@midwayjs/mock'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from event trigger', async () => { + + // create app + const app: Application = await createFunctionApp(join(__dirname, '../'), { + initContext: mockContext(), + }); + + // ... + + await close(app); + }); +}); +``` + +The `mockContext` method is used to simulate a FC Context data structure. You can customize a similar structure or modify some data. + +```typescript +import { Application, Context, Framework } from '@midwayjs/faas'; +import { mockContext } from '@midwayjs/fc-starter'; +import { createFunctionApp } from '@midwayjs/mock'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from event trigger', async () => { + + // create app + const app: Application = await createFunctionApp(join(__dirname, '../'), { + initContext: Object.assign(mockContext(), { + function: { + name: '***', + handler: '***' + } + }), + }); + + // ... + + await close(app); + }); +}); +``` + +Different triggers have different testing methods. Some common triggers are listed below. + + + + +Obtain the class instance through `getServerlessInstance`, directly call the instance method, and pass in the parameters for testing. + +```typescript +import { HelloAliyunService } from '../src/function/hello_aliyun'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from event trigger', async () => { + // ... + const instance = await app.getServerlessInstance(HelloAliyunService); + expect(await instance.handleEvent('hello world')).toEqual('hello world'); + // ... + }); +}); +``` + + + + + +Similar to the application, create a function app through `createFunctionApp` and test it through `createHttpRequest`. + +```typescript +import { HelloAliyunService } from '../src/function/hello_aliyun'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from http trigger', async () => { + // ... + const result = await createHttpRequest(app).get('/').query({ + name: 'zhangting', + }); + expect(result.text).toEqual('hello zhangting'); + // ... + }); +}); +``` + + + + + +The same as HTTP testing, create a function app through `createFunctionApp` and test it through `createHttpRequest`. + +```typescript +import { createHttpRequest } from '@midwayjs/mock'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from http trigger', async () => { + // ... + const result = await createHttpRequest(app).post('api_gateway_aliyun').send({ + name: 'zhangting', + }); + + expect(result.text).toEqual('hello zhangting'); + // ... + }); +}); +``` + + + + + +Different from HTTP testing, create a function app through `createFunctionApp`, obtain an instance of the entire class through `getServerlessInstance`, and call a specific method for testing. + +The structure passed by the platform can be quickly created through the `mockTimerEvent` method. + +```typescript +import { HelloAliyunService } from '../src/function/hello_aliyun'; +import { mockTimerEvent } from '@midwayjs/fc-starter'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from timer trigger', async () => { + // ... + const instance = await app.getServerlessInstance(HelloAliyunService); + expect(await instance.handleTimerEvent(mockTimerEvent())).toEqual('hello world'); + // ... + }); +}); +``` + + + + + +Unlike HTTP testing, through `createFunctionApp`Create a function app, get an instance of the entire class through `getServerlessInstance`, and call a specific method for testing. + +The structure passed by the platform can be quickly created through the `createOSSEvent` method. + +```typescript +import { HelloAliyunService } from '../src/function/hello_aliyun'; +import { mockOSSEvent } from '@midwayjs/fc-starter'; + +describe('test/hello_aliyun.test.ts', () => { + it('should get result from oss trigger', async () => { + // ... + const instance = await app.getServerlessInstance(HelloAliyunService); + expect(await instance.handleOSSEvent(mockOSSEvent())).toEqual('hello world'); + // ... + }); +}); +``` + + + + + +Different from HTTP testing, create a function app through `createFunctionApp`, obtain an instance of the entire class through `getServerlessInstance`, and call a specific method for testing. + +The structure passed in by the platform can be quickly created through the `createMNSEvent` method. + +```typescript +import { HelloAliyunService } from '../src/function/hello_aliyun'; +import { mockMNSEvent } from '@midwayjs/fc-starter'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from oss trigger', async () => { + // ... + const instance = await app.getServerlessInstance(HelloAliyunService); + expect(await instance.handleMNSEvent(mockMNSEvent())).toEqual('hello world'); + // ... + }); +}); +``` + + + + + +## Pure function deployment (built-in runtime) + +The following will briefly describe how to use Serverless Devs to deploy to Alibaba Cloud functions. + +### 1. Confirm the launcher + +In the `provider` section of `f.yml` in the project root directory, make sure the starter is `@midwayjs/fc-starter`. + +```yaml +provider: + name: aliyun + starter: '@midwayjs/fc-starter' +``` + + + +### 2. Install Serverless Devs tools + +aliyun uses [Serverless Devs tool](https://www.serverless-devs.com/) for function deployment. + +You can install it globally. + +```bash +$ npm install @serverless-devs/s -g +``` + +Refer to the [Key Configuration](https://docs.serverless-devs.com/serverless-devs/quick_start) document for configuration. + + + +### 3. Write a Serverless Devs description file + +Create a `s.yaml` in the root directory and add the following content. + +```yaml +edition: 1.0.0 +name: "midwayApp" # project name +access: "default" # Secret key alias + +vars: + service: + name: fc-build-demo + description: 'demo for fc-deploy component' +services: + project-0981cd9b07: + component: devsapp/fc + props: + region: cn-hangzhou + service: ${vars.service} + function: + name: hello # function name + handler: helloHttpService.handleHTTPEvent + codeUri: '.' + initializer: helloHttpService.initializer + customDomains: + - domainName: auto + protocol: HTTP + routeConfigs: + - path: /* + serviceName: ${vars.service.name} + functionName: helloHttpService-handleHTTPEvent + triggers: + - name: http + type: http + config: + methods: + -GET + authType: anonymous + +``` + +Every time you add a function, you need to adjust the `s.yaml` file. For this reason, Midway provides a `@midwayjs/serverless-yaml-generator` tool to write the decorator function information into `s.yaml`. + +```diff +{ +"scripts": { ++ "generate": "serverless-yaml-generator", + }, + "devDependencies": { ++ "@midwayjs/serverless-yaml-generator": "^1.0.0", + }, +} +``` + +By executing the following command, you can fill existing function information into `s.yaml` and generate an entry file to facilitate troubleshooting. + +```bash +$ npm run generate +``` + +The tool will look for the configuration in `s.yaml` using the function name as the key. + +* 1. If there is a function, it will cover specific fields, such as handler, http trigger methods +* 2. If the function does not exist, a new function will be added +* 3. The tool will not write the http routing method. To simplify subsequent updates, you can provide a `/*` route (as an example) + +We recommend that users only define the basic function name, function handler, and basic trigger information (such as the path and method of the http trigger) in the decorator, and write the rest in `yaml`. + +The complete configuration of `s.yaml` is more complicated. For details, please refer to [Description File Specification](https://docs.serverless-devs.com/serverless-devs/yaml). + + + +### 4. Write a deployment script + +Since deployment has multiple steps such as building and copying, we can write a deployment script to unify this process. + +For example, create a new `deploy.sh` file in the project root directory with the following content. + +```bash +#!/bin/bash + +set -e + +# Build product directory +export BUILD_DIST=$PWD/.serverless +#Build start time in milliseconds +export BUILD_START_TIME=$(date +%s%3N) + +echo "Building Midway Serverless Application" + +#Print the current directory cwd +echo "Current Working Directory: $PWD" +#Print result directory BUILD_DIST +echo "Build Directory: $BUILD_DIST" + +#Install current project dependencies +npm i + +# Execute build +./node_modules/.bin/tsc || return 1 +# Generate entry file +./node_modules/.bin/serverless-yaml-generator || return 1 + +# If the .serverless folder exists, delete it and recreate it +if [ -d "$BUILD_DIST" ]; then + rm -rf $BUILD_DIST +fi + +mkdir $BUILD_DIST + +# Copy dist, *.json, *.yml to the .serverless directory +cp -r dist $BUILD_DIST +cp *.yaml $BUILD_DIST 2>/dev/null || : +cp *.json $BUILD_DIST 2>/dev/null || : +# Move the entry file to the .serverless directory +mv *.js $BUILD_DIST 2>/dev/null || : + +# Enter the .serverless directory +cd $BUILD_DIST +# Install online dependencies +npm install --production + +echo "Build success" + +# Deploy in the .serverless directory +s deploy + +``` + +You can put this `deploy.sh` file in the `deploy` command of `package.json`, and execute `npm run deploy` for subsequent deployment. + +```json +{ + "scripts": { + "deploy": "sh deploy.sh" + } +} +``` + +:::tip + +* 1. `deploy.sh` is only tested on mac, other platforms can be adjusted by yourself. +* 2. The script content can be adjusted according to business logic, such as copied files, etc. + +::: + + + +## Custom runtime deployment + +### 1. Create a project + +Custom runtimes can be deployed using standard projects. Since port 9000 needs to be provided, the Midway koa/express/express project needs to be created. + +For initialization projects, please refer to [Creating the first application](/docs/quickstart). + +### 2. Adjust the port + +In order to avoid affecting local development, we only add ports at the entrance `bootstrap.js`. + +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); + +// Explicitly introduce user code as a component +Bootstrap.configure({ + globalConfig: { + koa: { + port: 9000, + } + } +}).run() +``` + +For different framework modification ports, please refer to: + +* [koa modification port](/docs/extensions/koa) +* [Egg modification port](/docs/extensions/egg) +* [Express modification port](/docs/extensions/express) + +### 3. Platform deployment configuration + +* 1. Select the running environment, such as `Node.js 18` +* 2. Select the code upload method, for example, you can upload a zip package locally +* 3. The startup command specifies node bootstrap.js +* 4. Listening port 9000 + +![](https://img.alicdn.com/imgextra/i3/O1CN010JA2GU1lxNeqm81AR_!!6000000004885-2-tps-790-549.png) + +After the configuration is completed, upload the compressed package to complete the deployment. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_context.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_context.md new file mode 100644 index 000000000000..8dd628ffb2af --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_context.md @@ -0,0 +1,212 @@ +# Function Context + +## Event conversion + +Midway Serverless has carried out input parameter wrapping according to the situation of different platforms. At the same time, when the function uses apigw(API gateway) and http (Aliyun) triggers, it has made special treatment on the input parameter (event). In order to simplify and unify the writing method, the event is unified and regularized into code similar to koa writing method. + +Normal trigger scenario: + +```typescript +import { Context } from '@midwayjs/faas'; +import { Provide } from '@midwayjs/core'; + +@Provide() +export class Index { + + @Inject() + ctx: Context; + + @ServerlessTrigger(...) + async handler(event) { + return 'hello world'; + } +} +``` + +HTTP and API gateway trigger scenarios: + +```typescript +import { Context } from '@midwayjs/faas'; +import { Provide } from '@midwayjs/core'; + +@Provide() +export class Index { + + @Inject() + ctx: Context; + + @ServerlessTrigger(...) + async handler() { + // The following two writing methods are the same + // this.ctx.body = 'hello world'; + return 'hello world'; + } +} +``` + +## Context + +Every time a function is called, a new ctx (function context) is created. For attributes or methods on ctx, we provide ts definitions. + +:::info +In Serverless v1 era, our definition is called FaaSContext. In v2, we have unified the definition and application, which is more consistent. +::: + +### ctx.logger + +- return `ILogger` + +The log object of each request passed by the runtime. The default value is console. + +```typescript +ctx.logger.info('hello'); +ctx.logger.warn('hello'); +ctx.logger.error('hello'); +``` + +### ctx.env + +- return `string` + +The current startup environment, that is, the value of the NODE_ENV or MIDWAY_SERVER_ENV. The default value is prod. + +```typescript +ctx.env; //default prod +``` + +### ctx.requestContext + +- return `MidwayRequestContainer` + +The IoC request scope container of midway faas is used to obtain object instances in other IoC containers. + +```typescript +const userService = await ctx.requestContext.getAsync(UserService); +``` + +## FaaSHTTPContext + +`Context` definitions are inherited from `FaaSHTTPContext`. The former retains the latter. In most scenarios, the former can be used directly. The latter is only available under apigw(API Gateway) and http (Aliyun) triggers. + +For ordinary users, just use `Context` definition directly. + +```typescript +import { Context } from '@midwayjs/faas'; + +@Inject() +ctx: Context; +``` + +In the ctx object, we provide some API similar to writing traditional Koa Web applications. The advantage of this is to reduce the cognitive cost of users, and, to a certain extent, compatibility with the original traditional code and community middleware becomes possible. + +We have provided some APIs similar to traditional APIs that support common capabilities. **Different platforms may not be exactly the same**. We will point out specific APIs. + +### ctx.request + +- return `FaaSHTTPRequest` + +HTTP Request object simulated by FaaS. + +### ctx.response + +- return `FaaSHTTPResponse` + +HTTP Response object simulated by FaaS. + +### **ctx.params** + +The proxy is `request.pathParameters` and is available under http triggers (Aliyun) and API gateway triggers. + +```typescript +// /api/user/[id] /api/user/faas +ctx.params.id; // faas +``` + +### ctx.set + +Set the response header, which is the `response.setHeader` agent. + +```typescript +ctx.set('X-FaaS-Duration', 2100); +``` + +### ctx.status + +Sets the return status code, which represents the `response.statusCode` from. + +```typescript +ctx.status = 404; +``` + + + +### Request aliases + +The attributes listed below are from the [Request](# k6AZp) object proxy + +- `ctx.headers` +- `ctx.method` +- `ctx.url` +- `ctx.path` +- `ctx.ip` +- `ctx.query` +- `ctx.get()` + +### Response aliases + +The attributes listed below are from the [Response](#kfTOD) object proxy + +- `ctx.body=` +- `ctx.status=` alias to `response.statusCode` +- `ctx.type=` +- `ctx.set()` alias to `response.setHeader` + + + +## FaaSHTTPRequest + +This object is obtained by converting the `event` and `context` input parameters of the function. + +### request.headers + +Object containing all request headers, key-value pair storage. + +### request.ip + +obtain the client request ip address. + +:::info +On Alibaba Cloud FC, only the HTTP trigger can obtain the value, and the api gateway cannot obtain the value for the time being. +::: + +### request.url + +the client requests the complete url. + +### request.path + +the client request path. + +### request.method + +The requested method. + +### request.body + +The body of the POST request has been parsed to JSON. + +## FaaSHTTPResponse + +This object is obtained by converting the `event` and `context` input parameters of the function. + +### response.setHeader + +Set the response header. + +### response.statusCode + +Set the return status code. + +### response.body + +Set the content of the response body, `string` or `buffer`. diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_dev.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_dev.md new file mode 100644 index 000000000000..91c334b3f1fe --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_dev.md @@ -0,0 +1,152 @@ +# Develop function + +## Initialization code + +Let's develop our first pure HTTP function and try to deploy it to a cloud environment. + +Execute `npm init midway` and select the `faas` scaffolding. + + + +## Directory Structure + +The following is the most streamlined structure of a function. The core will include a `f.yml` standardized function file and the TypeScript project structure. + +```bash +. +├── f.yml # Standardized spec file +├── package.json # Project dependencies +├── src +│ └── function +│ └── hello.ts # Function file +└── tsconfig.json +``` + +Let’s take a brief look at the file contents. + +- `f.yml` function definition file +- `tsconfig.json` TypeScript configuration file +- `src` function source code directory +- `src/function/hello.ts` sample function file + +We place functions in the `function` directory to better separate them from other types of code. + +## Function file + +Let’s first take a look at the function file. The traditional function is a `function`. In order to be more consistent with the midway system and use our dependency injection, it is turned into a Class here. + +Through the `@ServerlessTrigger` decorator, we mark the method as an HTTP interface and mark the `path` and `method` attributes. + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType, Query } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloHTTPService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.HTTP, { + path: '/', + method: 'get', + }) + async handleHTTPEvent(@Query() name = 'midway') { + return `hello ${name}`; + } +} +``` + +In addition to triggers, we can also use the `@ServerlessFunction` decorator to describe function-level meta-information, such as function name, concurrency, etc. + + +In this way, when we use multiple triggers on a function, we can set it up like this. + +```typescript +import { Provide, Inject, ServerlessFunction, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloServerlessService { + @Inject() + ctx: Context; + + // Multiple triggers for one function + @ServerlessFunction({ + functionName: 'abcde', + }) + @ServerlessTrigger(ServerlessTriggerType.TIMER, { + name: 'timer' + }) + async handleTimerEvent() { + //TODO + } +} +``` + +:::caution +Note that some platforms cannot put different types of triggers in the same function. For example, Alibaba Cloud stipulates that HTTP triggers and other triggers cannot take effect in the same function at the same time. +::: + +## Function definition file + +`f.yml` is a file with framework identification function information, the content is as follows. + +```yaml +provider: + name: aliyun #Publishing platform, here is Alibaba Cloud + starter: '@midwayjs/fc-starter' + +``` + +The `@midwayjs/fc-starter` here is the adapter that adapts to the aliyun function. + + + +## Trigger decorator parameters + +The `@ServerlessTrigger` decorator is used to define different triggers. Its parameters are each trigger information and general trigger parameters. + +For example, the trigger name is changed to abc. + +```typescript + @ServerlessTrigger(ServerlessTriggerType.TIMER, { + name: 'abc', // Trigger name + }) +``` + +If there is only one trigger, you can write the function name information to the trigger. + +```typescript + @ServerlessTrigger(ServerlessTriggerType.TIMER, { + functionName: 'hello' // If there is only one trigger, you can omit a decorator + name: 'abc', + }) +``` + + + +## Function decorator parameters + +The `@ServerlessFunction` decorator is used to define functions. If there are multiple triggers, the function name can be modified uniformly. + + +for example: + +```typescript +@ServerlessFunction({ + functionName: 'abcde' // function name +}) +``` + +## Local development + +Local development of HTTP functions is the same as traditional Web. Enter the following command. + +```shell +$ npm rundev +$ open http://localhost:7001 +``` + +Midway will start the HTTP server, open the browser, visit [http://127.0.0.1:7001](http://127.0.0.1:7001), the browser will print out the `Hello midwayjs` information. + +Non-HTTP functions cannot be triggered directly. Instead, you can write test functions for execution. \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_error.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_error.md new file mode 100644 index 000000000000..2dce12ebc7e9 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_error.md @@ -0,0 +1,60 @@ +# Default error behavior + +## Error value processing + + + +In order to ensure security, Midway has done some special treatment for errors returned in Serverless scenarios. + + +When the function business throws an error, the frame side will catch all the errors and return the error of "Internal Server Error. + + +For example, our function returns an error: + +```typescript +@ServerlessTrigger(//...) +async invoke() { + throw new Error('abc'); +} +``` + + + +Whether it is HTTP or non-HTTP triggers, the framework part has corresponding processing. + + +In **off-line environments**, such as `NODE_ENV = local`, the framework will reveal the entire error through the gateway. + + +For example (complete error stack): + +``` +2021-07-02T05:57:08.553Z 19be4d99-c9cb-4c4c-aac2-9330d31b4408 [error] Error: abc + at hello (/code/dist/function/index.js:17:15) + at invokeHandler (/code/node_modules/_@midwayjs_faas@2.11.2-beta.1@@midwayjs/faas/dist/framework.js:174:56) + at processTicksAndRejections (internal/process/task_queues.js:97:5) + at (/code/node_modules/_@midwayjs_faas@2.11.2-beta.1@@midwayjs/faas/dist/framework.js:117:40) + at cors (/code/node_modules/_@koa_cors@3.1.0@@koa/cors/index.js:98:16) + at invokeHandlerWrapper (/code/node_modules/_@midwayjs_runtime-engine@2.11.1@@midwayjs/runtime-engine/dist/lightRuntime.js:18:28) { +} +``` + + + +In the online environment, the framework will directly return **"Internal Server Error"**, but the log is a complete stack. + + +As shown in Fig. + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1625205528496-96f7d2b8-d728-4f04-82f4-f2617e00720b.png) + + + +## Adjustment error return + +The above is the default behavior. If an error needs to be displayed in a special environment, you can use the environment variable to enable forced output. + +```typescript +process.env.SERVERLESS_OUTPUT_ERROR_STACK = 'true'; +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_intro.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_intro.md new file mode 100644 index 000000000000..bd21f0d15988 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_intro.md @@ -0,0 +1,68 @@ +# Introduce + +## What Midway Serverless Can Do + +Midway Serverless is a Serverless framework for building Node.js cloud functions. help you significantly reduce maintenance costs and focus more on product development in the cloud-native era. + +## The relationship between Midway Serverless and Midway + +Midway Serverless is a set of development solutions for Serverless cloud platforms produced by Midway. Its content mainly includes the function framework `@midwayjs/faas`, as well as a series of tool chains and launchers matching the platform. + +After Midway Serverless 2.0, Midway Serverless and Midway's capabilities are reused, with the same CLI tool chain, compiler, decorator, etc. + +At present, Midway Serverless is mainly aimed at FaaS scenarios. + +## What the function (FaaS) can do + +Many people are not very clear about functions or do not understand what they can do. The current function can be used as a small container. Originally, we wanted to write a complete application to carry the capacity. Now we only need to write the middle logic part and consider the input and output data. + +By binding the trigger of the platform, you can carry traffic such as HTTP,Socket, etc. + +Through the BaaS SDK provided by the platform, you can call the database, Redis and other services. + +Through functions, traditional HTTP API services can be provided, and beautiful pages can be rendered one by one in combination with existing front-end frameworks (react,vue, etc.). It can also be used as an independent data module, waiting to be called (triggered), such as common file upload changes, decompression, etc. It can also be used as a logical part of a timing task and executed at a specified time or interval. + +With the change of time, the iteration of the platform, the ability of the function will become stronger, and the user's cost of getting started, the server cost will become lower and lower. + +## What can't a function do + +The architecture of functions determines that some requirements cannot be supported. In addition, functions and applications are still different in capability. + +Function not applicable: + +- The execution time exceeds the limit under the function configuration (preferably not more than 5S) +- stateful, storing data locally +- Long links, such as ws, etc. +- Background tasks, executed by big data +- Relying on multi-process communication +- Large file upload (for example, the gateway limit is more than 2M) +- Custom environment, such as nginx configuration, C ++ library (C ++ addon dynamic link library, etc.), Python version depends on +- A large number of server-side caches +- Fixed ip + +## Description of terms + +### function + +A logical snippet of code is executed by wrapping a common entry file. Functions are single link and stateless. Now many people think that Serverless = FaaS + BaaS, while FaaS is a stateless function. BaaS solves stateful services. + +### Function group + +The logical grouping name of multiple functions, corresponding to the original application concept. + +### Trigger + +Triggers, also called Event, Trigger, etc., specifically refer to the way functions are triggered. +Different from traditional development concepts, functions do not need to start a service to listen to data, but bind one (or more) triggers. Data is called to functions through a mechanism similar to event triggering. + +### Function runtime + +The English name is Runtime, which specifically refers to the environment in which the function is executed. It may be a mirror image or a Node.js code package on each platform. For example, there are kubeless when common communities are running. The code package will realize the capabilities of docking various interfaces of the platform, handling exceptions, forwarding logs, etc. + +### Publishing platform + +The platform finally carried by the function is now the most common in the community such as Aliyun FC, Tencent Cloud SCF,AWS Lambda, etc. + +### Layer + +Because the runtime code is relatively simple and needs to ensure stability and cannot be updated frequently, Layer is designed to extend the runtime capability and reduce the amount of local function code (some platforms limit the size of the uploaded compression package). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_post_difference.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_post_difference.md new file mode 100644 index 000000000000..f50b602313a6 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_post_difference.md @@ -0,0 +1,247 @@ +# Serverless trigger POST case differences + +## alibaba cloud API gateway + +Alibaba Cloud API Gateway supports different types of POST requests. + +### POST of entering and passing through + +The gateway configuration is as follows. + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593175823751-f9b305fc-ddeb-4b04-ba13-481a616be260.png) + +The event feature of gateway pass-through has a `body` field and the `isBase64Encoded` is true. It is easy to decode and directly solve base64. + +:::info +After passing through, all the results are handed over to the function for processing. +::: + +#### Example 1 (text/html) + +The following event is the simplest pass-through example. Because the `content-type` is `text/html`, the base64 decoding result passed by the body is also a string. + +```json +{ + "body": "eyJjIjoiYiJ9 ", + "headers": { + "x-ca-dashboard-action": "DEBUG ", + "x-ca-dashboard-uid": "125087", + "x-ca-stage": "RELEASE ", + "x-ca-dashboard-role": "USER ", + "user-agent": "Apache-HttpClient/4.5.6 (Java/1.8.0_172) ", + "accept-encoding": "gzip,deflate ", + "content-md5": "Kry+hjKjc2lvIrwoJqdY9Q== ", + "content-type": "text/html; charset=utf-8" + }, + "httpMethod": "POST ", + "isBase64Encoded": true + "path": "/api/321 ", + "pathParameters": { + "userId": "321" + }, + "queryParameters": {} +} +``` + +function result. + +```typescript +ctx.request.body; // '{"c":" B "}' => string +``` + +#### Example 2 (application/json) + +If the `content-type` is `application/json`, the framework is considered to be JSON and will be automatically used by JSON.parse. + +```json +{ + "body": "eyJjIjoiYiJ9 ", + "headers": { + "X-Ca-Dashboard-Action": "DEBUG ", + "X-Ca-Dashboard-Uid": "125087", + "X-Ca-Stage": "RELEASE ", + "X-Ca-Dashboard-Role": "USER ", + "User-Agent": "Apache-HttpClient/4.5.6 (Java/1.8.0_172) ", + "Accept-Encoding": "gzip,deflate ", + "Content-MD5": "Kry+hjKjc2lvIrwoJqdY9Q== ", + "Content-Type": "application/json; charset=utf-8" + }, + "httpMethod": "POST ", + "isBase64Encoded": true + "path": "/api/321 ", + "pathParameters": { + "userId": "321" + }, + "queryParameters": {} +} +``` + +function result. + +```typescript +ctx.request.body; // {"c":" B "} => object +``` + +#### Example 3 (application/x-www-form-urlencoded) + +If the `content-type` is set to `application/x-www-form-urlencoded`, the gateway will not pass through in base64 format. This is also the default submission type for front-end native forms. + +:::info +In the API gateway side test, keeping the "in-reference pass", it seems to have no effect, so I switched to the Postman to test. +::: + +The Postman simulation request is as follows: + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593188653464-2a5659de-40ad-4611-ba86-f5754c7d4425.png) + +The event value obtained by the function is as follows. + +```json +{ + "body": "{\"c\":\" B \"}", + "headers": { + "accept": "*/*", + "cache-control": "no-cache", + "user-agent": "PostmanRuntime/7.24.1", + "postman-token": "feb51b11-9103-463a-92ff-73076d37b683", + "accept-encoding": "gzip, deflate, br", + "content-type": "application/x-www-form-urlencoded" + }, + "httpMethod": "POST", + "isBase64Encoded": false + "path": "/api/321 ", + "pathParameters": { + "userId": "321" + }, + "queryParameters": {} +} +``` + +function result. + +```typescript +ctx.request.body; // {"c":" B "} => object +``` + +### POST of input parameter mapping + +After the gateway configuration selects input parameter mapping, there are two types of body data. + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593186831907-7975c65c-aee5-4f96-9ae4-ffaeee66c7dd.png) + +Once the mapping is selected, there is **no content-type** in the Headers the entire function gets. + +At this time, the return event of the gateway is + +```json +{ + "body": "eyJjIjoiYiJ9 ", + "headers": { + "X-Ca-Dashboard-Action": "DEBUG ", + "X-Ca-Dashboard-Uid": "111111", + "X-Ca-Dashboard-Role": "USER" + }, + "httpMethod": "POST ", + "isBase64Encoded": true + "path": "/api/321 ", + "pathParameters": { + "userId": "321" + }, + "queryParameters": {} +} +``` + +Because the function does not get the header by default, it only processes the base64 result, and the result is a string. + +```typescript +ctx.request.body; // '{"c":" B "}' => string +``` + +## Alibaba Cloud HTTP Trigger + +The HTTP trigger provided by the function (different from the gateway). + +### Ordinary POST(application/json) + +The verification code is as follows. + +```typescript +const body = this.ctx.request.body; +return { + type: typeof body + body +}; +``` + +String format. + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593321679770-a7609684-ec5e-4f93-99f2-d346ed79c1fa.png) + +```typescript +ctx.request.body; // "bbb" => string +``` + +JSON format + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593321730423-f9b2860f-7902-4f3a-81cf-bfbcfd4ee57f.png) + +```typescript +ctx.request.body; // {" B":"c"} => object +``` + +### Form (application/x-www-form-urlencoded) + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593321823455-23ec3970-35a5-4746-8995-d9146eaa4ab0.png) + +```typescript +ctx.request.body; // {" B":"c"} => object +``` + +### File upload (Binary) + +Not yet supported + +## Tencent Cloud Gateway + +Tencent Cloud provides a separate gateway. + +### Ordinary POST(application/json) + +The verification code is as follows. + +```typescript +const body = this.ctx.request.body; +return { + type: typeof body + body +}; +``` + +Use Postman requests. + +string format, normal parsing. + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593323223487-c4e5f365-b500-4a2d-85e3-45bd4aba4653.png) + +```typescript +ctx.request.body; // "bbb" => string +``` + +JSON format, can be parsed normally. + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593323187488-e7b4e32e-4195-404d-b309-ba436c3f5f8e.png) + + +```typescript +ctx.request.body; // {"c":" B "} => object +``` + +### Form (application/x-www-form-urlencoded) + +Normal parses to JSON. + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593323279728-983fd844-f37d-419b-90f3-f96d1ee8236d.png) + +```typescript +ctx.request.body; // {"c":" B "} => object +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_testing.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_testing.md new file mode 100644 index 000000000000..39f4da310276 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_testing.md @@ -0,0 +1,62 @@ +# Test function + +## Functions of HTTP classes + +This method is applicable to all functions of HTTP-like triggers, including `HTTP` and `API_GATEWAY`. + +Use the same test method as the application to test. For HTTP functions, use the supertest encapsulated `createHttpRequest` method to create HTTP clients. + +The only difference from the application is that the `createFunctionApp` method is used to create a function application (app). + +`createFunctionApp` is a customized method of `createApp` in the function scenario. + +The HTTP test code is as follows: + +```typescript +import { createFunctionApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework, Application } from '@midwayjs/faas'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from api gateway trigger', async () => { + + const app: Application = await createFunctionApp(); + + const result = await createHttpRequest(app).get('/').query({ + name: 'zhangting', + }); + expect(result.text).toEqual('hello zhangting'); + + await close(app); + + }); +}); +``` + +## Ordinary trigger + +In addition to HTTP-like triggers, we also have other function triggers such as timers and object storage. Since these triggers are closely related to the gateway, they cannot be tested using HTTP behavior. Instead, they use traditional method calls. + +Create a function app through the `createFunctionApp` method, obtain the class instance through the `getServerlessInstance` method, and then call it directly through the instance method and pass in the parameters for testing. + +```typescript +import { createFunctionApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework, Application } from '@midwayjs/faas'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from event trigger', async () => { + //Create function app + let app: Application = await createFunctionApp(); + + // Get the service class + const instance = await app.getServerlessInstance(HelloAliyunService); + + // Call the function method and pass in parameters + expect(await instance.handleEvent('hello world')).toEqual('hello world'); + + await close(app); + }); +}); +``` + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_v2_upgrade_serverless_v3.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_v2_upgrade_serverless_v3.md new file mode 100644 index 000000000000..fc5ec2fff72f --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/serverless/serverless_v2_upgrade_serverless_v3.md @@ -0,0 +1,139 @@ +# Migrate from Serverless v2 to v3 + +Based on the upgrade of Midway to v3, the Serverless system has also been upgraded to the v3 version simultaneously. + +This article describes how to migrate from Serverless v2.0 to Serverless v3.0, which is very similar to traditional application upgrades. + +:::caution + +The new Serverless currently only supports Alibaba Cloud functions. + +::: + + + +## 1. Upgrade of project package version + +Some dependency package upgrades include: + +* Midway and component versions upgraded to 3.x +* CLI, Jest and other version upgrades +* Removed some no longer used dependencies, such as `@midwayjs/serverless-app` + +```diff +"scripts": { + "dev": "cross-env NODE_ENV=local midway-bin dev --ts", + "test": "cross-env midway-bin test --ts", +- "deploy": "cross-env UDEV_NODE_ENV=production midway-bin deploy", + "lint": "mwts check", + "lint:fix": "mwts fix" +}, +"dependencies": { +- "@midwayjs/core": "^2.3.0", +- "@midwayjs/decorator": "^2.3.0", +- "@midwayjs/faas": "^2.0.0" ++ "@midwayjs/core": "^3.12.0", ++ "@midwayjs/faas": "^3.12.0", ++ "@midwayjs/fc-starter": "^3.12.0", ++ "@midwayjs/logger": "^2.0.0" +}, +"devDependencies": { +- "@midwayjs/cli": "^1.2.45", +- "@midwayjs/cli-plugin-faas": "^1.2.45", +- "@midwayjs/fcli-plugin-fc": "^1.2.45", +- "@midwayjs/mock": "^2.8.7", +- "@midwayjs/serverless-app": "^2.8.7", +- "@midwayjs/serverless-fc-trigger": "^2.10.3", +- "@midwayjs/serverless-fc-starter": "^2.10.3", +- "@types/jest": "^26.0.10", +- "@types/node": "14", +- "cross-env": "^6.0.0", +- "jest": "^26.4.0", +- "mwts": "^1.0.5", +- "ts-jest": "^26.2.0", +- "typescript": "~4.6.0" ++ "@midwayjs/mock": "^3.12.0", ++ "@types/jest": "29", ++ "@types/node": "16", ++ "cross-env": "^7.0.3", ++ "jest": "29", ++ "mwts": "^1.3.0", ++ "ts-jest": "29", ++ "ts-node": "^10.9.1", ++ "typescript": "~5.1.0" +} +``` + + + +## 2. Changes to the main entrance frame + +Explicitly declare faas as the main framework. + +```typescript +// src/configuration +import * as faas from '@midwayjs/faas'; + +@Configuration({ + // ... + imports: [ + faas + ], +}) +export class MainConfiguration { + // ... +} + +``` + + + +## 3. Test code changes + +Removed dependency on `@midwayjs/serverless-app`. + +```diff +import { createFunctionApp, close, createHttpRequest } from '@midwayjs/mock'; +- import { Framework, Application } from '@midwayjs/serverless-app'; ++ import { Framework, Application } from '@midwayjs/faas'; +``` + +Removed `@midwayjs/serverless-fc-trigger` and `@midwayjs/serverless-fc-starter` dependencies and changed to `@midwayjs/fc-starter`. + +```typescript +import { Application, Context, Framework } from '@midwayjs/faas'; +import { mockContext } from '@midwayjs/fc-starter'; +import { createFunctionApp } from '@midwayjs/mock'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from event trigger', async () => { + + // create app + const app: Application = await createFunctionApp(join(__dirname, '../'), { + initContext: Object.assign(mockContext(), { + function: { + name: '***', + handler: '***' + } + }), + }); + + // ... + + await close(app); + }); +}); +``` + +Some API replacements, such as the original `createXXXEvent`, will become `mockXXXEvent`, and the original `createInitializeContext` will become the `mockContext` method. + +These APIs will be exported directly from `@midwayjs/fc-starter`. + + + +## 5. Changes in deployment methods + +Instead of using `midway-bin deploy` for deployment, the platform's own CLI tool will be used. Midway only provides framework and local development capabilities. + +For more deployment adjustments, please check [Pure Function Deployment](/docs/serverless/aliyun_faas). \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/service.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/service.md new file mode 100644 index 000000000000..30909f18da96 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/service.md @@ -0,0 +1,173 @@ +# Service and Injection + +In business, only the code of the controller (Controller) is not enough. Generally speaking, some business logic is abstracted into a specific logical unit, which we generally call the service (Service). + + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01LLV2Qd20Fbu1NWXVA_!!6000000006820-2-tps-2130-344.png) + + +Providing this abstraction has the following benefits: + +- Keep the logic in the Controller more concise. +- To maintain the independence of business logic, abstract Service can be repeatedly called by multiple Controller. +- Separating logic from presentation makes it easier to write test cases. + + + +## Create service + + +In Midway, the common service is a Class. For example, we created a Controller to accept user requests before. We will add a service to process the data. + + +For service files, we usually store them in the `src/service` directory. Let's add a user service. + +```text +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ ├── user.ts +│ │ └── home.ts +│ ├── interface.ts +│ └── service +│ └── user.ts +├── test +├── package.json +└── tsconfig.json +``` + +The content is: + +```typescript +// src/service/user.ts +import { Provide } from '@midwayjs/core'; + +@Provide() +export class UserService { + + async getUser(id: number) { + return { + id, + name: 'Harry', + age: 18, + }; + } +} +``` +Except for an `@Provide` decorator, the structure of the entire service is exactly the same as the ordinary Class, so that's all. + + +We also added a User definition before, which can also be used directly here. + +```typescript +import { Provide } from '@midwayjs/core'; +import { User } from '../interface'; + +@Provide() +export class UserService { + + async getUser(id: number): Promise { + return { + id, + name: 'Harry', + age: 18', + }; + } +} +``` + + +## Use service + + +At Controller, we need to call this service. In traditional code writing, we need to initialize this Class(new) and then place the instance where it needs to be called. In Midway, you **don't need to do** this, you just need to write the **"dependency injection"** code we provide. + + +```typescript +import { Inject, Controller, Get, Provide, Query } from '@midwayjs/core'; +import { UserService } from '../service/user'; + +@Controller('/api/user') +export class APIController { + + @Inject() + userService: UserService; + + @Get('/') + async getUser(@Query('id') uid) { + const user = await this.userService.getUser(uid); + return {success: true, message: 'OK', data: user}; + } +} + +``` + +The process of using the service is divided into several parts: + + +- 1. Use the `@Provide` decorator to expose your service +- 2. In the code that you call, use the `@Inject` decorator to inject your service. +- 3, call the injection service, execute the corresponding method. + + +Midway's core "dependency injection" container will **automatically associate** your controller (Controller) and service (Service), and all code **will be automatically initialized** during operation. you **do not need to manually initialize** these classes. + + +## Injection behavior description + +Seeing here, you will have some doubts as to why there is an `@Provide` decorator on the service (Service), but not on the controller (Controller). + +In fact, the controller (Controller) also has this decorator, but in the new version, Controller includes Provide functions. If you are not sure when you can hide it, you can write it all down. + +If you don't write, the default is equivalent to the following code. + +```ts +@Provide() +@Controller('/api/user') +export class APIController { +``` + +`@Provide` the role of decorator: + + +- 1. This Class, hosted by the dependent injection container, will be automatically instantiated (new) +- 2. This Class can be injected by other Class in the container. + + +The corresponding `@Inject` decorator is used: + + +- 1. In the dependency injection container, find the corresponding attribute name and assign it to the corresponding instantiated object + + + +:::info +In the class of `@Inject`, the corresponding `@Provide` must be valid. +::: + + +`@Provide` and `@Inject` decorators appear in pairs, and the two are associated by the class name after the colon. +```typescript +// service +@Provide() +export class UserService { + //... +} + +// controller +@Provide() // <------ Because there are Controller that include Provide capabilities, the display here is more complete +@Controller('/api/user') +export class APIController { + + @Inject() + userService: UserService; // <------ The type here is Class, that is, an instance of this type will be injected + + //... +} + +``` +This combination will be used in many places. **Please remember this usage**. + + +Dependency injection is more complex. For more information, see [Dependency injection](container). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/service_factory.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/service_factory.md new file mode 100644 index 000000000000..d8e4f7b27b5d --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/service_factory.md @@ -0,0 +1,532 @@ +# Service factory + +Sometimes when writing components or services, you will encounter the situation that a service has multiple instances. At this time, the service factory (Service Factory) is suitable for this scenario. + +for example, our oss component creates multiple oss objects, so you need to leave many instance interfaces when writing. For this scenario, midway abstracted `ServiceFactory` class. + + +`ServiceFactory` is an abstract class, and every service that needs to be implemented needs to be inherited. + + +Take an http client as an example, we need to prepare a method to create an http client instance, which contains several parts: + + +- 1. Method for creating a client instance +- 2. Configuration of Client +- 3. Instantiate service class + +```typescript +// Create client configuration +const config = { + baseUrl: '', + timeout: 1000 +}; + +// Method for creating a client instance +const httpClient = new HTTPClient(config); +``` + + +## Implement a service class + + +We hope to implement a service factory of the above HTTPClient to create multiple httpClient objects in midway system. + + +The service factory is also a common export class in midway. As a member of the service, for example, we can also put it in `src/service/httpServiceFactory.ts`. + + +### 1. Implement the interface to create an instance + +`ServiceFactory` is an abstract class for inheritance, which contains a generic type (the instance type created, for example, the following is the HTTPClient type created). + + +We only need to inherit it, and at the same time, the general service factory is a single case. +```typescript +import { ServiceFactory, Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class HTTPClientServiceFactory extends ServiceFactory { + // ... +} +``` +Since it is an abstract class, we need to implement two of these methods. +```typescript +import { ServiceFactory, Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class HTTPClientServiceFactory extends ServiceFactory { + + // Create a single instance + protected createClient(config: any): any { + return new HTTPClient(config); + } + + getName() { + return 'httpClient'; + } +} +``` + +`createClient` method is used to pass in a create service configuration (such as httpClient configuration) and return a specific instance, as in the example. + + +`getName` method is used to return the name of this service factory to facilitate frame identification and log output. + + +### 2. Add configuration and initialization methods + + +We need to inject a configuration, for example, we use `httpClient` as the configuration of this service. +```typescript +// config.default.ts +export const httpClient = { + // ... +} +``` +Then inject it into the service factory. At the same time, we also need to call the method of creating multiple instances during initialization. +```typescript +import { ServiceFactory, Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class HTTPClientServiceFactory extends ServiceFactory { + + @Config('httpClient') + httpClientConfig; + + @Init() + async init() { + await this.initClients(this.httpClientConfig); + } + + protected createClient(config: any): any { + // Create an instance + return new HTTPClient(config); + } + + getName() { + return 'httpClient'; + } +} +``` +`initClients` method is implemented in the base class. It needs to pass a complete user configuration and call the `createClient` in a loop to create the object and save it to memory. + + +### 3. Instantiate service class + +To make it easier for users to use, we also need to create the service class in advance. Generally speaking, we only need to instantiate it in the lifecycle of components or projects. + +```typescript +import { Configuration } from '@midwayjs/core'; + +@Configuration({ + imports: [ + // ... + ] +}) +export class ContainerConfiguration { + async onReady(container) { + // Instantiate service class + await container.getAsync(HTTPClientServiceFactory); + } +} +``` + + +## Get instance + + +`createClient` method only defines the method of creating objects, and we also need to define the structure of the configuration. + +The structure of the configuration is divided into several parts: + +- 1. The default configuration, that is, the configuration in which all objects can be reused +- 2. Configuration required by a single instance +- 3. Configuration required by multiple instances + + + +Let's explain separately, + +**Default Configuration** + + +The default configuration, we agreed to `default` the attribute. +```typescript +// config.default.ts +export const httpClient = { + default: { + timeout: 3000 + } +} +``` + + +### Single instance + + +**Single Configuration** +```typescript +// config.default.ts +export const httpClient = { + default: { + timeout: 3000 + }, + client: { + baseUrl: '' + } +} +``` +`client` is used to describe the structure of a single instance. The object is merged with the `default` when it is created. Use the `get` method to obtain the default instance. +```typescript +import { HTTPClientServiceFactory } from './service/httpClientServiceFactory'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + serviceFactory: HTTPClientServiceFactory; + + async invoke() { + const httpClient = this.serviceFactory.get(); + } +} + +``` + + +### Multiple instances + + +Use `clients` to configure multiple instances. Each key is an independent instance configuration. +```typescript +// config.default.ts +export const httpClient = { + default: { + timeout: 3000 + }, + clients: { + aaa: { + baseUrl: '' + }, + bbb: { + baseUrl: '' + } + } +} +``` +use the key to obtain the instance. +```typescript +import { HTTPClientServiceFactory } from './service/httpClientServiceFactory'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + serviceFactory: HTTPClientServiceFactory; + + async invoke() { + + const aaaInstance = this.serviceFactory.get('aaa'); + // ... + + const bbbInstance = this.serviceFactory.get('bbb'); + // ... + + } +} +``` + + + +### Decorator gets instance + +Starting from v3.9.0, ServiceFactory has added an `@InjectClient` decorator to facilitate the selection of injection when multiple clients are involved. + +```typescript +import { HTTPClientServiceFactory } from './service/httpClientServiceFactory'; +import { join } from 'path'; +import { InjectClient } from '@midwayjs/core'; + +@Provide() +export class UserService { + + @InjectClient(HTTPClientServiceFactory, 'aaa') + aaaInstance: HTTPClientServiceFactory; + + @InjectClient(HTTPClientServiceFactory, 'bbb') + bbbInstance: HTTPClientServiceFactory; + + async invoke() { + // this.aaaInstance.xxx +// this.bbbInstance.xxx + //... + } +} +``` + +The `@InjectClient` decorator is used to quickly inject multiple instances of `ServiceFactory` derived implementations, and all classes that extend `ServiceFactory` can be used. + +The decorator takes two parameters, defined as follows: + +```typescript +export function InjectClient( + serviceFactoryClz: new (...args) => IServiceFactory, + clientName?: string +) { + //... +} +``` + +| Parameters | Description | +| ----------------- | ------------------------------------------------------------ | +| serviceFactoryClz | Required, the derived class of `ServiceFactory`, from which the decorator will get the lookup instance. | +| clientName | Optional, if not filled, the default instance name `defaultClientName` configuration item in the configuration will be searched by default. | + + + +### Dynamically create an instance + + +Instances can also be obtained dynamically through `createInstance` methods of the base class. + + +:::caution +Note that the createClient used here is not subclass, createClient does not contain and default configuration logic. +::: + + +```typescript +import { HTTPClientServiceFactory } from './service/httpClientServiceFactory'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + serviceFactory: HTTPClientServiceFactory; + + async invoke() { + + // config.bucket3 and config.default will be merged + let customHttpClient = await this.serviceFactory.createInstance({ + baseUrl: 'xxxxx' + }, 'custom'); + + // After passing the name, you can also get it from the factory. + customHttpClient = this.serviceFactory.get('custom'); + + } +} +``` +The first parameter of the `createInstance` method is configuration. If you call dynamically, you can manually pass the parameter. The second parameter is a string name. If the name is passed in, the created instance will be saved in memory and can be obtained from the service factory again later. + + + +## Instance configuration merge logic + +When the actual code is running, even if it is a single instance, configuring a `client` will transform the configuration into `clients` in memory. + +For example the following code: + +```typescript +// config.default.ts +export const httpClient = { + client: { + baseUrl: '' + } +} +``` + +in memory becomes: + +```typescript +// config.default.ts +export const httpClient = { + clients: { + default: { + baseUrl: '' + } + } +} +``` + +There will be an extra default instance called `default`, and the service factory will be initialized with the configuration of `clients`. + + + +## Default instance proxy (optional) + +It will be very cumbersome if the user needs to obtain it through `serviceFactory` every time they use it. For the most commonly used default instance, a proxy class can be provided to make it proxy all the target instance methods. + +```typescript +import { + ServiceFactory, + MidwayCommonError, + delegateTargetAllPrototypeMethod, + Provide, + Scope, + ScopeEnum, + Init +} from '@midwayjs/core'; + +//... +export class HTTPClientServiceFactory extends ServiceFactory { + //... +} + +// The following is the default proxy class +@Provide() +@Scope(ScopeEnum. Singleton) +export class HTTPClientService implements HTTPClient { + @Inject() + private serviceFactory: HTTPClientServiceFactory; + + // This property is used to hold the actual instance + private instance: HTTPClient; + + @Init() + async init() { + // In the initialization phase, get the default instance from the factory + this.instance = this.serviceFactory.get( + this.serviceFactory.getDefaultClientName() || 'default' + ); + if (!this. instance) { + throw new MidwayCommonError('http client default instance not found.'); + } + } +} + +// In the code below, the ts definition for the default instance class is correctly inherited + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface HTTPClientService extends HTTPClient { + //empty +} + +// The following code, for the implementation of the default instance class can be proxied +delegateTargetAllPrototypeMethod(HTTPClientService, HTTPClient); + +``` + +With the above code, we can use `HTTPClientService` directly without getting the default instance from `HTTPClientServiceFactory`. + +`delegateTargetAllPrototypeMethod` is a utility method provided by Midway to delegate instance methods. + +In addition, there are some other available tool methods, listed below: + +- `delegateTargetAllPrototypeMethod` is used to delegate all prototype methods of the target, including the prototype chain, excluding constructors and internal hidden methods +- `delegateTargetPrototypeMethod` is used to delegate all prototype methods of the target, excluding constructors and inner hidden methods +- `delegateTargetMethod` specifies the method on the proxy target + + + +## Modify the default instance name + +By default, the default instance name is `default`, and the default instance proxy will be proxied internally based on this instance. + +If the user does not configure the `default` instance, or wants to modify the default instance, the user can modify it through configuration. + +```typescript +// config.default.ts +export const httpClient = { + clients: { + default: { + baseUrl: '' + }, + default2: { + baseUrl: '' + } + }, + defaultClientName: 'default2', +} +``` + +In the default instance proxy, this value will be obtained through `this.serviceFactory.getDefaultClientName()`. + +```typescript +import { HTTPClientService } from './service/httpClientServiceFactory'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + httpClientService: HTTPClientService; + + async invoke() { + // this.httpClientService points to default2 + } +} +``` + + + +## Instance priority + +Starting from v3.14.0, service factory instances can add a priority attribute. In different scenarios, some different processing will be done based on the priority. + +There are three levels of priority for instances: `L1`, `L2`, and `L3`, which correspond to high, medium, and low levels respectively. + +The definition is as follows: + +```typescript +export const DEFAULT_PRIORITY = { + L1: 'High', + L2: 'Medium', + L3: 'Low', +}; +``` + +Through configuration, we can specify the priority of different instances. + +```typescript +//config.default.ts +import { DEFAULT_PRIORITY } from '@midwayjs/core'; + +export default { + httpClient: { + clients: { + default: { + baseUrl: '' + }, + default2: { + baseUrl: '' + } + }, + clientPriority: { + default: DEFAULT_PRIORITY.L1, + default2: DEFAULT_PRIORITY.L2, + } + } +} +``` + +If no setting is made, the default priority is medium, that is, `DEFAULT_PRIORITY.L2`. + +In order to better judge the priority, some methods will be added to the `ServiceFactory` base class. + +```typescript +@Provide() +@Scope(ScopeEnum.Singleton) +export class HTTPClientService implements HTTPClient { + @Inject() + private serviceFactory: HTTPClientServiceFactory; + + @Init() + async init() { + // Get priority + this.serviceFactory.getClientPriority('default'); // DEFAULT_PRIORITY.L2 + + // Determine priority + this.serviceFactory.isHighPriority('default'); + this.serviceFactory.isMediumPriority('default'); + this.serviceFactory.isLowPriority('default'); + } +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/testing.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/testing.md new file mode 100644 index 000000000000..97bf4e61ab98 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/testing.md @@ -0,0 +1,744 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Test + +In application development, testing is very important. In the period of rapid iteration of traditional Web products, each test case provides a guarantee for the stability of the application. API upgrade, test cases can well check whether the code is backwards compatible. For all possible inputs, once the test covers, its output can be clarified. After the code changes, you can judge whether the code changes affect the determined results through the test results. + + +Therefore, the Controller, Service and other codes of the application must have corresponding unit tests to ensure the code quality. Of course, each functional change and refactoring of the framework and components requires corresponding unit tests, and the modified code is required to be covered by the 100% as much as possible. + +The current testing libraries in the community are mainly `jest` and `mocha`. This article uses `jest` as an example. + +## Test directory structure + + +We agree that the `test` directory is the directory where all test scripts are stored, and the `fixtures` used for testing and related auxiliary scripts should be placed in this directory. + + +The test script file is named `${filename}.test.ts` and must be suffixed with `.test.ts`. + + +An example of an application's test directory: +```text +➜ my_midway_app tree +. +├── src +├── test +│ └── controller +│ └── home.controller.test.ts +├── package.json +└── tsconfig.json +``` + + + +## Test Run Tool + + +By default, Midway provides the `midway-bin` command to run the test script. In the new version, Midway replaces mocha with Jest by default. It is more powerful and more integrated, which allows us to **focus on writing test code** instead of choosing those test peripheral tools and modules. + + +You only need to configure the `scripts.test` on the `package.json`. + + + + + + +```json +{ + "scripts": { + "test": "jest" + } +} +``` + +Then you can run the test according to the standard `npm test`. In the default scaffolding, we have provided this command, so you can run the test out of the box. + +```bash +➜ my_midway_app npm run test + +> my_midway_project@1.0.0 test /Users/harry/project/application/my_midway_app +>jest + +Testing all *.test.ts... + PASS test/controller/home.controller.test.ts + PASS test/controller/api.controller.test.ts + +Test Suites: 2 passed, 2 total +Tests: 2 passed, 2 total +Snapshots: 0 total +Time: 3.26 seconds +Ran all test suites matching /\/test\/[^.]*\.test\.ts$/i. +``` + + + + + + +```json +{ + "scripts": { + "test": "midway-bin test --ts" + } +} +``` + + +Then you can run the test according to the standard `npm test`. By default, we have already provided this command in the scaffold, so you can run the test out of the box. + +```bash +➜ my_midway_app npm run test + +> my_midway_project@1.0.0 test /Users/harry/project/application/my_midway_app +> midway-bin test + +Testing all *.test.ts... + PASS test/controller/home.controller.test.ts + PASS test/controller/api.controller.test.ts + +Test Suites: 2 passed, 2 total +Tests: 2 passed, 2 total +Snapshots: 0 total +Time: 3.26 s +Ran all test suites matching /\/test\/[^.]*\.test\.ts$/i. +``` + + + + + + +## Assertion library + + +Jest has a powerful `expect` assertion library, which can be directly used in the global. + + +For example, commonly used. + + +```typescript +Expect (result.status).toBe(200); // Whether the value is equal to a certain value, the reference is equal +expect(result.status).not.toBe(200); +Expect (result).toEqual('hello'); //Simple match, the same object attribute is also true +Expect (result).toStrictEqual('hello'); // Strictly match +Expect (['lime', 'apple']).toContain('lime'); //Judge whether it is in an array +``` + + +For more information about assertion methods, see [https:// jestjs.io/docs/en/expect](https://jestjs.io/docs/en/expect). + + + +## Create test + + +Different upper-level frameworks have different testing methods. Take the most commonly used HTTP service as an example. If you need to test an HTTP service, generally speaking, we need to create an HTTP service and then request it with the client. + + +Midway provides a basic set of `@midwayjs/mock` tools to help the upper framework test in this area. At the same time, it also provides convenient methods to create Framework,App and close. + + +The whole process approach is divided into several parts: + + +- `createApp` to create an app object for a Framework +- `close` closes a Framework or an app + +In order to keep the test simple, the whole process currently reveals these two methods. +```typescript +// create app +const app = await createApp(); +``` +The `Framework` passed in here is used to derive the type for the TypeScript. In this way, the main frame app instance can be returned. + + +After the app is run, you can use the `close` method to close the app. +```typescript +import { createApp, close } from '@midwayjs/mock'; + +await close(app); +``` +In fact, `@midwayjs/bootstrap` is encapsulated in `createApp` method, and interested partners can read the source code. + + + +## Test HTTP service + + +In addition to creating app, `@midwayjs/mock` also provides a simple client method for quickly creating test behaviors corresponding to various services. + + +For example, for HTTP, we encapsulate supertest and provide `createHttpRequest` methods to create HTTP clients. + + +```typescript +// Create a client request +const result = await createHttpRequest(app).get('/'); +// Test returns results +expect(result.text).toBe('Hello Midwayjs!'); +``` + + +It is recommended to reuse the app instance in a test file. The complete test example is as follows. +```typescript +import { createApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework, Application } from '@midwayjs/koa'; +import * as assert from 'assert'; + +describe('test/controller/home.test.ts', () => { + + let app: Application; + + beforeAll(async () => { + // Create app only once and can be reused. + app = await createApp(); + }); + + afterAll(async () => { + // close app + await close(app); + }); + + it('should GET /', async () => { + // make request + const result = await createHttpRequest(app) + .get('/') + .set('x-timeout', '5000'); + + // use expect by jest + expect(result.status).toBe(200); + expect(result.text).toBe('Hello Midwayjs!'); + + // or use assert + assert.deepStrictEqual(result.status, 200); + assert.deepStrictEqual(result.text, 'Hello Midwayjs!'); + }); + + it('should POST /', async () => { + // make request + const result = await createHttpRequest(app) + .post('/') + .send({id: '1'}); + + // use expect by jest + expect(result.status).toBe(200); + }); + +}); + +``` + + +**Example:** + + +Create a get request and pass the query parameter. +```typescript +const result = await createHttpRequest(app) + .get('/set_header') + .query({ name: 'harry' }); +``` + + +create a post request and pass the body parameter. +```typescript +const result = await createHttpRequest(app) + .post('/user/catchThrowWithValidate') + .send({id: '1'}); +``` + + +create a post request and pass the form body parameter. +```typescript +const result = await createHttpRequest(app) + .post('/param/body') + .type('form') + .send({id: '1'}) +``` + + +Pass the header header. +```typescript +const result = await createHttpRequest(app) + .get('/set_header') + .set({ + 'x-bbb ': ' 123' + }) + .query({ name: 'harry' }); +``` +Pass cookie. +```typescript +const cookie = [ + "koa.sess=eyJuYW1lIjoiaGFycnkiLCJfZXhwaXJlIjoxNjE0MTQ5OTQ5NDcyLCJfbWF4QWdlIjo4NjQwMDAwMH0=; path=/; expires=Wed, 24 Feb 2021 06:59:09 GMT; httponly ", + "koa.sess.sig=mMRQWascH-If2-BC7v8xfRbmiNo; path=/; expires=Wed, 24 Feb 2021 06:59:09 GMT; httponly" +] + +const result = await createHttpRequest(app) + .get('/set_header') + .set('Cookie', cookie) + .query({ name: 'harry' }); +``` + + +## Test service + + +Outside the controller, sometimes we need to test a single service, which we can get from the dependency injection container. + +Assume that a test `UserService` is required. + + +```typescript +// src/service/user.ts +import { Provide } from '@midwayjs/core'; + +@Provide() +export class UserService { + async getUser() { + // xxx + } +} +``` + +Then write this in the test code. + +```typescript +import { createApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework } from '@midwayjs/web'; +import * as assert from 'assert'; +import { UserService } from '../../src/service/user'; + +describe('test/controller/home.test.ts', () => { + + it('should GET /', async () => { + // create app + const app = await createApp(); + + // Obtain instances based on dependency injection class (recommended) + const userService = await app.getApplicationContext().getAsync(UserService); + // Get the instance based on the dependency injection Id. + const userService = await app.getApplicationContext().getAsync('userService'); + // Incoming class Ignoring Generics can also correctly derive + const userService = await app.getApplicationContext().getAsync(UserService); + + // close app + await close(app); + }); + +}); +``` + + +If your service is associated with a request (ctx), you can use the request scope to get the service. + + +```typescript +import { createApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework } from '@midwayjs/web'; +import * as assert from 'assert'; +import { UserService } from '../../src/service/user'; + +describe('test/controller/home.test.ts', () => { + + it('should GET /', async () => { + // create app + const app = await createApp(); + + // Get the instance based on the dependency injection Id. + const userService = await app.createAnonymousContext() + .requestContext.getAsync('userService'); + + // You can also pass in class to get an instance. + const userService = await app.createAnonymousContext() + .requestContext.getAsync(UserService); + + // close app + await close(app); + }); + +}); +``` + + + +## createApp option parameters + +`createApp` method is used to create an app instance of a framework, and by passing in a generic framework type, the app we infer can be the app returned by the framework. + + +For example: + +```typescript +import { Framework } from '@midwayjs/grpc'; + +// The app here can ensure that it is the app returned by the gRPC framework. +const app = await createApp(); +``` +`createApp` method actually has parameters, and its method signature is as follows. +```typescript +async createApp ( + appDir = process.cwd() + options: IConfigurationOptions = {} +) +``` +The first parameter is the absolute root directory path of the project, which defaults to `process.cwd()`. +The second parameter is the Bootstrap startup parameter, such as the configuration of some global behaviors. for details, see TS definition. + + + +## Close option parameter + + +The `close` method is used to close the framework related to the app instance. + +```typescript +await close(app); +``` + +It has some parameters. + +```typescript +export declare function close ( + app: IMidwayApplication | IMidwayFramework + options ?: { + cleanLogsDir?: boolean; + cleanTempDir?: boolean; + sleep?: number; +}): Promise; +``` +The first parameter is an instance of app or framework. + + +The second parameter is an object that can perform some behaviors when executing shutdown: + + +- 1. `cleanLogsDir` defaults to false, and deletes the logs directory after the control test is completed (except windows) +- 2. The default `cleanTempDir` is false, and some temporary directories (such as run directory generated by egg) are cleaned up. +- 3. The default value of `sleep` is 50, in milliseconds, and the delay time after the app is turned off (to prevent the logs from being written without success). + +## Test with bootstrap files + +In general, you don't need to use `bootstrap.js` to test. If you want to test directly using the `bootstrap.js` entry file, you can pass the entry file information during the test. + +Unlike dev/test startup, startup using `bootstrap.js` is a real service that runs multiple frameworks at the same time and creates app instances of multiple frameworks. + +`@midwayjs/mock` provides `createBootstrap` methods to test the startup file type. We can pass in the `bootstrap.js` of the entry file as a startup parameter, so that `createBootstrap` method starts the code through the entry file. + +```typescript +it('should GET /', async () => { + // create app + const bootstrap = await createBootstrap(join(process.cwd(), 'bootstrap.js')); + // Get an app instance based on the frame type. + const app = bootstrap.getApp('koa'); + + // expect and test + + // close bootstrap + await bootstrap.close(); +}); +``` + + + +## Run a single test + + +Unlike the `only` of mocha, the `only` method of jest takes effect only for a single file. `midway-bin` provides the ability to run a single file. + + + + + +Execute a single file. + +```bash +$ jest test/controller/api.ts +``` + +If you want to run a specific test in a file, you can use jest's `-t` or `--testNamePattern` option, followed by the name of the test you want to run. For example: + +```bash +$ jest -t "name of your test" +``` + +This will only run tests with matching names. + + + + + +`midway-bin` provides the ability to run individual files. + +```bash +$ midway-bin test -f test/controller/api.ts +``` + +In this way, you can specify to run the test of a file, and then cooperate with the `describe.only` and `it.only`, so that you can run only a single test method in a single file. + +`midway-bin test --ts` is equivalent to the following command using jest directly. + +```bash +$ node --require=ts-node/register ./node_modules/.bin/jest +``` + + + + + + +## Customize Jest file content + + +In general, the Midway tool chain has built-in jest configuration, so that users do not need to add this file. However, in some special scenarios, such as using VSCode or Idea editors, you may need to specify a `jest.config.js` scenario when you need to develop and test in the visualization area. In this case, Midway supports creating a custom jest configuration file. + + +Create a `jest.config.js` file in the root directory of the project. + +``` +➜ my_midway_app tree +. +├── src +├── test +│ └── controller +│ └── home.test.ts +├── jest.config.js +├── package.json +└── tsconfig.json +``` +The content is as follows. The configuration is the same as the standard jest. + +```javascript +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'] + coveragePathIgnorePatterns: ['/test/'] +}; +``` + + +## Common settings + + +If you need to run some code before a single test, you can add `jest.setup.js`. +```javascript +const path = require('path'); + +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'] + coveragePathIgnorePatterns: ['/test/'] + setupFilesAfterEnv: ['/jest.setup.js'], // read jest.setup.js in advance +}; +``` + +:::caution +Note that `jest.setup.js` can only use js files. +::: + + +### Example 1: The problem of long test code time + + +If the following error occurs in the test, it means that your code takes a long time to execute (such as connecting to the database, running tasks, etc.). If you are sure that there is no problem with the code, you need to extend the startup time. +``` +Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout. +``` +The default time for jest is **5000ms(5 seconds)**. You can adjust it to more. + +Can be modified at startup via `package.json`. + + + + + +```json +{ + "scripts": { + "test": "jest --testTimeout=30000" + } +} +``` + + + + + +```json +{ + "scripts": { + "test": "midway-bin test --ts --testTimeout=30000" + } +} +``` + +Here `testTimeout` is the startup parameter of jest. + + + + + + +You can write the following code in the `jest.setup.js` file to adjust the jest timeout period. + +```javascript +// jest.setup.js +jest.setTimeout(30000); +``` + + +### Example 2: Global Environment Variables + + +Similarly, `jest.setup.js` can also run custom code, such as setting global environment variables. + +```javascript +// jest.setup.js +process.env.MIDWAY_TS_MODE = 'true'; +``` +### Example 3: Processing where the program cannot exit normally + + +Sometimes, because some codes (timers, listeners, etc.) run in the background, the process cannot be exited after a single test run. For this case, jest provides the `-forceExit` parameter. + + + + + +```bash +$ jest --forceExit +$ jest --coverage --forceExit +``` + + + + + +```bash +$ midway-bin test --ts --forceExit +$ midway-bin cov --ts --forceExit +``` + +The `testTimeout` here is the startup parameter of jest. + + + + + +You can also add attributes to a custom file. + +```javascript +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'] + coveragePathIgnorePatterns: ['/test/'] + forceExit: true +}; +``` + +### Example 4: Parallel Change to Serial Execution + + +By default, jest processes each test file in parallel. If there are scenarios such as startup ports in the test code, parallel processing may cause port conflicts and report errors. At this time, you need to add the `-runInBand` parameter. Note that this parameter can only be loaded in the command. + + + + + +```bash +$ jest --runInBand +$ jest --coverage --runInBand +``` + + + + + +```bash +$ midway-bin test --ts --runInBand +$ midway-bin cov --ts --runInBand +``` + + + + + + +## Editor configuration + + +### Jetbrain Webstorm/Idea configuration + + +In the Jetbrain editor, the "jest" plug-in needs to be enabled. Since the sub-process is used to start, we still need to specify the load `-require = ts-node/register` at startup. + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01Wa6UaE1p0zU82gnpL_!!6000000005299-2-tps-1500-951.png) + + +### VSCode configuration + + +Search the plug-in first and install Jest Runner. +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01D6zTxi1GiwwrqhHVW_!!6000000000657-2-tps-1242-877.png) +Open the configuration and configure the jest command path. + + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN017BK54o1n2FL7x8hI0_!!6000000005031-2-tps-1266-849.png) + + +Enter `node -- require = ts-node/register ./node_modules/.bin/jest` at the jest command. + + +Or set settings.json in the workspace folder. vscode. + +```json +{ + "jest.pathToJest": "node --require=ts-node/register ./node_modules/.bin/jest --detectOpenHandles ", + "jestrunner.jestCommand": "node --require=ts-node/register ./node_modules/.bin/jest --detectOpenHandles" +} +``` + + +Since the debugging of the jest runner plug-in uses the debugging of VSNode, the launch.json of VSNode needs to be configured separately. + + +Set launch.json in the folder. vscode + +```json +{ + "version": "0.0.1 ", + "configurations": [ + { + "name": "Debug Jest Tests ", + "type": "node ", + "request": "launch ", + "runtimeArgs": [ + "--inspect-brk ", + "--require=ts-node/register ", + "${workspaceRoot}/node_modules/.bin/jest ", + "--runInBand ", + "--detectOpenHandles" + ], + "console": "integratedTerminal ", + "internalConsoleOptions": "neverOpen" + } + ] +} +``` + + + +## About alias paths + +The `mwtsc` tool does not support the Alias Path feature. + + +## About mock data + +Simulation data is a capability that can be used in both development and testing. For more information, see [Simulation data](./mock). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/cli.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/cli.md new file mode 100644 index 000000000000..e4e2795f2e70 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/cli.md @@ -0,0 +1,522 @@ +# Midway CLI + +:::tip + +Since the underlying capabilities of the CLI are derived from the existing module functions of the community, in order to reduce the maintenance costs and understanding costs caused by transitional packaging, each function in the CLI will gradually become the existing modules of the community, and the CLI library will cease to exist. Iterate. + +The subsequent changes for this purpose are + +* Development will change from `midway-bin dev` to `mwtsc` +* Compilation will change from `midway-bin build` to `tsc` +* Test will change from `midway-bin test` to `mocha` or `jest` +* Coverage will change from `midway-bin cov` to `jest --coverage` or other similar directives + +::: + + + +`@midwayjs/cli` is a new version of Midway system tool chain, which is integrated with Serverless and the original application tool chain. + + +## Foundation entrance + +`@midwayjs/cli` provides two entry commands. `midway-bin` and `mw` commands. + +When `@midwayjs/CLI` is installed in the global system, the `mw` command is used, such as `mw Dev`. When installing the cli tool in a project, we usually use the `midway-bin` command, but remember that the two commands are the same. + + + +## dev command + +Start the local development command with the current directory. + +```bash +$ mw dev + -- baseDir the application directory, usually the folder where package.json is located, and the default is process.cwd() + -sourceDir ts code directory, automatically analyzed by default + -p, -- port dev listens on the port, default to 7001 + -- ts TS mode running code + -- fast speed mode + -framework the specified framework, it will be analyzed automatically by default + -f, -entryFile specifies to use the entry file to start the bootstrap.js + -watchFile more files or folders to modify listening + -Does not restart automatically when notWatch code changes +``` + +### **Standard Start** + +```bash +$ midway-bin dev --ts +``` + +### **Modify the startup port** + +For HTTP scenarios, `-p` or `-- port` can temporarily modify the port. + +```bash +$ midway-bin dev --ts --port=7002 +``` + +### **Modify the startup path** + +Specify the root directory of the application, usually the folder where the package.json is located, and the default is process.cwd() + +```shell +$ midway-bin dev --ts --baseDir=./app +``` + +### **Modify the source code path of ts** + +specifies the ts code directory, which is automatically analyzed by default. + +```shell +$ midway-bin dev --ts --sourceDir=./app/src +``` + + + +### Change tsconfig position +Specify the location of tsconfig.json by setting [TS_NODE_PROJECT](https://github.com/TypeStrong/ts-node#project). + +```shell +$ cross-env TS_NODE_PROJECT=./tsconfig.dev.json midway-bin dev -ts +``` + +### **Faster startup method** + +The default startup method is ts-node, which will be slower when the number of files is particularly large. You can switch to a new compilation method such as swc. + +```shell +// Use ts-node fast dev mode +$ midway-bin dev --ts --fast + +// Use swc fast dev mode +$ midway-bin dev --ts --fast=swc +``` + +### Monitoring file changes + +`-watchFile` is used to specify more files or folders to modify listening, default listening to files ending in `.ts`, `.yml`, and `.json` in the `sourceDir` directory (you can specify more extensions through the -- watchExt parameter), and `f.yml` files in the `baseDir` directory + +```shell +// Specify multiple files, separated by commas +$ midway-bin dev --ts --watchFile=./a.txt,./b.txt + +// Specify multiple folders and files separated by commas +$ midway-bin dev --ts --watchFile=./test,./b.txt +``` + + +- `-- watchExt`: Specify more listener file extensions. Default value: `.ts`, `.yml`, and `.json`. + +```shell +// Specify multiple file extensions separated by commas +$ midway-bin dev --ts --watchExt=.js,.html +``` + + +### Local single-step Debug debugging + +The `-- debug` parameter starts the debug mode. You can use the `chrome devtools` to perform single-step code debugging: + +![69456694-513D-4388-B52F-001562D4A520.png](https://cdn.nlark.com/yuque/0/2021/png/128621/1635994136312-f1eda8ba-165d-4322-82b8-b21d3b9c6beb.png#clientId=u32db4720-b7d0-4&crop=0&crop=0&crop=1&crop=1&from=ui&height=177&id=z4u1f&margin=%5Bobject%20Object%5D&name=69456694-513D-4388-B52F-001562D4A520.png&originHeight=666&originWidth=1538&originalType=binary&ratio=1&rotation=0&showTitle=false&size=276022&status=done&style=none&taskId=ud161d835-1e96-4246-8061-c795e9a0ff1&title=&width=409) +You can use `chrome:// inspect/` to open the `nodejs devtools` for breakpoint debugging: + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/128621/1635995391144-a9ec0d4a-c6fb-4638-a292-615a3588d33d.png#clientId=u069cda7c-313b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=236&id=u4986bfa4&margin=%5Bobject%20Object%5D&name=image.png&originHeight=942&originWidth=1948&originalType=binary&ratio=1&rotation=0&showTitle=false&size=572568&status=done&style=none&taskId=u07555349-8e09-42b2-bd94-f93160b0431&title=&width=488) + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/128621/1635995418427-282d256a-de65-4eba-9a83-b474d3d74f9f.png#clientId=u069cda7c-313b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=445&id=u83271ad1&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1280&originWidth=2280&originalType=binary&ratio=1&rotation=0&showTitle=false&size=710504&status=done&style=none&taskId=uc2614db9-dea9-48d7-b87d-8cb608c8770&title=&width=792) +You can also directly open the link of the `DevTools` protocol output from the command line through the Chrome browser, add a breakpoint to the corresponding code and debug it: + +![10016148-385E-46A4-8B3A-0A0110BECD18.png](https://cdn.nlark.com/yuque/0/2021/png/128621/1635994137067-f663409a-483d-41f5-bc86-4798182edb38.png#clientId=u32db4720-b7d0-4&crop=0&crop=0&crop=1&crop=1&from=ui&height=135&id=GooAh&margin=%5Bobject%20Object%5D&name=10016148-385E-46A4-8B3A-0A0110BECD18.png&originHeight=950&originWidth=2878&originalType=binary&ratio=1&rotation=0&showTitle=false&size=744085&status=done&style=none&taskId=u892d9925-9206-4946-a1ed-cb6043c557d&title=&width=409) + +If you use `vscode`, you can use the js debug terminal of vscode to execute the dev command (without adding the `-- debug` parameter) to start breakpoint debugging. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/128621/1625237917317-8e7bf448-fded-4bc7-b743-6aade0ebcba2.png#clientId=u7c8a3183-c32b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=650&id=u75e3aec7&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1300&originWidth=2868&originalType=binary&ratio=1&rotation=0&showTitle=false&size=1140427&status=done&style=none&taskId=ubcffa6c8-02eb-4256-ba7e-7ab3128c1ee&title=&width=1434) + + +## test command + +Start the test with the current directory. By default, the jest tool is used. You can use the -- mocha parameter to specify mocha. + +```bash +$ midway-bin test --ts + -c, -- cov gets code test coverage + -f, -- file specifies a test file, such as./test/index.test.ts + -- ts TS mode running single test + --forceExit jest forceExit + --runInBand jest runInBand + -w, -- watch watch mode + -- mocha single test using mocha +``` + +When you use mocha for a single test, you must manually install the `mocha` and `@types/mocha` dependencies in the `devDependencies`: `npm I mocha @types/mocha -D`. + +:::info +If the TypeScript path alias is used in the project, please refer to: [Test](../testing# Configuration-alias-paths) +::: + +### Use mocha instead of jest + + +Some students have a special liking for mocha and hope to use mocha as a testing tool. + + +You can use mocha mode for testing. + +```bash +$ midway-bin test --ts --mocha +``` + + +When using mocha for unit testing, you need to manually install the two dependencies `mocha` and `@types/mocha` into `devDependencies`: `npm i mocha @types/mocha -D`. + +### Configure alias paths + +When you configure paths in `tsconfig.json`, and the module package import uses paths, there will be mocha for unit testing, which will cause the path to not be parsed, which cannot be solved by importing `tsconfig-paths/register` + +```typescript +// src/configuration.ts + +import 'tsconfig-paths/register'; +// ... +``` + +Need to add `tsconfig-paths` and reference it for processing during testing + +```bash +$ npm install --save-dev tsconfig-paths +``` + +```bash +$ midway-bin test --ts --mocha -r tsconfig-paths/register +``` + +:::info +Note that since mocha does not come with an assertion tool, you need to use other tools such as assert and chai to make assertions. +::: + + + +## Cov command + +Start the test with the current directory and output the coverage information. By default, the jest tool is used. You can use the -- mocha parameter to specify mocha. + +```bash +$ midway-bin cov --ts +``` + +When using mocha for single-test coverage, you need to install the following additional dependencies. + +```bash +$ npm i mocha @types/mocha nyc --save-dev +``` + + + + +## Check command +Automatically analyze the problems in the code and give repair suggestions. + +```bash +$ midway-bin check +``` + +Verification of `32` issues has been provided. + + + + +## build command + +Use mwcc(tsc) to compile ts code, which is suitable for standard projects. Please use package for Serverless projects. + + +```bash +$ midway-bin build -c + + -c, -- clean Cleanup Build Results Directory + -- srcDir source code directory, default src + -- outDir builds the output directory, which defaults to outDir or dist in the tsconfig. + -tsConfig tsConfig json string or file location + -buildCache Preserve Build Cache +``` + + + + +## deploy command + +Applicable to runtime when Serverless projects are released to Aliyun FC, Tencent SCF, Aws Lambda, etc. + +Executing the deploy command automatically executes the package. + + +```bash +$ midway-bin deploy + + -Y, -- yes The confirmation released is yes + -- resetConfig reset release configuration, AK/AK/Region, etc. + -- serverlessDev Serverless Dev is used to publish aliyun fc functions. the default value is funcraft + ... all parameters compatible with package commands +``` + + + +#### Domain name configuration when the function is published + +If you set `custom.customDomain` to `auto` in `f.yml`, a temporary automatic domain name is configured when you publish it: + +```yaml +custom: + customDomain: auto +``` + +If you want to cancel the automatic domain name, change the `customDomain` to `false`: + +```yaml +custom: + customDomain: false +``` + +If there is a custom domain name, configure it in the `customDomain`: + +```yaml +custom: + customDomain: + domainName: test.example.com +``` + +If you need to use https for a custom domain name, you need to set the customDomain to false after configuring the https certificate in the cloud console to avoid resetting to http the next release: + +```cpp +custom: + customDomain: false +``` + + + +#### Each route is deployed as a function +You can use a high-density scheme and merge it into one function, f.yml plus the following configuration + +```cpp +aggregation: + main: + functionsPattern: + -'*' +``` + + +#### + +#### aliyun releases AK error issue +ak can be reset when aliyun is released for the first time or when the `-resetConfig` parameter is used. + +However, it should be noted that a new `access` group is created by default every time an ak is created. The group name is automatically generated when you modify the configuration. If you want to overwrite the previous AK, you need to manually enter it, as shown in the figure: + +![image.png](https://cdn.nlark.com/yuque/0/2022/png/128621/1645609990378-8a7f92c0-bda4-46e0-93a6-4d6feb6ec66d.png#clientId=u9f50c864-5385-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=122&id=u8a756167&margin=%5Bobject%20Object%5D&name=image.png&originHeight=122&originWidth=693&originalType=binary&ratio=1&rotation=0&showTitle=false&size=17245&status=done&style=none&taskId=u3b825703-abe6-4a2b-ae5f-86a88027cf8&title=&width=693) + +The default group used when publishing is `default`. If you use `default-2` as shown in the above figure when modifying the configuration, you need to specify `default-2` by using the `-- access` parameter when publishing: + +```cpp +midway-bin deploy --access=default-2 +``` + + + +## package command + +Applicable to Serverless project construction + +```bash +$ midway-bin package + -- npm npm client, the default is to automatically identify and add registry + -sourceDir the directory where the source code is located, which will be automatically analyzed by default. + -buildDir build result target directory + -- sharedTargetDir the target directory of shared files. The default is static. Refer to -- sharedDir parameters + -This directory will be copied to the $sharedTargetDir directory in the result directory when the sharedDir is built. + -skipZip skip zip packaging + -skipBuild skip ts code construction + -tsConfig tsConfig json string or file location + -function specify which functions to package, multiple in English, separated +``` + + +#### Detailed parameter explanation + +- `-- function`: Specify which functions to package. Multiple functions are separated in English. + +```shell +// Pack +midway-bin package --function=a, B ,c + +// Publish +midway-bin deploy --function=a, B ,c +``` + + + + +#### File copy logic when function builds packaging + +The content copied by default contains all files that are not suffixed with `.ts` in the `backend code folder` (usually `src`, and `src/apis` for both front and back ends of faas), and all files with `.js`, `.json`, and `.yml` extensions in the `project root directory`, and all files in the `config` and `app` folders. + +If you want to copy additional files, you can specify it by adding the `include` in the `package` field to the `f.yml` file, you can configure the file name, or you can use the `fast-glob` [syntax.↗](https://github.com/mrmlnc/fast-glob#pattern-syntax)The following example shows how to use the match: + +```cpp +#... The display of other attributes has been omitted + +package: + include: # Specify additional package file configuration by include attributes + -static# static folder under the root directory of the project + -a.json# a.json file under the root directory of the project + -a/B/c.js# c.js file under directory a under directory B under the root directory of the project + -a/B/c.json# c.js file under directory a under directory B under the root directory of the project + -xxx/**/*.js# All js files in xxx directory under the root directory of the project +``` + + + +## Experimental function + +Turn on the experimental function by `experimentalFeatures` configuration in `f.yml` + +### 1. ignoreTsError +Ignoring ts error during build without interrupting the build process. +```yaml +experimentalFeatures: + ignoreTsError: true +``` + + +### 2. removeUselessFiles +Removing a large number of invalid files, such as `LICENSE`, `*.ts.map`, and `**/test/` files, can effectively reduce the size of the build package. +```yaml +experimentalFeatures: + removeUselessFiles: true +``` + + + +### 3. fastInstallNodeModules +Selecting production dependencies from the current devDependencies for publishing at build time may significantly improve the publishing speed. + +```yaml +experimentalFeatures: + fastInstallNodeModules: true +``` + + + +## CLI extension + +### 1. Life cycle expansion + +Users can add `midway-integration` fields to `package.json` to extend cli's behavior according to the life cycle of each command. + +For example, add custom logic after the package command `installDevDep`: + + +```bash +{ + "midway-integration": { + "lifecycle": { + "after:package:installDevDep": "npm run build" + } + } +} +``` + +The format of the `lifecycle` is `${ 'before' | 'after' | ''}:${ command }:${ command life cycle}`. + +List of declaration cycles for package commands: + +```bash + 'cleanup', // Clean up the build directory + 'installDevDep', // installation and development period dependency + CopyFile', // copy file: package.include and shared content + 'compile', // + 'emit', // compile function' package:after:tscompile' + 'analysisCode', // analysis code + 'copyStaticFile', // Copy static files in src to the dist directory, such as html, etc. + 'checkAggregation', // Detect high-density deployments + 'generateSpec', // Generate the description file of the corresponding platform, such as serverless.yml, etc. + 'generateEntry', // Generate the portal file for the corresponding platform + 'installLayer', // Install layer + 'installDep', // installation dependency + 'package', // function packaging + 'finalize', // complete +``` + + + + +### 2. Extend through plug-ins + +Users can write cli plug-ins themselves to implement more complex cli behaviors through plug-ins, or add custom commands. +Currently, two plug-ins are supported: + +- Npm plug-in, plug-in is an npm package +- Local plug-in, the plug-in is located locally. +- + + +Cli loads the plug-in by configuring the `plugins` field in the f.yml file: + +```yaml +plugins: + -npm::test-plugin-model + -local::./test/plugin +``` + +The plugin configuration format is `${ 'npm' | 'local' }:${ provider | | ''}:${ pluginName | | path }` + +Code reference for plug-ins: + +```typescript +// src/index.ts + +import { BasePlugin } from '@midwayjs/command-core'; + +export class TestLalalaPlugin extends BasePlugin { + commands = { + lalala: { + Usage: 'custom command', + lifecycleEvents: [ + 'a', // Custom Lifecycle + 'b', + ], + // Not yet + options: { + name: { + usage: 'parameter name, for example: mw lalala -- name = 123', + shortcut: 'n', // parameter abbreviation + }, + }, + }, + }; + + hooks = { + // Add the command lifecycle extension in the current plugin + // the life cycle of the lalala command + 'lalala:a': async () => { + + // Output + this.core.cli.log('lalala command hook'); + + // Get the parameters entered by the user. + this.core.cli.log(this.core.options); + + // f.yml content + this.core.cli.log(this.core.service); + + // Only the output under the-V parameter + this.core.debug('lalala'); + }, + + // Add command lifecycle extensions in other plug-ins + // Execute "before" the copyFile life cycle of the package command + 'before:package:copyFile': async () => { + console.log('package command hook'); + }, + + }; +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/create_midway.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/create_midway.md new file mode 100644 index 000000000000..9452e124f103 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/create_midway.md @@ -0,0 +1,171 @@ +# Scaffolding + +Midway has written the `create-midway` package. Through the npx command, you can easily use the `npm init midway` command to create scaffolding. + +```bash +$ npm init midway@latest -y +``` + +:::tip + +If you don’t add the @latest tag, you may not be updated to the latest version. + +::: + + + +## Create scaffolding via CLI + + + +### Default behavior + +Without passing parameters, you can list the currently most commonly used templates. + +For example, execute + +```bash +$ npm init midway@latest -y +``` + +will output + +```bash +➜ ~ npm init midway +? Hello, traveler. + Which template do you like? … + + ⊙ v3 +❯ koa-v3 - A web application boilerplate with midway v3(koa) + egg-v3 - A web application boilerplate with midway v3(egg 2.0) + faas-v3 - A serverless application boilerplate with midway v3(faas) + component-v3 - A midway component boilerplate for v3 + quick-start - A midway quickstart exmaple for v3 + + ⊙ v3-esm + koa-v3-esm - A web application boilerplate with midway v3(koa) + + ⊙ v2 + web - A web application boilerplate with midway and Egg.js + koa - A web application boilerplate with midway and koa +``` + +In this mode, templates will be created according to user selections and guidelines. + + + +### About parameter passing + +Since `npm init midway` is equivalent to `npm exec create-midway`, the format of [passing parameters](https://docs.npmjs.com/cli/v10/commands/npm-exec) depends on different npm versions. different. + +For example, in the latest npm, use additional `--` to pass parameters. + +for example + +```bash +$ npm init midway -- -h +``` + +The `-h` parameter makes all available options explicit. + +All parameter examples below will be displayed in this mode. + + + +### Show all templates + +Templates that are not the current version will be hidden by default. All built-in templates can be displayed through the `-a` parameter. + +```bash +$ npm init midway -- -a +``` + + + +### Specify template name + +Each template has a template name and template description. For example, the template name of `koa-v3 - A web application boilerplate with midway v3(koa)` is `koa-v3`. + +The template name can be specified via the `--type` parameter. + +```bash +$ npm init midway -- --type=koa-v3 +``` + + + +### Specify template package name + +When the custom template is published on npm, we can use `-t` or `--template` to specify the package name. + +```bash +$ npm init midway -- -t=custom-template +``` + +If the package is still being developed locally, you can also specify a relative path or an absolute path. + +```bash +$ npm init midway -- -t=./custom-template +``` + + + +### Specify the creation target directory + +The directory to be created can be specified through the `--target` parameter, which must be used together with the `type` or `template` parameter. + +For example, the following command specifies the `koa-v3` template and generates it into the current abc directory. If the directory does not exist, it will be created. + +```bash +$ npm init midway -- --type=koa-v3 --target=abc +``` + +Generally, `target` can be omitted and the path can be placed as the last parameter. + +```bash +$ npm init midway -- --type=koa-v3 abc +``` + + + +### Specify npm client + +If you have a private client, you can use `--npm` to specify the client. + +```bash +$ npm init midway -- --npm=tnpm +``` + + + +### Specify registry + +If you have a private source, you can use `--registry` to specify the private source. + +```bash +$ npm init midway -- --registry=https://registry.npmmirror.com +``` + + + +### Scaffolding parameters + +If the scaffold contains user-passable parameters, they can also be passed through the command line. + +```bash +$ npm init midway -- --bbb=ccc +``` + +If the parameter name is the same as the parameter of the tool, you can use the parameter of `t_`, which will be automatically processed when the tool is passed to the scaffold. + +```bash +$ npm init midway -- --type=koa-v3 --t_type=ccc +``` + + + +## Write scaffolding + +Midway scaffolding uses the self-developed light-generator tool. For specific usage, please refer to [https://github.com/midwayjs/light-generator](https://github.com/midwayjs/light-generator). + +You can also refer to Midway's own [template project](https://github.com/midwayjs/midway-boilerplate/tree/master/v3). \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/egg-ts-helper.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/egg-ts-helper.md new file mode 100644 index 000000000000..1e9723861833 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/egg-ts-helper.md @@ -0,0 +1,46 @@ +# egg:ts-helper + +For scenarios where midway supports Egg.js, the original [egg-ts-helper](https://github.com/whxaxes/egg-ts-helper) package is rewritten, and the original TS and AST analysis dependencies are removed. + +The ts v3 environment that the original package depends on depends on the egg directory structure, considering many possibilities, it will not be used in the midway scenario. Based on the above considerations, midway rewrites this package to provide egg definitions in the simplest way. + +The [@midwayjs/egg-ts-helper](https://github.com/midwayjs/egg-ts-helper) package provides the `ets` global command. + +```bash +$ npm i @midwayjs/egg-ts-helper --save-dev +$ ets +``` + +Usually we will add it to the development command. + +```json + "scripts": { + "dev": "cross-env ets && cross-env NODE_ENV=local midway-bin dev --ts \", + }, +``` + +:::info +This package is customized for midway and can only be used for the new version of midway and its supporting code. +::: + +Finally, a `typings` directory will be generated in the project root directory with the following definition structure and files: + +``` +. +├── ... +└── typings + ├── extend + │ ├── request.d.ts + │ ├── response.d.ts + │ ├── application.d.ts + │ └── context.d.ts + ├── app + │ └── index.d.ts + └── config + ├── index.d.ts + └── plugin.d.ts +``` + +:::caution +Note that this module only aggregates the framework and plug-in definitions of midway v2(Egg.js), so that the current business code can smoothly read the definitions of the framework and plug-in. It does not support the definition of the business code itself or the definition when developing egg plug-ins. +::: diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/luckyeye.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/luckyeye.md new file mode 100644 index 000000000000..c1742a0cc893 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/luckyeye.md @@ -0,0 +1,52 @@ +# Rule check tool + +Midway provides some checking tools for common errors to facilitate users to quickly debug them. The `@midwayjs/luckyeye` package provides some basic inspection rules, which can quickly troubleshoot problems with the new version of Midway. + +> luckyeye, meaning lucky eyes, can quickly find and locate problems. + +## Use + +Install the `@midwayjs/luckyeye` package first. + +```bash +npm I @midwayjs/luckyeye --save-dev +``` + +In general, we will add it to a check script, such: + +```json +"scripts": { + // ...... + "check": "luckyeye" +}, +``` + +Next, we need to configure a "rule package". For example, `midway_ v2` is a rule check package for midway v2. + +Add the following paragraph to `package.json`. + +```json +"midway-luckyeye": { + "packages": [ + "midway_v2" + ] +}, +``` + +## Execution + +After the configuration is completed, you can execute the inspection script added above. + +```bash +npm run check +``` + +**Blue** indicates the output information. **Green** indicates that the check item passes. **Red** indicates that the check item has a problem and needs to be modified. **Yellow** indicates that the check item can be modified, but optional. + +The execution effect is as follows. + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1610983986151-79c54e7c-3ff0-4f94-98bc-359dda0fa694.png) + +## Custom rule package + +Please refer to README for [https:// github.com/midwayjs/luckyeye](https://github.com/midwayjs/luckyeye). diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/mwts.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/mwts.md new file mode 100644 index 000000000000..fb5ebf6b5f98 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/mwts.md @@ -0,0 +1,85 @@ +# Lint tools and formatting + +Midway's framework and business code are written by TypeScript. The default Midway provides a set of default lint, editor and formatting rules for more convenient development and testing. + +## Code style library + +The code style library of Midway is called [mwts](https://github.com/midwayjs/mwts), which is derived from Google's [gts](https://github.com/google/gts). Mwts is Midway's TypeScript style guide and the configuration of formatter, linter and automatic code fix. + +:::info +In the midway project, we will add mwts by default. The following process is just to explain how to use mwts. +::: + +In order to use mwts, we need to add it to the development dependency. + +```json + "devDependencies": { + "mwts": "^1.0.5 ", + "typescript": "^4.0.0" + }, +``` + +## ESLint configuration + +Mwts provides a default set of ESLint configurations (TSLint has been abandoned and merged into ESLint). + +Create a `.eslintrc.json` file in the root directory of the project, with the following contents (usually scaffolding will bring it with it): + +```json +{ + "extends": "./node_modules/mwts /", + "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "interface.ts"] + "env": { + "jest": true + } +} +``` + +The above is the default configuration of midway project. Other project `ignorePatterns` and `env` can be adjusted according to ESLint. + +For more information about the default rules for mwts, see [here](https://github.com/midwayjs/mwts/blob/master/.eslintrc.json). + +## Perform code checking and formatting + +You can run the `mwts check` command and the `mwts fix` command to check the code. For example, add script commands to the project (usually scaffolding will come with it). + +```typescript + "scripts": { + "lint": "mwts check ", + "lint:fix": "mwts fix", + }, +``` + +## Prettier configuration + +Mwts provides a set of default prettier configurations, creating a `.prettierrc.js` file with the following configuration contents (usually scaffolding comes with it). + +```javascript +module.exports = { + ...require('mwts/.prettierrc.json') +}; +``` + +## Configure save automatic formatting + +Let's take VSCode as an example. + +The first step is to install the Prettier plug-in. + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1618042429530-177c3636-aefc-419d-8d3a-5258cad13631.png) + +Open the configuration, search for "save", find "Format On Save" on the right, and check. + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1618042494782-71b6cc3c-18ae-4344-987b-ec82084f2dd8.png) + +If saving the file has no effect, the editor usually has multiple formatting methods, you can right-click to make the default selection. + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1618125271116-845e8452-0f7b-46a9-a28a-388f2db9c5e3.png) + +Select Configure Default Formatters ". + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1618125381302-d3fe30c1-e56d-43f8-ada2-6e315f4ff2c4.png) + +Select Prettier. + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1618125423564-8e46b0f8-f422-4e3d-a805-3b0a1db037f8.png) diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/mwtsc.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/mwtsc.md new file mode 100644 index 000000000000..256047051ea2 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/mwtsc.md @@ -0,0 +1,111 @@ +# Development tools + +Based on the standard tsc module, midway has developed a simple tool for developing and building ts files locally. + +Its usage is almost identical to standard tsc. + +```bash +$ npx mwtsc +``` + +Equivalent to executing the `tsc` command. + + + +## Common commands + +Since mwtsc is developed based on tsc, it can use all tsc commands. + +for example + +```bash +# Listening mode +$ npx mwtsc --watch + +# Use different tsconfig files +$ npx mwtsc --project tsconfig.production.json +``` + +More parameters can be consulted [tsc cli tool](https://www.typescriptlang.org/docs/handbook/compiler-options.html). + +The following introduces more new parameters of midway. + + + +## Run command + +In order to make tsc effective during the code development period, midway provides a `run` parameter, which is used to execute a file after tsc is compiled successfully, which is similar to the `tsc-watch` module. + +for example + +```bash +$ mwtsc --watch --run @midwayjs/mock/app.js +``` + +The above command will execute the following logic: + +* 1. Compile the code and execute the `@midwayjs/mock/app.js` file after successful compilation +* 2. If you modify the code, compilation will be automatically triggered. After killing the last executed file, the `@midwayjs/mock/app.js` file will be automatically executed. + +The `run` parameter can execute any js file, and midway relies on this parameter for local development. + +for example + +```bash +$ npx mwtsc --watch --run ./bootstrap.js +``` + +Of course, it can also be used with other parameters. + +```bash +$ npx mwtsc --watch --project tsconfig.production.json --run ./bootstrap.js +``` + +Note that the `run` command must be placed at the end, and all parameters after it will be passed to the child process. + + + +## Framework configuration + +You can use mwtsc in the Midway project for development and testing, such as: + +```json +{ + "scripts": { + "dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app", + "build": "cross-env rm -rf dist && tsc" + }, +} +``` + +Here `@midwayjs/mock/app` refers to the `app.js` file in the `@midwayjs/mock` package. This file is used to start the framework during local development. For Serverless environments, there are also corresponding startup files. + +```json +{ + "scripts": { + "dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/function", + }, +} +``` + + + +## Common abilities + +### Adjust port + +The started http port can be dynamically modified through the parameter `--port`. This parameter has a higher priority than the port configuration in the code. + +```bash +$ npx mwtsc --watch --run @midwayjs/mock/app --port 7001 +``` + + + +### Enable https + +The framework has a built-in https certificate for local testing, which can be enabled through the parameter `--ssl`. + +```bash +$ npx mwtsc --watch --run @midwayjs/mock/app --ssl +``` \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/sequelize_generator.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/sequelize_generator.md new file mode 100644 index 000000000000..ca7e08247ea2 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/sequelize_generator.md @@ -0,0 +1,203 @@ +# sequelize-auto-midway + +forked from [sequelize/sequelize-auto](https://github.com/sequelize/sequelize-auto) + +Generate `Sequelize` entities for `Midway` through an existing database. + +For other detailed documents and usage, please refer to [sequelize/sequelize-auto](https://github.com/sequelize/sequelize-auto). + +## Installation + +```bash +$ npm i sequelize-auto-midway +``` + +## Usage + +```bash +# Recommended +# Please replace the configuration information +npx sequelize-auto-midway -h localhost -d yourDBname -u root -x yourPassword -p 13306 --dialect mysql -o ./models --noInitModels true --caseModel c --caseProp c --caseFile c --indentation 1 -a ./additional.json +``` + +additional.json + +```json +{ + "timestamps": true + "paranoid": true +} +``` + +The automatically generated template files are as follows: + +```ts +import { Column, DataType, Table, Model } from 'sequelize-typescript'; + +@Table({ + tableName: 'task', + timestamps: false + indexes: [ + { + name: 'PRIMARY', + unique: true, + using: 'BTREE', + fields: [{ name: 'task_id' }] + }, + ], +}) +export class TaskEntity extends Model { + @Column({ + autoIncrement: true + type: DataType.INTEGER.UNSIGNED + allowNull: false + primaryKey: true + field: 'task_id', + }) + taskId: number; + + @Column({ + type: DataType.TINYINT.UNSIGNED, + allowNull: false, + defaultValue: 0, + comment: '******', + field: 'app_id', + }) + appId: number; + + @Column({ + type: DataType.STRING(64), + allowNull: false, + comment: '****', + field: 'task_name', + }) + taskName: string; + + @Column({ + type: DataType.TINYINT.UNSIGNED + allowNull: false + defaultValue: 0 + comment: 'Task Category: 1-cron,2-interval', + }) + type: number; + + @Column({ + type: DataType.TINYINT.UNSIGNED + allowNull: false + defaultValue: 0 + comment: 'Task Status: 0-Pause, 1-Startup', + }) + status: number; + + @Column({ + type: DataType.DATE + allowNull: true + comment: 'Task Start Time', + field: 'start_time', + }) + startTime: string; + + @Column({ + type: DataType.DATE + allowNull: true + comment: 'Mission End Time', + field: 'end_time', + }) + endTime: string; + + @Column({ + type: DataType.INTEGER + allowNull: false + defaultValue: -1 + comment: 'Number of Task Executations', + }) + limit: number; + + @Column({ + type: DataType.STRING(128) + allowNull: true + defaultValue: '', + comment: 'task cron configuration', + }) + cron: string; + + @Column({ + type: DataType.INTEGER.UNSIGNED + allowNull: true + defaultValue: 0 + comment: 'Task Execution Interval', + }) + every: number; + + @Column({ + type: DataType.STRING(255) + allowNull: true + comment: 'parameter', + }) + args: string; + + @Column({ + type: DataType.STRING(255) + allowNull: true + comment: 'Remarks', + }) + remark: string; +} +``` + +Use `npx sequelize-auto-midway --help` to see all available parameters with their descriptions. Some basic parameters below: + +```bash +Usage: npx sequelize-auto-midway -h -d -p [port] -u -x +[password] -e [engine] + +Options: + --help Show help [boolean] + --version Show version number [boolean] +-h, --host IP/Hostname for the database. [string] +-d, --database Database name. [string] +-u, --user Username for database. [string] +-x, --pass Password for database. If specified without providing + a password, it will be requested interactively from + the terminal. +-p, --port Port number for database (not for sqlite). Ex: + MySQL/MariaDB: 3306, Postgres: 5432, MSSQL: 1433 + [number] +-c, --config Path to JSON file for Sequelize-Auto options and + Sequelize's constructor "options" flag object as + defined here: + https://sequelize.org/master/class/lib/sequelize.js~Sequelize.html#instance-constructor-constructor + [string] +-o, --output What directory to place the models. [string] +-e, --dialect The dialect/engine that you're using: postgres, + mysql, sqlite, mssql [string] +-a, --additional Path to JSON file containing model options (for all + tables). See the options: https://sequelize.org/master/class/lib/model.js~Model.html#static-method-init + [string] + --indentation Number of spaces to indent [number] +-t, --tables Space-separated names of tables to import [array] +-T, --skipTables Space-separated names of tables to skip [array] +--caseModel, --cm Set case of model names: c|l|o|p|u + c = camelCase + l = lower_case + o = original (default) + p = PascalCase + u = UPPER_CASE +--caseProp, --cp Set case of property names: c|l|o|p|u +--caseFile, --cf Set case of file names: c|l|o|p|u|k + k = kebab-case +--noAlias Avoid creating alias `as` property in relations + [boolean] +--noInitModels Prevent writing the init-models file [boolean] +-n, --noWrite Prevent writing the models to disk [boolean] +-s, --schema Database schema from which to retrieve tables[string] +-v, --views Include database views in generated models [boolean] +-l, --lang Language for Model output: es5|es6|esm|ts + es5 = ES5 CJS modules (default) + es6 = ES6 CJS modules + esm = ES6 ESM modules + ts = TypeScript [string] +--useDefine Use `sequelize.define` instead of `init` for es6|esm|ts +--singularize, --sg Singularize model and file names from plural table + names +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/typeorm_generator.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/typeorm_generator.md new file mode 100644 index 000000000000..8157b84d77ca --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/typeorm_generator.md @@ -0,0 +1,50 @@ +# typeorm:Model Generator + +Thank community user @youtiao66 for providing this module. + + +With this tool, you can quickly create a TypeORM Model for Midway. + + +## Use + +For example, generate a mysql model. + +```bash +# Recommended +# Please replace the configuration information +$npx mdl-gen-midway -h localhost -p 3306 -d yourdbname -u root -x yourpassword -e mysql --noConfig --case-property none +``` + +Full parameters: + +``` +Usage: npx mdl-gen-midway -h -d -p [port] -u -x +[password] -e [engine] + +Options: + --help Show help [boolean] + --version Show version number [boolean] + -h, --host IP address/Hostname for database server + [default: "127.0.0.1"] + -d, --database Database name(or path for sqlite) [required] + -u, --user Username for database server + -x, --pass Password for database server [default: ""] + -p, --port Port number for database server + -e, --engine Database engine + [choices: "mssql", "postgres", "mysql", "mariadb", "oracle", "sqlite"] + [default: "mssql"] + -o, --output Where to place generated models + [default: "./output"] + -s, --schema Schema name to create model from. Only for mssql + and postgres. You can pass multiple values + separated by comma eg. -s scheme1,scheme2,scheme3 + --ssl [boolean] [default: false] + + --noConfig Doesn't create tsconfig.json and + ormconfig.json [Boolean] [Default: false] + + --cp, --case-property Convert property names to specified case + [Optional values: "pascal", "camel", "snake", "none"] [Default value: "camel"] + +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/version_check.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/version_check.md new file mode 100644 index 000000000000..90e8d4ced8bf --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/tool/version_check.md @@ -0,0 +1,164 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Version check tool + +Due to the uncertainty of the installed version of dependencies, Midway provides a version check tool `midway-version`, which can quickly check compatibility errors between versions. + +## Check compatibility + +You can use the following command to execute the check in the project root directory. + +The following command will check the version actually installed in `node_modules`, not the version written in `package.json`. + + + + + +```bash +$ npx midway-version@latest +``` + + + + + +```bash +$ pnpx midway-version@latest +``` + + + + + +```bash +$ yarn add midway-version@latest +$ yarn midway-version +``` + + + + + +## Upgrade to the latest version + +You can use the following command to execute the upgrade in the project root directory. + +The `-u` parameter will check all midway modules and upgrade them to the `latest` version according to the actual installed version in `node_modules` and the version written in `package.json`. + +If the currently installed component version is `3.16.2` and the latest version is `3.18.0`, you will be prompted to upgrade to `3.18.0`. + +When using the `-u -w` parameter: + +* Update the version of `package.json` and keep the prefix, for example, `^3.16.0` will become `^3.18.0` +* Write the `3.18.0` version to the lock file (if exists) + + + + + +```bash +$ npx midway-version@latest -u +``` + +After confirming that the output is correct, you can use the `-w` parameter to write the `package.json` and `package-lock.json` files (if exists). + +```bash +$ npx midway-version@latest -u -w +``` + + + + +```bash +$ pnpx midway-version@latest -u +``` + +After the output is confirmed to be correct, you can use the `-w` parameter to write `package.json` and `pnpm-lock.yaml` files (if exists). + +```bash +$ pnpx midway-version@latest -u -w +``` + + + + + +```bash +$ yarn add midway-version@latest +$ yarn midway-version -u +``` + +After the output is confirmed to be correct, you can use the `-w` parameter to write `package.json` and `yarn.lock` files (if exists). + +```bash +$ yarn midway-version -u -w +``` + + + + + +## Upgrade to the latest compatible version + +The `-m` parameter will check all midway modules and upgrade them to the `latest compatible` version according to the actual installed version in `node_modules` and the version written in `package.json`. + +If the currently installed component version is `3.16.0`, the latest version is `3.18.0`, and the compatible versions are `3.16.1` and `3.16.2`, it will prompt to upgrade to `3.16.2`. + +The `-m` parameter is generally used to fix the lower version and check the wrong component version, so the strategy is different from `-u`. + +When using the `-m -w` parameter: + +* Update the version of `package.json` + +* If there is a lock file, the prefix will be retained, such as `^3.16.0` will become `^3.16.2` + +* If there is no lock file, the prefix will be removed and the version will be fixed, such as `^3.16.0` will become `3.16.2` + +* Write the `3.16.2` version to the lock file (if any) + + + + + +```bash +$ npx midway-version@latest -m +``` + +After confirming that the output is correct, you can use the `-w` parameter to write the `package.json` and `package-lock.json` files (if exists). + +```bash +$ npx midway-version@latest -m -w +``` + + + + +```bash +$ pnpx midway-version@latest -m +``` + +After the output is confirmed to be correct, you can use the `-w` parameter to write `package.json` and `pnpm-lock.yaml` files (if exists). + +```bash +$ pnpx midway-version@latest -m -w +``` + + + + + +```bash +$ yarn add midway-version@latest +$ yarn midway-version -m +``` + +After the output is confirmed to be correct, you can use the `-w` parameter to write `package.json` and `yarn.lock` files (if exists). + +```bash +$ yarn midway-version -m -w +``` + + + + \ No newline at end of file diff --git a/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/upgrade_v3.md b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/upgrade_v3.md new file mode 100644 index 000000000000..b7c9f199aa44 --- /dev/null +++ b/site/i18n/en/docusaurus-plugin-content-docs/version-3.0.0/upgrade_v3.md @@ -0,0 +1,540 @@ +# Upgrade to 3.x + +This article will introduce how to upgrade from midway v2 to midway v3. + +Upgrading from Midway v2 to Midway v3, there will be some Breaking Changes. This document will list these Breaking places in detail, so that users can know the changes in advance and respond to them. + + + +## Automatic upgrade tool + +**Before the upgrade, please cut out a new branch to avoid the failure of the upgrade and cause no recovery! ! ! ** + +Copy the following script and execute it in the project root directory: + +```bash +$ npx --ignore-existing midway-upgrade +``` + +:::tip + +Due to different business situations, please check the manual upgrade after the script upgrade. + +::: + + + +## Manual upgrade + +**midway v3 support since node v12. ** + + +### Package version update + +All component packages, core packages will be upgraded to 3.x version. + +```json +{ + "dependencies": { + "@midwayjs/bootstrap": "^3.0.0", + "@midwayjs/core": "^3.0.0", + "@midwayjs/decorator": "^3.0.0", + "@midwayjs/koa": "^3.0.0", + "@midwayjs/task": "^3.0.0", + }, + "devDependencies": { + "@midwayjs/cli": "^1.2.90", + "@midwayjs/luckyeye": "^1.0.0", + "@midwayjs/mock": "^3.0.0", + // ... + } +} + +``` + +`@midwayjs/cli` and `@midwyajs/luckeye`, except `@midwayjs/logger` version. + + + +### Query/Body/Param/Header decorator changes + + +Mainly the default behavior without parameters. + + +old + +```typescript +async invoke(@Query() name) { + // ctx.query.name +} +``` +new +```typescript +async invoke(@Query() name) { + // ctx.query +} + +async invoke(@Query('name') name) { + // ctx.query.name +} +``` + + + +### Validate/Rule decorator + + +old +```typescript +import { Validate, Rule, RuleType } from '@midwayjs/decorator'; +``` +new +```typescript +import { Validate, Rule, RuleType } from '@midwayjs/validate'; +``` +Since validate is abstracted into a component, dependencies need to be installed and enabled in the code. +```typescript +// src/configuration +import * as validate from '@midwayjs/validate'; + +@Configuration({ + // ... + imports: [ + validate + ], +}) +export class MainConfiguration { + // ... +} + +``` + +### task component configuration key change + +old + +```typescript +export const taskConfig = {}; +``` + +new + +```typescript +export const task = {}; +``` + + + +### Configured absolute path + + +Relative paths are no longer supported + + +old + +```typescript +// src/configuration + +@Configuration({ + // ... + importConfigs: [ + './config' // ok + ] +}) +export class MainConfiguration { + // ... +} + +``` +new + +```diff +// src/configuration +import { join } from 'path'; + +@Configuration({ + // ... + importConfigs: [ +- './config' // error ++ join(__dirname, './config') // ok + ] +}) +export class MainConfiguration { + // ... +} + +``` + +### Use default frame/multiframe + + +Old, will be introduced in bootstrap.js +```typescript +const WebFramework = require('@midwayjs/koa').Framework; +const GRPCFramework = require('@midwayjs/grpc').Framework; +const { Bootstrap } = require('@midwayjs/bootstrap'); + +Bootstrap + .load(config => { + return new WebFramework().configure(config.cluster); + }) + .load(config => { + return new GRPCFramemwork().configure(config.grpcServer); + }) + .run(); +``` + + +new version + + +Separate instantiation is no longer required in bootstrap.js +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap.run(); +``` +Instead, import as a component +```typescript +// src/configuration +import * as web from '@midwayjs/web'; +import * as grpc from '@midwayjs/grpc'; + +@Configuration({ + // ... + imports: [ + web, + grpc, + //... + ], +}) +export class MainConfiguration { + // ... +} +``` + + +Other effects: + + + +- 1. It is no longer necessary to use the createBootstrap method to start from bootstrap.js in the test +- 2. The configuration of the original entry Framework can now be placed in config.*.ts, with the framework name as the key + + + +### Removed batch of IoC container APIs + + +Remove the following methods on container + + +- getConfigService(): IConfigService; +- getEnvironmentService(): IEnvironmentService; +- getInformationService(): IInformationService; +- setInformationService(service: IInformationService): void; +- getAspectService(): IAspectService; +- getCurrentEnv(): string; + + +Now there are corresponding framework built-in services to replace. + + +For example, the old way of writing: + +```typescript +const environmentService = app.getApplicationContext().getEnvironmentService(); +const env = environmentService.getCurrentEnvironment(); +``` + + +new spelling +```typescript +const environmentService = app.getApplicationContext().get(MidwayEnvironmentService) +const env = environmentService.getCurrentEnvironment(); +``` + + + +## @midwayjs/web(egg) section + +### start port + +The new version of the framework will read a port configuration when it is started. If it is not configured, port monitoring may not be started. + +```json +// src/config/config.default +export default { + // ... + egg: { + port: 7001, + }, +} +``` + + + +### Add egg-mock + +Since the framework removed the egg-mock package, in the new version `package.json` needs to be referenced manually. + +```json +{ + "devDependencies": { + "egg-mock": "^1.0.0", + // ... + } +} +``` + +### logger + +The new version uses @midwayjs/logger uniformly, whether egg logger is enabled or not. + +In order not to conflict with the egg log, we use a new key, and the original `midwayFeature` field is no longer used. + +old + +```typescript +export const logger = { + level: 'warn', + consoleLevel: 'info' +} +``` + +new + +```typescript +export const midwayLogger = { + default: { + level: 'warn', + consoleLevel: 'info' + } +} +``` + +Egg's `customLogger` field is compatible with egg plugins that cannot be modified. For business code, it is best to modify them. + +```typescript +export const midwayLogger = { + default: { + level: 'warn', + consoleLevel: 'info' + }, + clients: { + // custom log + customLoggerA: { + // ... + } + } +} +``` + +For the rest of the more specific configuration, please refer to the Customization section in [Log Chapter](logger). + +### egg plugin + +In Midway3, we turned off most of the egg default plugins in order to unify documentation and behavior. + +The default plugins in the new version are as follows: + +```javascript +module.exports = { + onerror: true, + security: true, + static: false, + development: false, + watcher: false, + multipart: false, + logrotator: false, + view: false, + schedule: false, + i18n: false, +} +``` + +Please turn it on as appropriate (may conflict with midway ability). + +The default egg log cutting plugin (logrotator), because the log no longer supports egg logger, we directly closed it in the framework (midway logger comes with cutting). + + + +### Scheduled tasks + +If you want to use the old `@Schedule` decorator, you need to additionally install the `midway-schedule` package and import it as an egg plugin. + +```typescript +// src/config/plugin.ts + +export default { + schedule: true, + schedulePlus: { + enable: true, + package: 'midway-schedule', + } + // ... +} +``` + + + + + +## Other adjustments for component/framework developers + + + +### RegisterObject in the component no longer adds namespace + + +During component development, the namespace prefix is no longer added. + + +old, component entry +```typescript +@Configuration({ + namespace: 'A' + // ... +}) +export class MainConfiguration { + + async onReady(container) { + container.registerObject('aaa', 'bbb'); + } +} + +container.getAsync('A:aaa'); // => OK +``` + + +new component entry + +```typescript +@Configuration({ + namespace: 'A' + // ... +}) +export class MainConfiguration { + + async onReady(container) { + container.registerObject('aaa', 'bbb'); + } +} + +container.getAsync('aaa'); // => OK +``` + + + + +### Custom framework section + + +The changes in the custom framework are relatively large, and the componentization of the framework is the goal of this version. There are several places that need to be modified. + + +**1. Add the @Framework logo to the original framework** + + +old +```typescript +export class CustomKoaFramework extends BaseFramework { +// ... +} +``` +new +```typescript +import { Framework } from '@midwayjs/core'; + +@Framework() +export class CustomKoaFramework extends BaseFramework { +// ... +} +``` + + +**2. Export Configuration at the entrance according to the component specification** + + +You can use lifecycles in configuration, same as components. The `run` method will be called and executed explicitly during the newly added `onServerReady` lifecycle. + +```typescript +import { Configuration,Inject } from '@midwayjs/core'; +import { MidwayKoaFramework } from './framework'; + +@Configuration({ + namespace: 'koa', +}) +export class KoaConfiguration { + @Inject() + framework: MidwayKoaFramework; + + async onReady() {} + + async onServerReady() { + // ... + } +} + +``` + + +**3. During framework development** + +**It should be noted that since the framework is initialized before the user life cycle, when applicationInit, do not inject the configuration through the @Config decorator, but call configService to obtain it. ** + + +```typescript +import { Framework } from '@midwayjs/core'; + +@Framework() +export class CustomKoaFramework extends BaseFramework { + + configure() { + /** + * return your configuration here + * The returned value will be assigned to this.configurationOptions, and the original user's explicit input parameters will be connected + * + */ + return this.configService.getConfiguration('xxxxxxx'); + } + + /** + * This new method is used to determine whether the framework is loaded + * Sometimes components include server side (framework) and client side, you need to judge + * + */ + isEnable(): boolean { + return this.configurationOptions.services?.length > 0; + } + + // ... +} +``` + +This can also be judged when used outside. + +```typescript +import { Configuration,Inject } from '@midwayjs/core'; +import { MidwayKoaFramework } from './framework'; + +@Configuration({ + namespace: 'koa', +}) +export class KoaConfiguration { + @Inject() + framework: MidwayKoaFramework; + + async onReady() {} + + async onServerReady() { + // If isEnable is true, the framework will call framework.run() by default + // If enable is false at the beginning, you can also delay to manually run + if (/* defer execution */) { + await this.framework.run(); + } + } +} + +``` diff --git a/site/src/components/Preview/class.json b/site/src/components/Preview/class.json index 5de642f7eaf9..2dbca2f1bad8 100644 --- a/site/src/components/Preview/class.json +++ b/site/src/components/Preview/class.json @@ -22,7 +22,7 @@ "content": "{\n \"compileOnSave\": true,\n \"compilerOptions\": {\n \"target\": \"ES2018\",\n \"module\": \"commonjs\",\n \"moduleResolution\": \"node\",\n \"experimentalDecorators\": true,\n \"emitDecoratorMetadata\": true,\n \"inlineSourceMap\":true,\n \"noImplicitThis\": true,\n \"noUnusedLocals\": true,\n \"stripInternal\": true,\n \"skipLibCheck\": false,\n \"pretty\": true,\n \"declaration\": true,\n \"typeRoots\": [ \"./typings\", \"./node_modules/@types\"],\n \"outDir\": \"dist\"\n },\n \"exclude\": [\n \"dist\",\n \"node_modules\",\n \"test\"\n ]\n}" }, "src/configuration.ts": { - "content": "import { Configuration, App } from '@midwayjs/core';\nimport { Application } from '@midwayjs/koa';\nimport * as bodyParser from 'koa-bodyparser';\nimport * as orm from '@midwayjs/orm';\nimport { join } from 'path';\n\n@Configuration({\n conflictCheck: true,\n imports: [\n orm // 加载 orm 组件\n ],\n importConfigs: [\n join(__dirname, './config')\n ]\n})\nexport class ContainerLifeCycle {\n @App()\n app: Application;\n\n async onReady() {\n // bodyparser options see https://github.com/koajs/bodyparser\n this.app.use(bodyParser());\n }\n}\n" + "content": "import { Configuration, MainApp } from '@midwayjs/core';\nimport { Application } from '@midwayjs/koa';\nimport * as bodyParser from 'koa-bodyparser';\nimport * as orm from '@midwayjs/orm';\nimport { join } from 'path';\n\n@Configuration({\n conflictCheck: true,\n imports: [\n orm // 加载 orm 组件\n ],\n importConfigs: [\n join(__dirname, './config')\n ]\n})\nexport class ContainerLifeCycle {\n @MainApp()\n app: Application;\n\n async onReady() {\n // bodyparser options see https://github.com/koajs/bodyparser\n this.app.use(bodyParser());\n }\n}\n" }, "src/interface.ts": { "content": "/**\n * @description User-Service parameters\n */\nexport interface IUserOptions {\n uid: string;\n}\n" diff --git a/site/versioned_docs/version-1.0.0/deploy.md b/site/versioned_docs/version-1.0.0/deploy.md index 6e0a11c7a5b9..2d1e9055659d 100644 --- a/site/versioned_docs/version-1.0.0/deploy.md +++ b/site/versioned_docs/version-1.0.0/deploy.md @@ -6,6 +6,14 @@ title: 部署 由于 TypeScript 的特殊性,本地开发可以有 ts-node 等类似的工具进行开发,而在服务器端运行的时候,我们希望可以通过 js 来运行,这中间就需要编译工具。 +```typescript +// 类型声明示例 +type Config = { + typescript: boolean; + srcDir: string; +} +``` + 幸好 TypeScript 官方提供了 tsc 工具来帮助这个过程,而编译时会自动调用 `tsconfig.json` 来做一些编译时处理,midway 默认提供了一份该文件,用户也可以进行自定义。 同时,在脚手架中,我们提供了 `build` 命令帮助用户更好的生成文件。 @@ -57,9 +65,9 @@ module.exports = (pandora) => { 支持的参数见 [启动参数](https://github.com/eggjs/egg-cluster/blob/master/lib/master.js#L33),同时,midway 框架额外增加了几个参数。 -- typescript {boolean} 如果为 true,则会开启 ts 模式,加载 src 或者 dist 目录,默认内部会进行判断,无需手动处理 -- srcDir {string} 源码路径,默认为 src -- targetDir {string} 编译后路径,默认为 dist +- typescript `{boolean}` 如果为 true,则会开启 ts 模式,加载 src 或者 dist 目录,默认内部会进行判断,无需手动处理 +- srcDir `{string}` 源码路径,默认为 src +- targetDir `{string}` 编译后路径,默认为 dist ```json { diff --git a/site/versioned_docs/version-1.0.0/injection.md b/site/versioned_docs/version-1.0.0/injection.md index 790c3ff91d02..6be268288c51 100644 --- a/site/versioned_docs/version-1.0.0/injection.md +++ b/site/versioned_docs/version-1.0.0/injection.md @@ -14,7 +14,7 @@ midway 默认使用 [injection](https://www.npmjs.com/package/injection) 这个 :::info 我们在 midway 包上做了自动导出,所以 injection 包中的模块,都能从 midway 中获取到。 -import {Container} from 'injection' 和 import {Container} from 'midway' 是一样的。 +`import {Container} from 'injection'` 和 `import {Container} from 'midway'` 是一样的。 ::: ## IoC 概览 diff --git a/site/versioned_docs/version-2.0.0/deployment.md b/site/versioned_docs/version-2.0.0/deployment.md index 0fa155073dd3..83e40195b17a 100644 --- a/site/versioned_docs/version-2.0.0/deployment.md +++ b/site/versioned_docs/version-2.0.0/deployment.md @@ -297,7 +297,7 @@ $ egg-scripts start --port=7001 --daemon --title=egg-server-showcase - `--daemon` 是否允许在后台模式,无需 nohup。若使用 Docker 建议直接前台运行。 - `--env=prod` 框架运行环境,默认会读取环境变量 process.env.EGG_SERVER_ENV, 如未传递将使用框架内置环境 prod。 - `--workers=2` 框架 worker 线程数,默认会创建和 CPU 核数相当的 app worker 数,可以充分的利用 CPU 资源。 -- `--title=egg-server-showcase` 用于方便 ps 进程时 grep 用,默认为 egg-server-${appname}。 +- `--title=egg-server-showcase` 用于方便 ps 进程时 grep 用,默认为 `egg-server-${appname}`。 - `--framework=yadan` 如果应用使用了[自定义框架](https://eggjs.org/zh-cn/advanced/framework.html),可以配置 package.json 的 egg.framework 或指定该参数。 - `--ignore-stderr` 忽略启动期的报错。 - `--https.key` 指定 HTTPS 所需密钥文件的完整路径。 diff --git a/site/versioned_docs/version-2.0.0/extensions/task.md b/site/versioned_docs/version-2.0.0/extensions/task.md index 2534f80f0d70..5596efa9c817 100644 --- a/site/versioned_docs/version-2.0.0/extensions/task.md +++ b/site/versioned_docs/version-2.0.0/extensions/task.md @@ -397,7 +397,7 @@ export class QueueTask { -这个问题基本明确,问题会出现在 redis 的集群版本上。原因是 redis 会对 key 做 hash 来确定存储的 slot,集群下这一步@midwayjs/task 的 key 命中了不同的 slot。临时的解决办法是 taskConfig 里的 prefix 配置用{}包括,强制 redis 只计算{}里的 hash,例如 prefix: '{midway-task}' +这个问题基本明确,问题会出现在 redis 的集群版本上。原因是 redis 会对 key 做 hash 来确定存储的 slot,集群下这一步@midwayjs/task 的 key 命中了不同的 slot。临时的解决办法是 taskConfig 里的 prefix 配置用{}包括,强制 redis 只计算{}里的 hash,例如 `prefix: '{midway-task}'` ### 历史日志删除 diff --git a/site/versioned_docs/version-3.0.0/aspect.md b/site/versioned_docs/version-3.0.0/aspect.md new file mode 100644 index 000000000000..f1d812958428 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/aspect.md @@ -0,0 +1,414 @@ +# 拦截器(AOP) + +我们经常有全局统一处理逻辑的需求,比如统一处理错误,转换格式等等,虽然在 Web 场景有 Web 中间件来处理,但是在其他场景下,无法使用这个能力。 + + +Midway 设计了一套通用的方法拦截器(切面),用于在不同场景中,统一编写逻辑。 + +拦截器和传统的 Web 中间件和装饰器都不同,是由 Midway 框架提供的能力,在执行顺序上,处于中间的位置,这个能力能对任意的 Class 方法做拦截。 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01DFfT1y1FC8xYeocrX_!!6000000000450-2-tps-823-133.png) + +## 使用拦截器(切面) + + +拦截器一般会放在 `src/aspect` 目录。下面我们写一个对控制器(Controller)方法拦截的示例。创建一个 `src/aspect/report.ts` 文件。 + + +``` +➜ my_midway_app tree +. +├── src +│ │── aspect ## 拦截器目录 +│ │ └── report.ts +│ └── controller ## Web Controller 目录 +│ └── home.ts +├── test +├── package.json +└── tsconfig.json +``` +```typescript +// src/controller/home.ts + +import { Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + return "Hello Midwayjs!"; + } +} +``` + + +内容如下: +```typescript +import { Aspect, IMethodAspect, JoinPoint } from '@midwayjs/core'; +import { HomeController } from '../controller/home'; + +@Aspect(HomeController) +export class ReportInfo implements IMethodAspect { + async before(point: JoinPoint) { + console.log('before home router run'); + } +} + +``` +启动项目,运行后,在控制台会输出 `before home router run` 的字样。 + + +你会发现,我们不需要去侵入控制器的代码,既没有在业务文件中加装饰器,也没有在主流程前后可见的加代码。 + + +拦截器(切面)的能力非常强大,也非常可怕,我们一定要小心而正确的使用。 + + +拦截器 **固定为单例**。 + +:::caution +在继承的情况下,拦截器不会对父类的方法生效。 +::: + + +## 可切面的生命周期 + + +方法拦截器可以对整个方法进行拦截,拦截的方式包括几个方面。 +```typescript +export interface IMethodAspect { + after?(joinPoint: JoinPoint, result: any, error: Error); + afterReturn?(joinPoint: JoinPoint, result: any): any; + afterThrow?(joinPoint: JoinPoint, error: Error): void; + before?(joinPoint: JoinPoint): void; + around?(joinPoint: JoinPoint): any; +} +``` +| 方法 | 描述 | +| --- | --- | +| before | 方法调用前执行 | +| around | 包裹方法的前后执行 | +| afterReturn | 正确返回内容时执行 | +| afterThrow | 抛出异常时执行 | +| after | 最后执行(不管正确还是错误) | + +简单理解如下; +```javascript +try { + // before + // around or invokeMethod + // afterReturn +} catch(err){ + // afterThrow +} finally { + // after +} +``` +| | 修改入参 | 调用原方法 | 获取返回值 | 修改返回值 | 获取错误 | 拦截并抛出错误 | +| --- | --- | --- | --- | --- | --- | --- | +| before | √ | √ | | | | | +| around | √ | √ | √ | √ | √ | √ | +| afterReturn | | | √ | √ | | | +| afterThrow | | | | | √ | √ | +| after | | | √ | | √ | | + + + +我们常会在 `before` 的过程中修改入参、校验,以符合程序执行的逻辑,比如: +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home(data1, data2) { + return data1 + data2; // 因为拦截了方法,这里的返回值是 3 + } +} + +// src/aspect/ +@Aspect(HomeController, 'home') // 这里只对 home 方法做拦截 +export class ReportInfo implements IMethodAspect { + async before(point: JoinPoint) { + console.log(point.args); // 这里因为对 Controller 方法做切面,原本的参数为 [ctx, next] + point.args = [1, 2]; // 修改入参 + } +} + +``` +这里的 `JoinPoint` 就是可以对方法做修改的参数,定义如下。 +```typescript +export interface JoinPoint { + methodName: string; + target: any; + args: any[]; + proceed(...args: any[]): any; +} +``` +| 参数 | 描述 | +| --- | --- | +| methodName | 拦截到的方法名 | +| target | 方法调用时的实例 | +| args | 原方法调用的参数 | +| proceed | 原方法本身,只会在 before 和 around 中存在 | + +`around` 是比较全能的方法,它可以包裹整个方法调用流程。 +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + return 'hello'; + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') // 这里只对 home 方法做拦截 +export class ReportInfo implements IMethodAspect { + async around(point: JoinPoint) { + const result = await point.proceed(...point.args); // 执行原方法 + return result + ' world'; + } +} + +``` +最终 Controller 会返回 `hello world` 。 + + +`afterReturn` 方法会多一个返回结果参数,如果只需要修改返回结果,可以直接使用它,上面的 `around` 例子用 `afterReturn` 改写会更简单。 +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + return 'hello'; + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') // 这里只对 home 方法做拦截 +export class ReportInfo implements IMethodAspect { + async afterReturn(point: JoinPoint, result) { + return result + ' world'; + } +} + +``` +`afterThrow` 用于拦截错误。 + + +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + throw new Error('custom error'); + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') +export class ReportInfo implements IMethodAspect { + async afterThrow(point: JoinPoint, error) { + if(/not found/.test(error.message)) { + throw new Error('another error'); + } else { + console.error('got custom error'); + } + } +} + +``` +`afterThrow` 能拦截错误,相应的,它不能在流程中返回结果,一般用来记录错误日志。 + + +`after` 用来做最后的处理,不管是成功或者失败,都可以用它执行一些事情,比如记录所有成功或者失败的次数。 + + +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + throw new Error('custom error'); + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') +export class ReportInfo implements IMethodAspect { + async after(point: JoinPoint, result, error) { + if(error) { + console.error(error); + } else { + console.log(result); + } + } +} + +``` + + +## 切面的异步问题 + + +如果被拦截的方法是异步的,则原则上我们的 `before` 等方法应该都是异步的,反之,则都是同步的。 +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + async home() { // 这里是异步的,则下面的 before 是异步的 + + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') +export class ReportInfo implements IMethodAspect { + async before(point: JoinPoint) { + + } +} + +``` +```typescript +// src/controller/home.ts +@Controller('/') +export class HomeController { + + @Get('/') + home() { // 这里是同步的,则下面的 before 也是同步的 + + } +} + +// src/aspect/report.ts +@Aspect(HomeController, 'home') +export class ReportInfo implements IMethodAspect { + before(point: JoinPoint) { + + } +} + +``` +## 应用到多个 Class + + +`@Aspect` 装饰器的参数可以是一个数组,我们可以提供多个 Class,这些 Class 的 **所有方法 **都将被拦截。比如,我们可以将上面的拦截器应用到多个 Controller,这样 **每一个 Class 的每一个方法 **都会被拦截。 + + +```typescript +@Aspect([HomeController, APIController]) +export class ReportInfo implements IMethodAspect { + + async before(point: JoinPoint) { + + } +} +``` + + +## 特定方法匹配 + + +一般情况下,我们只需要对某个 Class 特定的方法做拦截。我们提供了一些匹配方法的能力。 `@Aspect` 装饰的第二个参数则是一个通配方法的字符串。使用的规则为 [picomatch](https://github.com/micromatch/picomatch)。 + + +假如我们的方法为: + + +```typescript +// src/controller/home.ts + +import { Controller, Get } from "@midwayjs/core"; + +@Controller('/') +export class HomeController { + + @Get('/1') + async hello1() { + return "Hello Midwayjs!"; + } + + @Get('/2') + async hello2() { + return "Hello Midwayjs, too!"; + } +} +``` +那么,我们如下配置时,只会匹配到 `hello2` 这个方法。 +```typescript +@Aspect([HomeController], '*2') +export class ReportInfo implements IMethodAspect { + + async before(point: JoinPoint) { + console.log('hello method with suffix 2'); + } +} +``` + + +## 切面执行顺序 + + +如果多个拦截器(切面)同时针对一个方法做操作,可能会出现顺序错乱的问题,如果在两个文件中,这个顺序是随机的。 + + +`@Aspect` 的第三个参数用于指定拦截器的优先级,默认为 0,数字越大,优先级越高,即先被注册到方法上,**先注册的方法会被后调用,**即洋葱模型**。** + + +以下面的代码作为示例。 `MyAspect2` 的优先级高于 `MyAspect1` ,所以会优先注册。示意图如下,整个拦截流程分为两部分,先是注册,后是执行。 + + +**注册流程** + + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01d31RXA1cpHyjyPHCs_!!6000000003649-2-tps-924-497.png) + + +**执行流程** + + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01RXmEtD26Thmkg8eX8_!!6000000007663-2-tps-769-311.png) + + + + +代码如下。 +```typescript +@Aspect([HomeController]) +export class MyAspect1 implements IMethodAspect { + before(point: JoinPoint) { + console.log('111'); + } +} + +@Aspect([HomeController], '*', 1) // 这里可以设置优先级 +export class MyAspect2 implements IMethodAspect { + before(point: JoinPoint) { + console.log('222'); + } +} +``` +执行输出为 +``` +111 +222 +``` + + +## 一些限制 + + +- 1、拦截器不会对父类生效 diff --git a/site/versioned_docs/version-3.0.0/auto_run.md b/site/versioned_docs/version-3.0.0/auto_run.md new file mode 100644 index 000000000000..26d3573cc12d --- /dev/null +++ b/site/versioned_docs/version-3.0.0/auto_run.md @@ -0,0 +1,70 @@ +# 自执行代码 + +在初始化过程中,当我们的代码和主流程无关,却想执行的时候,一般会在启动 onReady 阶段来执行,随着的代码量越来越多,onReady 会变的臃肿。 + +比如,我们有一些需要提前执行的逻辑,一个用于监听 Redis 错误,一个用于初始化数据同步: + +```typescript +@Provide() +@Scope(ScopeEnum.Singleton) +export class RedisErrorListener { + // ... +} + +@Provide() +@Scope(ScopeEnum.Singleton) +export class DataSyncListener { + // ... +} +``` + +一般,我们会在启动时通过 `getAsync` 方法来创建实例,使其执行。 + +```typescript + + +// configuration.ts +//... + +@Configuration({ + // ... +}) +export class MainConfiguration { + async onReady(container) { + await container.getAsync(RedisErrorListerner); + await container.getAsync(DataSyncListerner); + } +} + +``` + +这样一旦代码多了,onReady 中会出现许多非必要流程的代码。 + + + +## 自初始化 + +如果代码和主流程不耦合,属于独立的逻辑,比如上述的监听一些事件,初始化数据同步等,就可以使用 @Autoload 装饰器,使某个类可以自初始化。 + +比如: + +```typescript +import { Autoload, Scope, ScopeEnum } from '@midwayjs/core'; + +@Autoload() +@Scope(ScopeEnum.Singleton) +export class RedisErrorListener { + @Init() + async init() { + const redis = new Redis(); + redis.on('xxx', () => { + // ... + }); + } +} +``` + +这样无需在 `onReady` 中使用 `getAsync` 方法即可自动初始化,并执行 init 方法。 + + + diff --git a/site/versioned_docs/version-3.0.0/awesome_midway.md b/site/versioned_docs/version-3.0.0/awesome_midway.md new file mode 100644 index 000000000000..fcd37d5ee1a4 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/awesome_midway.md @@ -0,0 +1,110 @@ +# Awesome Midway + +以下列举了与 Midwayjs 相关的优质社区项目 + +## 微服务 + +| 名称 | 作者 | 描述 | +| ---------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [@letscollab/midway-nacos][@letscollab/midway-nacos] | Nawbc | midway nacos 组件 | +| [midway-elasticsearch][midway-elasticsearch] | ddzyan | midway elasticsearch 组件 | +| [midway-apollo][midway-apollo] | helloHT | midway 携程异步动态配置 apollo 组件 | +| [@mwcp/cache][@mwcp/cache] | waitingsong | midway Cache 增强组件 支持 [`Cacheable`][Cacheable], [`CacheEvict`][CacheEvict], [`CachePut`][CachePut] 装饰器 并支持[传入泛型参数获得方法入参类型][cache-generics-cn] | +| [@mwcp/kmore][@mwcp/kmore] | waitingsong | midway 数据库组件 基于 [Knex],通过 `Transactional` 装饰器支持声明式事务,支持自动分页、智能连表,集成 [OpenTelemetry] 链路追踪 | +| [@mwcp/otel][@mwcp/otel] | waitingsong | midway [OpenTelemetry] 增强组件,协议支持 HTTP 和 [gRPC (Unary)] 支持 [`Trace`][Trace], [`TraceLog`][TraceLog], [`TraceInit`][TraceInit] 装饰器 并支持[传入泛型参数获得方法入参类型][otel-generics-cn] | +| [@mwcp/jwt][@mwcp/jwt] | waitingsong | midway JWT 增强组件 支持 [`Public`][jwt-public] 装饰器 | +| [@mwcp/paradedb][@mwcp/paradedb] | waitingsong | midway [ParadeDb] 组件。首个基于 Postgres 的 Elasticsearch 开源替代,采用 Rust 编写, 旨在提供快速的全文检索、语义检索和混合检索能力,适用于搜索场景 | +| [@mwcp/pgmq][@mwcp/pgmq] | waitingsong | midway [pqmg-js] 组件 支持 [`Consumer`][Consumer], [`PgmqListener`][PgmqListener] 装饰器, 支持事务以及事务保护的类似 MQ `Exchange` 概念的路由。 [PGMQ] 是一个基于 [PG] 数据库扩展的轻量级消息队列,原生支持消息持久化和延迟消息,类似 `AWS SQS` 或 `RSMQ` | +| [midway-throttler][midway-throttler] | larryzhuo | midway throttler 限流组件 | + +## 插件 + +| 名称 | 作者 | 描述 | +| --------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| [邮件组件][mailer-zh] | MrDotYan | midway 邮箱组件,基于nodemailer和midwayjs,以服务的形式注入控制器使用[文档(国内)][mailer-zh-doc] [文档(国外)][mailer-en-doc] | +## swagger + +| 名称 | 作者 | 描述 | +| -------------------------------------- | ----- | --------------------- | +| [midwayjs-knife4j2][midwayjs-knife4j2] | Junyi | midway swagger 新皮肤 | + +## 模板渲染 + +| 名称 | 作者 | 描述 | +| ---------------------------------------------------------- | ---------- | -------------------------------------------------------------------- | +| [yuntian001/midway-vite-view][yuntian001/midway-vite-view] | yuntian001 | midway vite 服务端渲染(ssr)/客户端渲染(client)组件 支持 vue3 react | + +## 社区示例 + +| 名称 | 作者 | 描述 | +| ---------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [midwayjs-crud][midwayjs-crud] | DeveloperYvan | 一个包含 prisma+casbin+nacos+crud 的示例 | +| [midway-practice][midway-practice] | ddzyan | 一个包含 请求日志链路,统一响应体,统一异常处理,异常过滤器 + 三大主流 ORM 模型 (sequelize,typeORM,prisma) 的示例 | +| [midway-boot][midway-boot] | 码道功臣 | 一个比较完整的后端功能最佳实践,包含:增删改查及基类封装、数据库操作、缓存操作、用户安全认证及访问安全控制、JWT 访问凭证、分布式访问状态管理、密码加解密、统一返回结果封装、统一异常管理、Snowflake 主键生成、Swagger 集成及支持访问认证、环境变量的使用、Docker 镜像构建、Serverless 发布等 | +| [midway-vue3-ssr][midway-vue3-ssr] | LiQingSong | 基于 Midway、Vue 3 组装的 SSR 框架,简单、易学易用、方便扩展、集成 Midway 框架,您一直想要的 Vue SSR 框架。 | +| [midway-learn][midway-learn] | hbsjmsjwj | 一个学习midway的demo,包含 midway3 + egg + 官方的组件&扩展(consul, jwt, typeorm, prometheus, swagger, mysql2,grpc,rabbitmq) | +| [midway-admin][midwayjs-admin] | MrDotYan | 一套GeekerAdmin+Midwayjs构建的后台管理框架 | + +## 学习资料 + +| 名称 | 作者 | 描述 | +| -------------- | -------- | --------------------------------------- | +| Midway开发实践 | 码道功臣 | https://edu.51cto.com/course/32086.html | + + +:::tip +欢迎大家为社区贡献力量, 编辑此页添加你所喜爱的高质量 midway 项目/组件 +::: + + +[midway-elasticsearch]: https://github.com/ddzyan/midway-elasticsearch +[midway-apollo]: https://github.com/helloHT/midway-apollo +[@letscollab/midway-nacos]: https://github.com/deskbtm-letscollab/midway-nacos +[@mwcp/kmore]: https://github.com/waitingsong/kmore + +[@mwcp/cache]: https://github.com/waitingsong/midway-components/tree/main/packages/cache +[Cacheable]: https://github.com/waitingsong/midway-components/blob/main/packages/cache/README.zh-CN.md#cacheable-%E8%A3%85%E9%A5%B0%E5%99%A8 +[CacheEvict]: https://github.com/waitingsong/midway-components/blob/main/packages/cache/README.zh-CN.md#cacheevict-%E8%A3%85%E9%A5%B0%E5%99%A8 +[CachePut]: https://github.com/waitingsong/midway-components/blob/main/packages/cache/README.zh-CN.md#cacheput-%E8%A3%85%E9%A5%B0%E5%99%A8 + +[@mwcp/otel]: https://github.com/waitingsong/midway-components/tree/main/packages/otel +[Trace]: https://github.com/waitingsong/midway-components/blob/main/packages/otel/README.zh-CN.md#trace-%E8%A3%85%E9%A5%B0%E5%99%A8 +[TraceLog]: https://github.com/waitingsong/midway-components/blob/main/packages/otel/README.zh-CN.md#tracelog-%E8%A3%85%E9%A5%B0%E5%99%A8 +[TraceInit]: https://github.com/waitingsong/midway-components/blob/main/packages/otel/README.zh-CN.md#traceinit-%E8%A3%85%E9%A5%B0%E5%99%A8 +[otel-generics]: https://github.com/waitingsong/midway-components/tree/main/packages/otel#auto-parameter-type-of-keygenerator-from-generics +[otel-generics-cn]: https://github.com/waitingsong/midway-components/blob/main/packages/otel/README.zh-CN.md#%E4%BB%8E%E6%B3%9B%E5%9E%8B%E5%8F%82%E6%95%B0%E8%87%AA%E5%8A%A8%E8%8E%B7%E5%8F%96%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E5%8F%82%E6%95%B0%E7%B1%BB%E5%9E%8B +[cache-generics]: https://github.com/waitingsong/midway-components/tree/main/packages/cache#auto-parameter-type-of-keygenerator-from-generics +[cache-generics-cn]: https://github.com/waitingsong/midway-components/blob/main/packages/cache/README.zh-CN.md#%E4%BB%8E%E6%B3%9B%E5%9E%8B%E5%8F%82%E6%95%B0%E8%87%AA%E5%8A%A8%E8%8E%B7%E5%8F%96%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E5%8F%82%E6%95%B0%E7%B1%BB%E5%9E%8B + +[@mwcp/jwt]: https://github.com/waitingsong/midway-components/tree/main/packages/jwt +[jwt-public]: https://github.com/waitingsong/midway-components/blob/main/packages/jwt/README.md#public-decorator + +[@mwcp/paradedb]: https://github.com/waitingsong/paradedb/tree/main/packages/mwcp-paradedb +[ParadeDB]: https://pigsty.cc/zh/blog/pg/paradedb/ + +[@mwcp/pgmq]: https://github.com/waitingsong/pgmq-js/tree/main/packages/mwcp-pgmq-js +[PGMQ]: https://tembo-io.github.io/pgmq/ +[PG]: https://pigsty.cc/zh/blog/pg/pg-eat-db-world/ +[pqmg-js]: https://github.com/waitingsong/pgmq-js/tree/main/packages/pgmq-js +[Consumer]: https://github.com/waitingsong/pgmq-js/tree/main/packages/mwcp-pgmq-js#consumer-decorator +[PgmqListener]: https://github.com/waitingsong/pgmq-js/tree/main/packages/mwcp-pgmq-js#consumer-decorator + +[midwayjs-knife4j2]: https://github.com/fangbao-0418/midway/tree/master/packages/swagger +[yuntian001/midway-vite-view]: https://github.com/yuntian001/midway-vite-view + +[midwayjs-crud]: https://github.com/developeryvan/midwayjs-crud +[midway-practice]: https://github.com/ddzyan/midway-practice +[midway-boot]: https://github.com/bestaone/midway-boot +[midway-vue3-ssr]: https://github.com/lqsong/midway-vue3-ssr +[midway-learn]: https://github.com/hbsjmsjwj/midway-learn.git +[midway-throttler]: https://github.com/larryzhuo/midway-throttler + +[Knex]: https://knexjs.org/ +[OpenTelemetry]: https://github.com/open-telemetry +[mailer-zh]:https://gitee.com/onlymry_admin/midwayjs_mailer +[mailer-zh-doc]:https://gitee.com/onlymry_admin/midwayjs_mailer/blob/main/readme.md +[mailer-en]:https://github.com/MrDotYan/midwayjs_mailer +[mailer-en-doc]:https://github.com/MrDotYan/midwayjs_mailer/blob/main/readme.md +[midwayjs-admin]:https://gitee.com/yncykj/midway-admin.git + +[gRPC (Unary)]: https://github.com/midwayjs/midway/tree/main/packages/grpc diff --git a/site/versioned_docs/version-3.0.0/built_in_service.md b/site/versioned_docs/version-3.0.0/built_in_service.md new file mode 100644 index 000000000000..a32c25a5d1f5 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/built_in_service.md @@ -0,0 +1,645 @@ +# 内置服务 + +在 Midway 中,提供了众多的内置对象,方便用户使用。 + +在本章节,我们会介绍和框架相关联的的 Application,Context 对象,Midway 默认容器上的一些服务对象,这些对象在整个业务的开发中都会经常遇到。 + +以下是一些 Midway 依赖注入容器内置的服务,这些服务由依赖注入容器初始化,在业务中全局可用。 + + + +## MidwayApplicationManager + +Midway 内置的应用管理器,可以使用它获取到所有的 Application。 + +可以通过注入获取,比如对不同的 Application 添加同一个中间件。 + +```typescript +import { MidwayApplicationManager, onfiguration, Inject } from '@midwayjs/core' +import { CustomMiddleware } from './middleware/custom.middleware'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + applicationManager: MidwayApplicationManager; + + async onReady() { + this.applicationManager + .getApplications(['koa', 'faas', 'express', 'egg']) + .forEach(app => { + app.useMiddleware(CustomMiddleware); + }); + } +} + +``` + +| API | 返回类型 | 描述 | +| ------------------------------------ | -------------------- | ------------------------------------------------------ | +| getFramework(namespace: string) | IMidwayFramework | 返回参数指定的 framework | +| getApplication(namespace: string) | IMidwayApplication | 返回参数指定的 Application | +| getApplications(namespace: string[]) | IMidwayApplication[] | 返回参数指定的多个 Application | +| getWebLikeApplication() | IMidwayApplication[] | 返回类似 Web 场景的 Application(express/koa/egg/faas) | + + + +## MidwayInformationService + +Midway 内置的信息服务,提供基础的项目数据。 + +可以通过注入获取。 + +```typescript +import { Inject, Controller, Get, MidwayInformationService } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + informationService: MidwayInformationService; + + @Get('/') + async home() { + // this.informationService.getAppDir(); + } +} +``` + +一般用来返回用户相关的目录。 + +| API | 返回类型 | 描述 | +| ------------ | -------- | ------------------------------------------------------- | +| getAppDir() | String | 返回应用根目录 | +| getBaseDir() | String | 返回应用代码目录,默认本地开发为 src,服务器运行为 dist | +| getHome | String | 返回机器用户目录,指代 ~ 的地址。 | +| getPkg | Object | 返回 package.json 的内容 | +| getRoot | String | 在开发环境,返回 appDir,在其他环境,返回 Home 目录 | + + + +## MidwayEnvironmentService + +Midway 内置的环境服务,提供环境设置和判断。 + +可以通过注入获取。 + +```typescript +import { Inject, Controller, Get, MidwayEnvironmentService } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + environmentService: MidwayEnvironmentService; + + @Get('/') + async home() { + // this.environmentService.getCurrentEnvironment(); + } +} +``` + +一般用来获取当前的环境,API 如下: + +| API | 返回类型 | 描述 | +| ------------------------ | -------- | ------------------ | +| getCurrentEnvironment() | String | 返回应用当前环境 | +| setCurrentEnvironment() | | 设置当前环境 | +| isDevelopmentEnvironment | Boolean | 判断是否是开发环境 | + + + +## MidwayConfigService + +Midway 内置的多环境配置服务,提供配置的加载和获取,它也是 `@Config` 装饰器的数据源。 + +可以通过注入获取。 + +```typescript +import { Inject, Controller, Get, MidwayConfigService } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + configService: MidwayConfigService; + + @Get('/') + async home() { + // this.configService.getConfiguration(); + } +} +``` + +一般用来获取当前的配置,API 如下: + +| API | 返回类型 | 描述 | +| ------------------ | -------- | ------------------------ | +| addObject(obj) | | 动态添加配置对象 | +| getConfiguration() | Object | 返回当前合并好的配置对象 | +| clearAllConfig() | | 清空所有配置 | + + + +## MidwayLoggerService + +Midway 内置的日志服务,提供日志创建,获取等 API,它也是 `@Logger` 装饰器的数据源。 + +可以通过注入获取。 + +```typescript +import { Inject, Controller, Get, MidwayLoggerService } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + loggerService: MidwayLoggerService; + + @Get('/') + async home() { + // this.loggerService.getLogger('logger'); + } +} +``` + +一般用来获取日志对象,API 如下: + +| API | 返回类型 | 描述 | +| ---------------------------- | -------- | ------------------------------ | +| createInstance(name, config) | ILogger | 动态创建一个 Logger 实例 | +| getLogger(name) | ILogger | 根据日志名返回一个 Logger 实例 | + + + +## MidwayFrameworkService + +Midway 内置的自定义框架服务,配合组件中自定义的 `@Framework` 标记的 Class,提供不同协议的对外服务。 + +可以通过注入获取。 + +```typescript +import { Inject, Controller, Get, MidwayFrameworkService } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + frameworkService: MidwayFrameworkService; + + @Get('/') + async home() { + // this.frameworkService.getMainFramework(); + } +} +``` + +一般用来获取 Framework 对象,API 如下: + +| API | 返回类型 | 描述 | +| --------------------------------- | ------------------ | ---------------------------------- | +| getMainFramework() | IMidwayFramework | 返回主框架实例 | +| getMainApp() | IMidwayApplication | 返回主框架中的 app 对象 | +| getFramework(nameOrFrameworkType) | IMidwayFramework | 根据框架名或者框架类型返回框架实例 | + + + +## MidwayMiddlewareService + +Midway 内置的中间件处理服务,用于自建中间件的处理。 + +Midway 内置的自定义装饰器服务,用于实现框架层面的自定义装饰器,一般在自定义框架时使用。 + +可以通过注入获取。 + +```typescript +import { Inject, Controller, Get, MidwayMiddlewareService } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + middlewareService: MidwayMiddlewareService; + + @Get('/') + async home() { + // this.middlewareService.compose(/** 省略 **/); + } +} +``` + +API 如下: + +| API | 返回类型 | 描述 | +| -------------------------------- | ----------- | -------------------------------------------- | +| compose(middlewares, app, name?) | IMiddleawre | 将多个中间件数组组合到一起返回一个新的中间件 | + + + +## MidwayDecoratorService + +Midway 内置的自定义装饰器服务,用于实现框架层面的自定义装饰器。 + +可以通过注入获取。 + +```typescript +import { Inject, Controller, Get, MidwayDecoratorService } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + decoratorService: MidwayDecoratorService; + + @Get('/') + async home() { + // this.decoratorService.registerPropertyHandler(/** 省略 **/); + } +} +``` + +API 如下: + +| API | 返回类型 | 描述 | +| ----------------------------------------------- | -------- | ---------------------- | +| registerPropertyHandler(decoratorKey, handler) | | 添加一个属性装饰器实现 | +| registerMethodHandler(decoratorKey, handler) | | 添加一个方法装饰器实现 | +| registerParameterHandler(decoratorKey, handler) | | 添加一个参数装饰器实现 | + +具体示例,请参考 **自定义装饰器** 部分。 + + + +## MidwayAspectService + +Midway 内置的拦截器服务,用于加载 `@Aspect` 相关的能力,自定义装饰器也使用了该服务。 + +可以通过注入获取。 + +```typescript +import { Inject, Controller, Get, MidwayAspectService } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + aspectService: MidwayAspectService; + + @Get('/') + async home() { + // this.aspectService.interceptPrototypeMethod(/** 省略 **/); + } +} +``` + +API 如下: + +| API | 返回类型 | 描述 | +| ------------------------------------------------------------------------ | -------- | ---------------------------------------- | +| addAspect(aspectInstance, aspectData) | | 添加一个拦截器实现 | +| interceptPrototypeMethod(Clazz, methodName, aspectObject: IMethodAspect) | | 拦截原型上的方法,将拦截器的实现添加上去 | + + + +## MidwayLifeCycleService + +Midway 内置的生命周期运行服务,用于运行 `configuration` 中的生命周期。 + +该服务均为内部方法,用户无法直接使用。 + + + +## MidwayMockService + +Midway 内置的数据模拟服务,用于在开发和单测时模拟数据。 + +可以通过注入获取。 + +```typescript +import { Inject, Controller, Get, MidwayMockService } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + mockService: MidwayMockService; + + @Get('/') + async home() { + // this.mockService.mockProperty(/** 省略 **/); + } +} +``` + +API 如下 + +| API | 返回类型 | 描述 | +| -------------------------------------------- | -------- | ---------------------------------- | +| mockClassProperty(clzz, propertyName, value, group?) | | mock 一个 class 上的属性(方法),支持分组,默认分组为 `default` | +| mockProperty(obj, key, value, group?) | | mock 一个普通对象上的属性(方法),支持分组,默认分组为 `default` | +| mockContext(app, key, value, group?) | | mock 上下文对象上的属性,支持分组,默认分组为 `default` | +| restore(group?) | | 恢复指定分组的 mock 数据,未指定则恢复所有 | +| restoreAll() | | 清空所有 mock 数据 | + +### mockClassProperty + +用于模拟类的某个属性或者方法。支持通过 `group` 参数指定分组。如果不传 `group` 参数,默认使用 `default` 分组。 + +```typescript +@Provide() +export class UserService { + data; + + async getUser() { + return 'hello'; + } +} +``` + +我们也可以在代码中模拟。 + +```typescript + +import { MidwayMockService, Provide, Inject } from '@midwayjs/core'; + +@Provide() +class TestMockService { + @Inject() + mockService: MidwayMockService; + + mock() { + // 模拟属性,使用默认分组 + this.mockService.mockClassProperty(UserService, 'getUser', async () => { + return 'midway'; + }); + + // 模拟属性,指定分组 + this.mockService.mockClassProperty(UserService, 'data', { + bbb: '1' + }, 'group2'); + } +} +``` + + + +### mockProperty + +使用 `mockProperty` 方法来模拟对象的属性。支持通过 `group` 参数指定分组。 + +```typescript +import { MidwayMockService, Provide, Inject } from '@midwayjs/core'; + +@Provide() +class TestMockService { + @Inject() + mockService: MidwayMockService; + + mock() { + const a = {}; + // 默认分组 + this.mockService.mockProperty(a, 'name', 'hello'); + // 模拟属性,自定义分组 + this.mockService.mockProperty(a, 'name', 'hello', 'group1'); + // a['name'] => 'hello' + + // 模拟方法 + this.mockService.mockProperty(a, 'getUser', async () => { + return 'midway'; + }, 'group2'); + // await a.getUser() => 'midway' + } +} + +``` + + + +### mockContext + +由于 Midway 的 Context 和 app 关联,所以在模拟的时候需要传入 app 实例。支持通过 `group` 参数指定分组。 + +使用 `mockContext` 方法来模拟上下文。 + +```typescript +import { MidwayMockService, Configuration, App } from '@midwayjs/core'; + +@Configuration(/**/) +export class MainConfiguration { + @Inject() + mockService: MidwayMockService; + + @App() + app; + + async onReady() { + // 模拟上下文, 默认分组 + this.mockService.mockContext(app, 'user', 'midway'); + // 自定义分组 + this.mockService.mockContext(app, 'user', 'midway', 'group1'); + } +} + +// ctx.user => midway +``` + +如果你的数据比较复杂,或者带有逻辑,也可以使用回调形式。 + +```typescript +import { MidwayMockService, Configuration, App } from '@midwayjs/core'; + +@Configuration(/**/) +export class MainConfiguration { + @Inject() + mockService: MidwayMockService; + + @App() + app; + + async onReady() { + // 模拟上下文 + this.mockService.mockContext(app, (ctx) => { + ctx.user = 'midway'; + }, 'group2'); + } +} + +// ctx.user => midway +``` + +注意,这个 mock 行为是在所有中间件之前执行。 + + + +## MidwayWebRouterService + +Midway 内置的路由表服务,用于应用路由和函数的创建。 + +可以通过注入获取。 + +```typescript +import { MidwayWebRouterService, Configuration, Inject } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + webRouterService: MidwayWebRouterService; + + async onReady() { + this.webRouterService.addRouter(async (ctx) => { + return 'hello world'; + }, { + url: '/', + requestMethod: 'GET', + }); + } +} + +``` + +API 如下 + +| API | 返回类型 | 描述 | +| ------------------------------------------------- |--------------------------------------| -------------------------------------- | +| addController(controllerClz, controllerOption) | | 动态添加一个 Controller | +| addRouter(routerFunction, routerInfoOption) | | 动态添加一个路由函数 | +| getRouterTable() | Promise\> | 获取带层级的路由 | +| getFlattenRouterTable() | Promise\ | 获取扁平化路由列表 | +| getRoutePriorityList() | Promise\ | 获取路由前缀列表 | +| getMatchedRouterInfo(url: string, method: string) | Promise\ | 根据访问的路径,返回当前匹配的路由信息 | + +更多使用请参考 [Web 路由表](#router_table)。 + + + +## MidwayServerlessFunctionService + +Midway 内置的函数信息服务,继承与 `MidwayWebRouterService` ,方法几乎相同。 + +可以通过注入获取。 + +```typescript +import { MidwayServerlessFunctionService, Configuration, Inject } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + serverlessFunctionService: MidwayServerlessFunctionService; + + async onReady() { + this.serverlessFunctionService.addServerlessFunction(async (ctx, event) => { + return 'hello world'; + }, { + type: ServerlessTriggerType.HTTP, + metadata: { + method: 'get', + path: '/api/hello' + }, + functionName: 'hello', + handlerName: 'index.hello', + }); + } +} + +``` + +API 如下 + +| API | 返回类型 | 描述 | +| ---------------------------------------------------------- |--------------------------| ---------------- | +| addServerlessFunction(fn, triggerOptions, functionOptions) | | 动态添加一个函数 | +| getFunctionList() | Promise\ | 获取所有函数列表 | + +更多使用请参考 [Web 路由表](#router_table)。 + + + +## MidwayHealthService + +Midway 内置的健康检查执行服务,用于外部扩展的健康检查能力。 + +完整的健康检查包含两个部分: + +* 1、健康检查的触发端,比如外部的定时请求,通常为一个 Http 接口 +* 2、健康检查的执行端,一般在各个组件或者业务中,检查特定的项是否正常 + +`MidwayHealthService` 一般用于健康检查的触发端,下面描述的内容一般在触发端会实现。 + +可以通过注入获取后,执行健康检查任务。 + +```typescript +import { MidwayHealthService ,Configuration, Inject } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + healthService: MidwayHealthService; + + async onServerReady() { + setInterval(() => { + const results = await this.healthService.getStatus(); + + // console.log(results); + // => + // { + // "status": false + // "namespace": "redis", + // "reason": "health check timeout", + // "results": [ + // { + // "status": false + // "reason": "health check timeout", + // "namespace": "redis" + // } + // ] + // } + + }, 1000); + // ... + } +} +``` + +API 如下 + +| API | 返回类型 | 描述 | +| -------------------------------- |--------------------------| ---------------- | +| getStatus() | Promise\ | 动态添加一个函数 | +| setCheckTimeout(timeout: number) | void | 设置超时时间 | + +`getStatus` 方法用于外部调用轮询 `configuration` 中的 `onHealthCheck` 方法,返回一个符合 `HealthResults` 结构的数据。 + + `HealthResults` 包含几个字段,`status` 表示本次检查是否成功, 如果失败,`reason` 表示本次第一个失败组件的原因,`namespace` 代表第一个失败的组件名, `results` 则表示本次检查所有的返回项内容,返回项的结构和外部相同。 + +在执行过程时,如果 `onHealthCheck` 方法出现下列的情况,都会标记为失败。 + +* 1、未返回符合 `HealthResult` 结构的数据 +* 2、未返回值 +* 3、执行超时 +* 4、抛出错误 +* 5、返回符合 `HealthResult` 结构的代表错误的数据,比如 `{status: false}` + +健康检查默认等待超时时间 1s。 + +可以使用全局的配置进行覆盖。 + +```typescript +// config.default +export default { + core: { + healthCheckTimeout: 2000, + } +}; +``` + +健康检查的执行端在业务或者组件的生命周期中实现,具体请查看 [生命周期](/docs/lifecycle#onhealthcheck)。 + + diff --git a/site/versioned_docs/version-3.0.0/change_start_dir.md b/site/versioned_docs/version-3.0.0/change_start_dir.md new file mode 100644 index 000000000000..9a8b32744295 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/change_start_dir.md @@ -0,0 +1,119 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 修改源码目录 + +在某些特殊场景下,可以修改源码所在的 `src` 目录。 + + +一些限制: + +- 1、@midwayjs/web(egg)egg 由于目录固定,无法修改 +- 2、只在纯 node 项目下测试通过(非一体化) + +## 源码目录的修改 + +下面,我们以将 `src` 目录修改为 `server` 为例。 + +### dev 开发 + +`package.json` 中的 dev 命令需要增加源码目录,方便 dev 查找。 + + + + + +默认可以识别 `tsconfig.json` 中的 `outDir` 字段,无需调整。 + + + + + +```typescript +"dev": "cross-env NODE_ENV=local midway-bin dev --sourceDir=./server --ts", +``` + + + + + +### build 编译 + + + + + +默认可以识别 `tsconfig.json` 中的 `outDir` 字段,无需调整。 + + + + + +为了让 tsc 编译能找到源码目录,需要修改 `tsconfig.json` ,增加 `rootDir` 字段。 + +```typescript +{ + "compileOnSave": true, + "compilerOptions": { + // ... + "rootDir": "server" + }, +} +``` + +这样,开发和编译就都正常了。 + + + + + + + + +## 编译目录的修改 + +编译目录影响到部署,也可以修改。我们以将 `dist` 目录修改为 `build` 为例。 + +### build 编译 + +修改 `tsconfig.json` 中的 `outDir` 字段。 + +```typescript +{ + "compileOnSave": true, + "compilerOptions": { + // ... + "outDir": "build" + }, + "exclude": { + "build", + //... + } +} +``` + +这样编译就正常了。 + + +### bootstrap 启动 + + +编译目录修改之后,线上部署会找不到代码,所以如果走 `bootstrap.js` 启动,需要修改代码。 + +```typescript +// bootstrap.js + +const { join } = require('path'); +const { Bootstrap } = require('@midwayjs/bootstrap'); + +//... + +// 需要用 configure 方法配置 baseDir +Bootstrap + .configure({ + baseDir: join(__dirname, 'build'), + }) + .run(); +``` + +对 `Bootstrap` 配置入口目录即可。 diff --git a/site/versioned_docs/version-3.0.0/component_development.md b/site/versioned_docs/version-3.0.0/component_development.md new file mode 100644 index 000000000000..1866d37512ea --- /dev/null +++ b/site/versioned_docs/version-3.0.0/component_development.md @@ -0,0 +1,718 @@ +# 自定义组件 + +组件(Component)是一个可复用与多框架的模块包,一般用于几种场景: + +- 1、包装往下游调用的代码,包裹三方模块简化使用,比如 orm(数据库调用),swagger(简化使用) 等 +- 2、可复用的业务逻辑,比如抽象出来的公共 Controller,Service 等 + +组件可以本地加载,也可以打包到一起发布成一个 npm 包。组件可以在 midway v3/Serverless 中使用。你可以将复用的业务代码,或者功能模块都放到组件中进行维护。几乎所有的 Midway 通用能力都可以在组件中使用,包括但不限于配置,生命周期,控制器,拦截器等。 + +设计组件的时候尽可能的面向所有的上层框架场景,所以我们尽可能只依赖 `@midwayjs/core` 。 + +从 v3 开始,框架(Framework)也变为组件的一部分,使用方式和组件保持统一。 + + + +## 开发组件 + +### 脚手架 + +只需执行下面的脚本,模板列表中选择 `component-v3` 模板,即可快速生成示例组件。 + +```bash +$ npm init midway@latest -y +``` + +注意 [Node.js 环境要求](/docs/intro#环境准备工作)。 + + + +### 组件目录 + +组件的结构和 midway 的推荐目录结构一样,组件的目录结构没有特别明确的规范,和应用或者函数保持一致即可。简单的理解,组件就是一个 “迷你应用"。 + +一个推荐的组件目录结构如下。 + +``` +. +├── package.json +├── src +│ ├── index.ts // 入口导出文件 +│ ├── configuration.ts // 组件行为配置 +│ └── service // 逻辑代码 +│ └── bookService.ts +├── test +├── index.d.ts // 组件扩展定义 +└── tsconfig.json +``` + +对于组件来说,唯一的规范是入口导出的 `Configuration` 属性,其必须是一个带有 `@Configuration` 装饰器的 Class。 + +一般来说,我们的代码为 TypeScript 标准目录结构,和 Midway 体系相同。 + +同时,又是一个普通的 Node.js 包,需要使用 `src/index.ts` 文件作为入口导出内容。 + +下面,我们以一个非常简单的示例来演示如何编写一个组件。 + + + +### 组件生命周期 + +和应用相同,组件也使用 `src/configuration.ts` 作为入口启动文件(或者说,应用就是一个大组件)。 + +其中的代码和应用完全相同。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; + +@Configuration({ + namespace: 'book' +}) +export class BookConfiguration { + async onReady() { + // ... + } +} +``` + +唯一不同的是,你需要加一个 `namespace` 作为组件的命名空间。 + +每个组件的代码是一个独立的作用域,这样即使导出同名的类,也不会和其他组件冲突。 + +和整个 Midway 通用的 [生命周期扩展](lifecycle) 能力相同。 + + + +### 组件逻辑代码 + +和应用相同,编写类导出即可,由依赖注入容器负责管理和加载。 + +```typescript +// src/service/book.service.ts +import { Provide } from '@midwayjs/core'; + +@Provide() +export class BookService { + async getBookById() { + return { + data: 'hello world', + } + } +} +``` + +:::info +一个组件不会依赖明确的上层框架,为了达到在不同场景复用的目的,只会依赖通用的 `@midwayjs/core`。 +::: + + + +### 组件配置 + +配置和应用相同,参考 [多环境配置](env_config)。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as DefaultConfig from './config/config.default'; +import * as LocalConfig from './config/config.local'; + +@Configuration({ + namespace: 'book', + importConfigs: [ + { + default: DefaultConfig, + local: LocalConfig + } + ] +}) +export class BookConfiguration { + async onReady() { + // ... + } +} +``` + +在 v3 有一个重要的特性,组件在加载后,`MidwayConfig` 定义中就会包含该组件配置的定义。 + +为此,我们需要独立编写配置的定义。 + +在根目录下的 `index.d.ts` 中增加配置定义。 + +```typescript +// 由于修改了默认的类型导出位置,需要额外导出 dist 下的类型 +export * from './dist/index'; + +// 标准的扩展声明 +declare module '@midwayjs/core/dist/interface' { + + // 将配置合并到 MidwayConfig 中 + interface MidwayConfig { + book?: { + // ... + }; + } +} + +``` + +同时,组件的 `package.json` 也有对应的修改。 + +```json +{ + "name": "****", + "main": "dist/index.js", + "typings": "index.d.ts", // 这里的类型导出文件使用项目根目录的 + // ... + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "index.d.ts" // 发布时需要额外带上这个文件 + ], +} +``` + + + +### 组件约定 + +组件和应用本身略微有些不同,差异主要在以下几个方面。 + +- 1、组件的代码需要导出一个 `Configuration` 属性,其必须是一个带有 `@Configuration` 装饰器的 Class,用于配置组件自身能力 +- 2、所有 **显式导出的代码 **才会被依赖注入容器加载,简单来说,所有 **被装饰器装饰** 的类都需要导出,包括控制器,服务,中间件等等 + +比如: + +```typescript +// src/index.ts +export { BookConfiguration as Configuration } from './configuration'; +export * from './service/book.service'; +``` + +:::info +这样项目中只有 `service/book.service.ts` 这个文件才会被依赖注入容器扫描和加载。 +::: + +以及在 `package.json` 中指定 main 路径。 + +```typescript +"main": "dist/index" +``` + +这样组件就可以被上层场景依赖加载了。 + + + +### 测试组件 + +测试单独某个服务,可以通过启动一个空的业务,指定当前组件来执行。 + +```typescript +import { createLightApp } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + const app = await createLightApp('', { + imports: [ + custom + ] + }); + const bookService = await app.getApplicationContext().getAsync(custom.BookService); + expect(await bookService.getBookById()).toEqual('hello world'); + }); +}); + +``` + +如果组件是 Http 协议流程中的一部分,强依赖 context,必须依赖某个 Http 框架,那么,请使用一个完整的项目示例,使用 `createApp` 来测试。 + +```typescript +import { createApp, createHttpRequest } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + // 在示例项目中,需要自行依赖 @midwayjs/koa 或其他对等框架 + const app = await createApp(join(__dirname, 'fixtures/base-app'), { + imports: [ + custom + ] + }); + + const result = await createHttpRequest(app).get('/'); + // ... + + }); +}); + + +``` + + + +### 依赖其他组件 + +如果组件依赖另一个组件中的类,和应用相同,需要在入口处声明,框架会按照模块顺序加载并处理重复的情况。 + +如果明确依赖某个组件中的类,那么必然是该组件的强依赖。 + +比如: + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as axios from '@midwayjs/axios'; + +@Configuration({ + namespace: 'book', + imports: [axios] +}) +export class BookConfiguration { + async onReady() { + // ... + } +} +``` + +还有一种弱依赖的情况,无需显式声明,但是需要额外的判断。 + +```typescript +// src/configuration.ts +import { Configuration, IMidwayContainer } from '@midwayjs/core'; + +@Configuration({ + namespace: 'book', +}) +export class BookConfiguration { + async onReady(container: IMidwayContainer) { + // ... + if (container.hasNamespace('axios')) { + // 当 axios 组件被加载时才执行 + } + // ... + } +} +``` + +增加依赖。 + +```json +// package.json +{ + "dependencies": { + "@midwayjs/axios": "xxxx" + } +} +``` + +在根目录下的 `index.d.ts` 中增加显式导入依赖的组件定义。 + +```typescript +// 显式导入依赖的组件 +import '@midwayjs/axios'; +export * from './dist/index'; + +// ... + +``` + +:::tip + +如果主应用不显式依赖 axios,代码执行是正常的,但是 typescript 的 axios 的定义不会被扫描到,导致编写配置时没有 axios 定义,上述代码可以解决这个问题。 + +::: + + +### 应用中开发组件 + +推荐使用 [lerna](https://github.com/lerna/lerna),以及开启 lerna 的 hoist 模式来编写组件。如果想在非 lerna 的场景场景下开发组件,请保证组件在 `src` 目录下,否则会出现加载失败的情况。 + +#### 使用 lerna + +使用 lerna 开发相对比较简单,具体的目录结构类似如下。 + +``` +. +├── src +├── packages/ +│ ├── component-A +│ │ └── package.json +│ ├── component-B +│ │ └── package.json +│ ├── component-C +│ │ └── package.json +│ └── web +│ └── package.json +├── lerna.json +└── package.json +``` + +#### 非 lerna + +下面是一种常见的组件开发方式,示例结构为在应用代码开发时同时开发两个组件,当然,你也可以自定义你喜欢的目录结构。 + +``` +. +├── package.json +├── src // 源码目录 +│ ├── components +│ │ ├── book // book 组件代码 +│ │ │ ├── src +│ │ │ │ ├── service +│ │ │ │ │ └── bookService.ts +│ │ │ │ ├── configuration.ts +│ │ │ │ └── index.ts +│ │ │ └── package.json +│ │ │ +│ │ └── school +│ │ ├── src +│ │ │ ├── service // school 组件代码 +│ │ │ │ └── schoolService.ts +│ │ │ └── configuration.ts +│ │ └── package.json +│ │ +│ ├── configuration.ts // 应用行为配置文件 +│ └── controller // 应用路由目录 +├── test +└── tsconfig.json +``` + +组件行为配置。 + +```typescript +// src/components/book/src/bookConfiguration.ts +import { Configuration } from '@midwayjs/core'; + +@Configuration() +export class BookConfiguration {} +``` + +为了让组件能导出,我们需要在组件的入口 `src/components/book/src/index.ts` 导出 `Configuration` 属性。 + +```typescript +// src/components/book/src/index.ts +export { BookConfiguration as Configuration } from './bookConfiguration/src`; + +``` + +:::info +注意,这里引用的地方是 "./xxxx/src",是因为一般我们 package.json 中的 main 字段指向了 dist/index,如果希望代码不修改,那么 main 字段要指向 src/index,且在发布时记得修改回 dist。 + +将组件引入的目录指向 src ,是为了能在保存是自动生效(重启)。 +::: + +另外,在新版本可能会出现扫描冲突的问题。可以将 `configuration.ts` 中的依赖注入冲突检查功能关闭。 + + + +### 使用组件 + +在任意的 midway 系列的应用中,可以通过同样的方式引入这个组件。 + +首先,在应用中加入依赖。 + +```json +// package.json +{ + "dependencies": { + "midway-component-book": "*" + } +} +``` + +然后,在应用(函数)中引入这个组件。 + +```typescript +// 应用或者函数的 src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as book from 'midway-component-book'; + +@Configuration({ + imports: [book], +}) +export class MainConfiguration {} +``` + +至此,我们的准备工作就做完了,下面开始使用。 + +直接引入组件的类注入。 + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { BookService } from 'midway-component-book'; + +@Provide() +export class Library { + + @Inject(); + bookService: BookService; + +} +``` + +其余如果组件有包含特定的能力,请参考组件本身的文档。 + + + +### 组件发布 + +组件就是一个普通 Node.js 包,编译后发布到 npm 分发即可。 + +```bash +## 编译并发布对应的component +$ npm run build && npm publish +``` + + + +### 组件示例 + +[这里](https://github.com/czy88840616/midway-test-component) 有一个组件示例。已经发布到 npm,可以尝试直接引入到项目中启动执行。 + + + +## 开发框架(Framework) + +在 v3 中,组件可以包含一个 Framework,来提供不同的服务,利用生命周期,我们可以扩展提供 gRPC,Http 等协议。 + +这里的 Framework 只是组件里的一个特殊业务逻辑文件。 + +比如: + +``` +. +├── package.json +├── src +│ ├── index.ts // 入口导出文件 +│ ├── configuration.ts // 组件行为配置 +│ └── framework.ts // 框架代码 +│ +├── test +├── index.d.ts // 组件扩展定义 +└── tsconfig.json +``` + + + + + +### 扩展现有 Framework + +上面提到,Framework 是组件的一部分,同时也遵循组件规范,是可以注入以及扩展的。 + +我们以扩展 `@midwayjs/koa` 举例。 + +首先创建一个自定义组件,和普通应用相同,由于需要扩展 `@midwayjs/koa` ,那么在组件中,我们需要依赖 `@midwayjs/koa` 。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + namespace: 'myKoa', + imports: [koa] +}) +export class MyKoaConfiguration { + async onReady() { + // ... + } +} +``` + +随后,我们就可以注入 `@midwayjs/koa` 导出的 Framework,来做扩展了。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + namespace: 'myKoa', + imports: [koa] +}) +export class MyKoaConfiguration { + @Inject() + framework: koa.Framework; + + async onReady() { + // 添加中间件,koa 中的 app.useMiddleware 其实代理了 framework 上的方法 + this.framework.useMiddleware(/* ... */); + + // 添加过滤器,koa 中的 app.useFilter 其实代理了 framework 上的方法 + this.framework.useFilter(/* ... */); + + // koa 自身的扩展能力,比如扩展 context + const app = this.framework.getApplication(); + Object.defineProperty(app.context, 'user', { + get() { + // ... + return 'xxx'; + }, + enumerable: true, + }); + // ... + } + + async onServerReady() { + const server = this.framework.getServer(); + // server.xxxx + } +} +``` + +这是一种基于现有 Framework 去扩展的一种方法。 + +- 如果组件中扩展了 context,那么请参考 [扩展上下文定义](./context_definition) +- 如果组件中扩展了配置,那么请参考 [组件配置](#组件配置) + +等组件发布后,比如叫 `@midwayjs/my-koa`,业务可以直接使用你的组件,而无需引入 `@midwayjs/koa` 。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +// 你自己的组件 +import * as myKoa from '@midwayjs/my-koa'; + +@Configuration({ + imports: [myKoa], +}) +export class MyConfiguration { + async onReady() { + // ... + } +} +``` + +如果希望完全定义自己的组件,比如不同的协议,就需要完整自定义 Framework。 + + + +### 编写 Framework + +框架都遵循 `IMidwayFramewok` 的接口定义,以及如下约定。 + +- 每个框架有要自定义独立的启停流程 +- 每个框架需要定义自己独立的 `Application` ,`Context` +- 每个框架可以有自己独立的中间件能力 + +为了简化开发,Midway 提供了一个基础的 `BaseFramework` 类供继承。 + +```typescript +import { Framework, BaseFramework, IConfigurationOptions, IMidwayApplication, IMidwayContext } from '@midwayjs/core'; + +// 定义 Context +export interface Context extends IMidwayContext { + // ... +} + +// 定义 Application +export interface Application extends IMidwayApplication { + // ... +} + +// 框架的配置 +export interface IMidwayCustomConfigurationOptions extends IConfigurationOptions { + // ... +} + +// 实现一个自定义框架,继承基础框架 +@Framework() +export class MidwayCustomFramework extends BaseFramework { + + // 处理初始化配置 + configure() { + // ... + } + + // app 初始化 + async applicationInitialize() { + // ... + } + + // 框架启动,比如 listen + async run() { + // ... + } +} +``` + + + +### 自定义示例 + +接下去我们会以实现一个基础的 HTTP 服务框架作为示例。 + +```typescript +import { BaseFramework, IConfigurationOptions, IMidwayApplication, IMidwayContext } from '@midwayjs/core'; +import * as http from 'http'; + +// 定义一些上层业务要使用的定义 +export interface Context extends IMidwayContext {} + +export interface Application extends IMidwayApplication {} + +export interface IMidwayCustomConfigurationOptions extends IConfigurationOptions { + port: number; +} + +// 实现一个自定义框架,继承基础框架 +export class MidwayCustomHTTPFramework extends BaseFramework { + + configure(): IMidwayCustomConfigurationOptions { + return this.configService.getConfiguration('customKey'); + } + + async applicationInitialize(options: Partial) { + // 创建一个 app 实例 + this.app = http.createServer((req, res) => { + // 创建请求上下文,自带了 logger,请求作用域等 + const ctx = this.app.createAnonymousContext(); + // 从请求上下文拿到注入的服务 + ctx.requestContext + .getAsync('xxxx') + .then((ins) => { + // 调用服务 + return ins.xxx(); + }) + .then(() => { + // 请求结束 + res.end(); + }); + }); + + // 给 app 绑定上 midway 框架需要的一些方法,比如 getConfig, getLogger 等。 + this.defineApplicationProperties(); + } + + async run() { + // 启动的参数,这里只定义了启动的 HTTP 端口 + if (this.configurationOptions.port) { + new Promise((resolve) => { + this.app.listen(this.configurationOptions.port, () => { + resolve(); + }); + }); + } + } +} +``` + +我们定义了一个 `MidwayCustomHTTPFramework` 类,继承了 `BaseFramework` ,同时实现了 `applicationInitialize` 和 `run` 方法。 + +这样,一个最基础的框架就完成了。 + +最后,我们只要按照约定,将 Framework 导出即可。 + +```typescript +export { + Application, + Context, + MidwayCustomHTTPFramework as Framework, + IMidwayCustomConfigurationOptions, +} from './custom'; +``` + +上面是一个最简单的框架示例。事实上,Midway 所有的框架都是这么编写的。 diff --git a/site/versioned_docs/version-3.0.0/container.md b/site/versioned_docs/version-3.0.0/container.md new file mode 100644 index 000000000000..60ce90d9aaeb --- /dev/null +++ b/site/versioned_docs/version-3.0.0/container.md @@ -0,0 +1,1265 @@ +# 依赖注入 + +Midway 中使用了非常多的依赖注入的特性,通过装饰器的轻量特性,让依赖注入变的优雅,从而让开发过程变的便捷有趣。 + + +依赖注入是 Java Spring 体系中非常重要的核心,我们用简单的做法讲解这个能力。 + + +我们举个例子,以下面的函数目录结构为例。 + + +``` +. +├── package.json +├── src +│ ├── controller # 控制器目录 +│ │ └── user.controller.ts +│ └── service # 服务目录 +│ └── user.service.ts +└── tsconfig.json +``` + + +在上面的示例中,提供了两个文件, `user.controller.ts` 和 `user.service.ts` 。 + +:::tip +下面的示例,为了展示完整的功能,我们会写完整的 `@Provide` 装饰器,而在实际使用中,如果有其他装饰器(比如 `@Controller` )的情况下, `@Provide` 可以被省略。 +::: + + +为了解释方便,我们将它合并到了一起,内容大致如下。 + + +```typescript +import { Provide, Inject, Get } from '@midwayjs/core'; + +// user.controller.ts +@Provide() // 实际可省略 +@Controller() +export class UserController { + + @Inject() + userService: UserService; + + @Get('/') + async get() { + const user = await this.userService.getUser(); + console.log(user); // world + } +} + +// user.service.ts +@Provide() +export class UserService { + async getUser() { + return 'world'; + } +} + +``` + +抛开所有装饰器,你可以看到这是标准的 Class 写法,没有其他多余的内容,这也是 Midway 体系的核心能力,依赖注入最迷人的地方。 + +`@Provide` 的作用是告诉 **依赖注入容器**,我需要被容器所加载。 `@Inject` 装饰器告诉容器,我需要将某个实例注入到属性上。 + +通过这两个装饰器的搭配,我们可以方便的在任意类中拿到实例对象,就像上面的 `this.userService` 。 + +**注意**:实际使用中,如果有其他装饰器(比如 `@Controller` )的情况下 `@Provide` 经常被省略。 + + + +## 依赖注入原理 + + +我们以下面的伪代码举例,在 Midway 体系启动阶段,会创建一个依赖注入容器(MidwayContainer),扫描所有用户代码(src)中的文件,将拥有 `@Provide` 装饰器的 Class,保存到容器中。 + + +```typescript +/***** 下面为 Midway 内部代码 *****/ + +const container = new MidwayContainer(); +container.bind(UserController); +container.bind(UserService); + +``` + +这里的依赖注入容器类似于一个 Map。Map 的 key 是类对应的标识符(比如 **类名的驼峰形式字符串**),Value 则是 **类本身**。 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01qRbFaS1dETlDbbrsl_!!6000000003704-2-tps-623-269.png) + + +在请求时,会动态实例化这些 Class,并且处理属性的赋值,比如下面的伪代码,很容易理解。 + + +```typescript +/***** 下面为依赖注入容器伪代码 *****/ +const userService = new UserService(); +const userController = new UserController(); + +userController.userService = userService; +``` + + +经过这样,我们就能拿到完整的 `userController` 对象了,实际的代码会稍微不一样。 + + +MidwayContainer 有 `getAsync` 方法,用来异步处理对象的初始化(很多依赖都是有异步初始化的需求),自动属性赋值,缓存,返回对象,将上面的流程合为同一个。 + + +```typescript +/***** 下面为依赖注入容器内部代码 *****/ + +// 自动 new UserService(); +// 自动 new UserController(); +// 自动赋值 userController.userService = await container.getAsync(UserService); + +const userController = await container.getAsync(UserController); +await userController.handler(); // output 'world' +``` + + +以上就是依赖注入的核心过程,创建实例。 + + +:::info +此外,这里还有一篇名为 [《这一次,教你从零开始写一个 IoC 容器》](https://mp.weixin.qq.com/s/g07BByYS6yD3QkLsA7zLYQ)的文章,欢迎扩展阅读。 +::: + + + +## 依赖注入作用域 + + +默认的未指定或者未声明的情况下,所有的 `@Provide` 出来的 Class 的作用域都为 **请求作用域**。这意味着这些 Class ,会在**每一次请求第一次调用时被实例化(new),请求结束后实例销毁。**我们默认情况下的控制器(Controller)和服务(Service)都是这种作用域。 + +在 Midway 的依赖注入体系中,有三种作用域。 + +| 作用域 | 描述 | +| --------- | ------------------------------------------------------------ | +| Singleton | 单例,全局唯一(进程级别) | +| Request | **默认**,请求作用域,生命周期绑定 **请求链路**,实例在请求链路上唯一,请求结束立即销毁 | +| Prototype | 原型作用域,每次调用都会重复创建一个新的对象 | + +不同的作用域有不同的作用,**单例 **可以用来做进程级别的数据缓存,或者数据库连接等只需要执行一次的工作,同时单例由于全局唯一,只初始化一次,所以调用的时候速度比较快。而 **请求作用域 **则是大部分需要获取请求参数和数据的服务的选择,**原型作用域 **使用比较少,在一些特殊的场景下也有它独特的作用。 + + + +### 配置作用域 + + +如果我们需要将一个对象定义为其他两种作用域,需要额外的配置。Midway 提供了 `@Scope` 装饰器来定义一个类的作用域。下面的代码就将我们的 user 服务变成了一个全局唯一的实例。 + + +```typescript +// service +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class UserService { + //... +} +``` + +:::info + +注意,所有的入口类,比如 Controller,均为请求作用域,不支持修改。大部分情况下,只需要调整 Service 即可。 + +::: + + + +### 单例作用域 + +在显式配置后,某个类的作用域就可以变成单例作用域。。 + +```typescript +// service +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class UserService { + //... +} + +``` + +后续不管获取这个类的实例多少次,在 **同一个进程下**,都是同一个实例。 + +比如基于上面的单例服务,下面两个注入的 `userService` 属性是同一个实例: + +```typescript +@Provide() +export class A { + + @Inject() + userService: UserService + //... +} + +@Provide() +export class B { + + @Inject() + userService: UserService + //... +} +``` + +在 v3.10 版本之后,可以使用单例装饰器来简化原来的写法。 + +```typescript +import { Singleton } from '@midwayjs/core'; + +@Singleton() +class UserService { + // ... +} +``` + + + +### 请求作用域 + +默认情况下,代码中编写的类均为 **请求作用域**。 + +在每个协议入口框架会自动创建一个请求作用域下的依赖注入容器,所有创建的实例都会绑定当前协议的上下文。 + +比如: + +- http 请求进来的时候,会创建一个请求作用域,每个 Controller 都是在请求路由时动态创建 +- 定时器触发,也相当于创建了请求作用域 ctx,我们可以通过@Inject()ctx可以拿到这个请求作用域。 + +:::info +默认为请求作用域的目的是为了和请求上下文关联,显式传递 ctx 更为安全可靠,方便调试。 +::: + +所以在请求作用域中,我们可以通过 `@Inject()` 来注入当前的 ctx 对象。 + +```typescript +import { Controller, Provide, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Provide() // 实际可省略 +@Controller('/user') +export class UserController { + + @Inject() + ctx: Context; + //... +} +``` + + + + +我们的 `@Inject` 装饰器也是在 **当前类的作用域** 下去寻找对象来注入的。比如,在 `Singleton` 作用域下,由于和请求不关联 ,默认没有 `ctx` 对象,所以注入 ctx 是不对的 。 + +```typescript +@Provide() +@Scope(ScopeEnum.Singleton) +export class UserService { + + @Inject() + ctx; // undefined + //... +} +``` + + + +### 作用域固化 + + +当作用域被设置为单例(Singleton)之后,整个 Class 注入的对象在第一次实例化之后就已经被固定了,这意味着,单例中注入的内容不能是其他作用域。 + + +我们来举个例子。 +```typescript +// 这个类是默认的请求作用域(Request) +@Provide() // 实际可省略 +@Controller() +export class HomeController { + @Inject() + userService: UserService; +} + + +// 设置了单例,进程级别唯一 +@Provide() +@Scope(ScopeEnum.Singleton) +export class UserService { + async getUser() { + // ... + } +} +``` +调用的情况如下。 +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01FN99rS1Xb1YydSFi0_!!6000000002941-2-tps-1110-388.png) + +这种情况下,不论调用 `HomeController` 多少次,每次请求的 `HomeController` 实例是不同的,而 `UserService` 都会固定的那个。 + + +我们再来举个例子演示单例中注入的服务是否还会保留原有作用域。 + +:::info +这里的 `DBManager` 我们特地设置成请求作用域,来演示一下特殊场景。 +::: +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01eAyxrC1xVEYzbNf9P_!!6000000006448-2-tps-1964-334.png) + +```typescript +// 这个类是默认的请求作用域(Request) +@Provide() +export class HomeController { + @Inject() + userService: UserService; +} + + +// 设置了单例,进程级别唯一 +@Provide() +@Scope(ScopeEnum.Singleton) +export class UserService { + + @Inject() + dbManager: DBManager; + + async getUser() { + // ... + } +} + +// 未设置作用域,默认是请求作用域(这里用来验证单例链路下,后续的实例都被缓存的场景) +@Provide() +export class DBManager { +} + +``` +这种情况下,不论调用 `HomeController` 多少次,每次请求的 `HomeController` 实例是不同的,而 `UserService` 和 `DBManager` 都会固定的那个。 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01UoLu1526stZQFhp1U_!!6000000007718-2-tps-1870-762.png) +简单的理解为,单例就像一个缓存,**其中依赖的所有对象都将被冻结,不再变化。** + + + +### 作用域降级 + +上面提到,当单例作用域注入请求作用域对象时,请求作用域的对象实例将被固化,会保存一个固定的实例在单例的缓存中。 + +这个时候,请求作用域变为单例,出现 **作用域降级** 的情况。 + +在日常开发中,一不留神就会发生这种情况,比如中间件中调用服务。 + +```typescript +//下面这段是错误的示例 + +@Provide() +export class UserService { + @Inject() + ctx: Context; + + async getUser() { + const id = this.ctx.xxxx; + // ctx not found, will throw error + } +} + +// 中间件是单例 +@Middleware() +export class ReportMiddleware implements IMiddleware { + @Inject() + userService: UserService; // 这里的用户服务是请求作用域 + + resolve() { + return async(ctx, next) => { + await this.userService.getUser(); + // ... + } + } +} +``` + +这个时候,虽然 `UserService` 可以正常注入中间件,但是实际上是以 单例 的对象注入,而不是请求作用域的对象,会导致 `ctx` 为空的情况。 + +这个时候的内存对象图为: + +![](https://img.alicdn.com/imgextra/i3/O1CN01SwATKb1zUtVUCaQGj_!!6000000006718-2-tps-1292-574.png) + +`UserService` 的实例变成了不同的对象,一个是单例调用的实例(单例,不含 ctx),一个是正常的请求作用域调用的实例(请求作用域,含 ctx)。 + +为了避免发生这种情况,默认在这类错误的注入时,框架会自动抛出名为 `MidwaySingletonInjectRequestError` 的错误,阻止程序执行。 + +如果用户了解其中的风险,明确需要在单例中调用请求作用域对象,可以通过作用域装饰器的参数来设置允许降级。 + +并在其中做好 `ctx` 的空对象判断。 + +```typescript +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Request, { allowDowngrade: true }) +export class UserService { + @Inject() + ctx: Context; + + async getUser() { + if (ctx && ctx.xxxx) { + // ... + } + // ... + } +} +``` + +当然,如果只是误写,那可以使用动态的获取方式,使得作用域统一。 + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + const userService = await ctx.requestContext.getAsync(UserService); + // TODO userService.xxxx + await next(); + }; + } +} +``` + + + +### 获取对象作用域 + +从 v3.12.0 版本开始,依赖注入容器增加了一个新的获取对象作用域的 API。 + +```typescript +import { Controller, Inject, ApplicationContext, Get, IMidwayContainer } from '@midwayjs/core'; +import { UserService} from '../service/user.service'; + +@Singleton() +export class UserSerivce { + // ... +} + +@Controller('/') +export class HomeController { + @Inject() + userService: UserService; + + @ApplicationContext() + applicationContext: IMidwayContainer; + + @Get('/') + async home(): Promise { + console.log(this.applicationContext.getInstanceScope(this)); + // => Request + + console.log(this.applicationContext.getInstanceScope(this.userService)); + // => Singleton + + // ... + } +} +``` + +`getInstanceScope` 方法的返回值为 `ScopeEnum` 值。 + + + +## 注入规则 + +Midway 支持多种方式的注入。 + +### 基于 Class 的注入 + +导出一个 Class,注入的类型使用 Class,这是最简单的注入方式,大部分的业务和组件都是使用这样的方式。 + +```typescript +import { Provide, Inject } from '@midwayjs/core'; + +@Provide() // <------ 暴露一个 Class +export class B { + //... +} + +@Provide() +export class A { + + @Inject() + b: B; // <------ 这里的属性使用 Class + + //... +} +``` + +Midway 会自动使用 B 作为 b 这个属性的类型,在容器中实例化它。 + +在这种情况下,Midway 会自动创建一个唯一的 uuid 关联这个 Class,同时这个 uuid 我们称为 **依赖注入标识符**。 + + +默认情况: + + +- 1、 `@Provide` 会自动生成一个 uuid 作为依赖注入标识符 +- 2、 `@Inject` 根据类型的 uuid 来查找 + +如果要获取这个 uuid,可以使用下面的 API。 + +```typescript +import { getProviderUUId } from '@midwayjs/core'; + +const uuid = getProviderUUId(B); +// ... +``` + + + +### 基于固定名字的注入 + +```typescript +import { Provide, Inject } from '@midwayjs/core'; + +@Provide('bbbb') // <------ 暴露一个 Class +export class B { + //... +} + +@Provide() +export class A { + + @Inject('bbbb') + b: B; // <------ 这里的属性使用 Class + + //... +} +``` + +Midway 会将 `bbbb` 作为 B 这个 Class 的依赖注入标识符,在容器中实例化它。这种情况下,即使写了类型 B,依赖注入容器依旧会查找 `bbbb` 。 + +`@Provide` 和 `@Inject` 装饰器的参数是成对出现。 + +规则如下: + + +- 1、如果装饰器包含参数,则以 **参数 **作为依赖注入标识符 +- 2、如果没有参数,标注的 TS 类型为 Class,则将类 `@Provide` 的 key 作为 key,如果没有 key,默认取 uuid +- 3、如果没有参数,标注的 TS 类型为非 Class,则将 **属性名** 作为 key + + + +### 基于属性名的注入 + +Midway 也可以基于接口进行注入,但是由于 Typescirpt 编译后会移除接口类型,不如使用类作为定义好用。 + +比如,我们定义一个接口,以及它的实现类。 + +```typescript +export interface IPay { + payMoney() +} + +@Provide('APay') +export class A implements IPay { + async payMoney() { + // ... + } +} + +@Provide('BPay') +export class B implements IPay { + async payMoney() { + // ... + } +} +``` + +这个时候,如果有个服务需要注入,可以使用下面显式声明的方式。 + +```typescript +@Provide() +export class PaymentService { + + @Inject('APay') + payService: IPay; // 注意,这里的类型是接口,编译后类型信息会被移除 + + async orderGood() { + await this.payService.payMoney(); + } + +} +``` + +由于接口类型会被移除,Midway 只能通过 `@Inject` 装饰器的 **参数** 或者 **属性名** 类来匹配注入的对象信息,类似 Java Spring 中的 `Autowire by name` 。 + +### 注入已有对象 + + +有时候,应用已经有现有的实例,而不是类,比如引入了一个第三库,这个时候如果希望对象能够被其他 IoC 容器中的实例引用,也可以通过增加对象的方式进行处理。 + + +我们拿常见的工具类库 lodash 来举例。 + +假如我们希望在不同的类中直接注入来使用,而不是通过 require 的方式。 + +你需要在业务调用前(一般在启动的生命周期中)通过 `registerObject` 方法添加这个对象。 + + +在添加的时候需要给出一个 **依赖注入标识符**,方便其他类中注入。 + + +```typescript +// src/configuration.ts +import * as lodash from 'lodash'; +import { Configuration, IMidwayContainer } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration { + + async onReady(applicationContext: IMidwayContainer) { + // 向依赖注入容器中添加一些全局对象 + applicationContext.registerObject('lodash', lodash); + } +} + +``` + + +这个时候就可以在任意的类中通过 `@Inject` 来使用了。 + + +```typescript +@Provide() +export class BaseService { + + @Inject('lodash') + lodashTool; + + async getUser() { + // this.lodashTool.defaults({ 'a': 1 }, { 'a': 3, 'b': 2 }); + } +} +``` + + + +### 注入默认标识符 + + +Midway 会默认注入一些值,方便业务直接使用。 + +| **标识符** | **值类型** | **作用域** | **描述** | +| ---------- | ---------- | ---------- | ------------------------------------------------------------ | +| baseDir | string | 全局 | 本地开发时为 src 目录,否则为 dist 目录 | +| appDir | string | 全局 | 应用的根路径,一般为 process.cwd() | +| ctx | object | 请求链路 | 对应框架的上下文类型,比如 Koa 和 EggJS 的 Context,Express 的 req | +| logger | object | 请求链路 | 等价于 ctx.logger | +| req | object | 请求链路 | Express 特有 | +| res | object | 请求链路 | Express 特有 | +| socket | object | 请求链路 | WebSocket 场景特有 | + +```typescript +@Provide() +export class BaseService { + + @Inject() + baseDir; + + @Inject() + appDir; + + async getUser() { + console.log(this.baseDir); + console.log(this.appDir); + } +} +``` + + + +## 获取依赖注入容器 + + +在一般情况下,用户无需关心依赖注入容器,但是在一些特殊场景下,比如 + + +- 需要动态调用服务的,比如 Web 的中间件场景,启动阶段需要调用服务的 +- 封装框架或者其他三方 SDK 中需要动态获取服务的 + +简单来说,任意需要 **通过 API 动态获取服务** 的场景,都需要先拿到依赖注入容器。 + +### 从 @ApplicationContext() 装饰器中获取 + +在新版本中,Midway 提供了一个 @ApplicationContext() 的装饰器,用来获取依赖注入容器。 + +```typescript +import { ApplicationContext, IMidwayContainer } from '@midwayjs/core'; +import { IMidwayContainer } from '@midwayjs/core'; + +@Provide() +export class BootApp { + + @ApplicationContext() + applicationContext: IMidwayContainer; // 这里也可以换成实际的框架的 app 定义 + + async invoke() { + + // this.applicationContext + + } + +} +``` + + + +### 从 app 中获取 + + +Midway 将依赖注入容器挂载在两个地方,框架的 app 以及每次请求的上下文 Context,由于不同上层框架的情况不同,我们这里列举一下常见的示例。 + + +对于不同的上层框架,我们统一提供了 `IMidwayApplication` 定义,所有的上层框架 app 都会实现这个接口,定义如下。 + +```typescript +export interface IMidwayApplication { + getApplicationContext(): IMidwayContainer; + //... +} +``` + +即通过 `app.getApplicationContext()` 方法,我们都能获取到依赖注入容器。 + +```typescript +const container = app.getApplicationContext(); +``` + +配合 `@App` 装饰器,我们可以方便的在任意地方拿到当前运行的 app 实例。 + +```typescript +import { App, IMidwayApplication } from '@midwayjs/core'; + +@Provide() +export class BootApp { + + @App() + app: IMidwayApplication; // 这里也可以换成实际的框架的 app 定义 + + async invoke() { + + // 获取依赖注入容器 + const applicationContext = this.app.getApplicationContext(); + + } + +} +``` + + +除了普通的依赖注入容器之外,Midway 还提供了一个 **请求链路的依赖注入容器,**这个请求链路的依赖注入容器和全局的依赖注入容器关联,共享一个对象池。但是两者还是有所区别的。 + + +请求链路的依赖注入容器,是为了获取特有的请求作用域的对象,这个容器中获取的对象,都是**和请求绑定**,关联了当前的上下文。这意味着,**如果 Class 代码和请求关联,必须要从这个请求链路的依赖注入容器中获取**。 + + +请求链路的依赖注入容器,必须从请求上下文对象中获取,最常见的场景为 Web 中间件。 + + +```typescript +@Middleware() +export class ReportMiddleware { + + resolve() { + return async(ctx, next) => { + // ctx.requestContext 请求链路的依赖注入容器 + await next(); + } + } +} +``` +Express 的请求链路依赖注入容器挂载在 req 对象上。 + +```typescript +@Middleware() +export class ReportMiddleware { + + resolve() { + return (req, res, next) => { + // req.requestContext 请求链路的依赖注入容器 + next(); + } + } +} +``` + + + +### 在 Configuration 中获取 + +在代码的入口 `configuration` 文件的生命周期中,我们也会额外传递依赖注入容器参数,方便用户直接使用。 + +```typescript +// src/configuration.ts +import { Configuration, IMidwayContainer } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration { + async onReady(applicationContext: IMidwayContainer) { + // ... + } +} + +``` + + + +## 动态 API + + + +### 动态获取实例 + +拿到 **依赖注入容器 **或者 **请求链路的依赖 **注入容器之后,才可以通过容器的 API 获取到对象。 + +我们可以使用标准的依赖注入容器 API 来获取实例。 + +```typescript +// 全局容器,获取的是单例 +const userSerivce = await applicationContext.getAsync(UserService); + +// 请求作用域容器,获取请求作用域实例 +const userSerivce = await ctx.requestContext.getAsync(UserService); +``` + +我们可以在任意能获取依赖注入容器的地方使用,比如中间件中。 + +```typescript +import { Middleware, ApplicationContext, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; +import { UserService } from './service/user.service'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + @ApplicationContext() + applicationContext: IMidwayContainer; + + resolve() { + return async(ctx, next) => { + // 指定泛型类型,比如某个接口 + const userService1 = await this.applicationContext.getAsync(UserService); + // 不写泛型,也能推导出正确的类型 + const userService1 = await this.applicationContext.getAsync(UserService); + + // 下面的方法获取的服务和请求关联,可以注入上下文 + const userService2 = await ctx.requestContext.getAsync(UserService); + await next(); + } + } +} +``` + + +Express 的写法 +```typescript +import { UserService, Middleware } from './service/user'; +import { NextFunction, Context, Response } from '@midwayjs/express'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (req, res, next) => { + const userService = await req.requestContext.getAsync(UserService); + // ... + next(); + } + } +} +``` + + + +### 传递构造器参数 + +在个别场景下,我们可以通过 `getAsync` 获取实例的时候,传递构造器的参数。普通装饰器模式无法做到,仅在 API 形式下可用。 + +```typescript +@Provide() +class UserService { + constructor(private readonly type) {} + + getUser() { + // this.type => student + } +} + +// 全局容器,获取的是单例 +const userSerivce = await applicationContext.getAsync(UserService, [ + 'student', // 构造器参数,会 apply 到构造器中 +]); + +// 请求作用域容器,获取请求作用域实例 +const userSerivce = await ctx.requestContext.getAsync(UserService, [ + 'student' +]); +``` + +注意,构造器不能使用注入形式传递实例,只能传递固定的值。 + + + + +### 动态函数注入 + + +在某些场景下,我们需要函数作为某个逻辑动态执行,而依赖注入容器中的对象属性则都是已经创建好的,无法满足动态的逻辑需求。 + + +比如你需要一个工厂函数,根据不同的场景返回不同的实例,也可能有一个三方包,是个函数,在业务中想要直接调用,种种的场景下,你就需要直接注入一个工厂方法,并且在函数中拿到上下文,动态去生成实例。 + + +下面是标准的工厂方法注入样例。 + + +一般工厂方法用于返回相同接口的实现,比如我们有两个 `ICacheService` 接口的实现: +```typescript +export interface ICacheService { + getData(): any; +} + +@Provide() +export class LocalCacheService implements ICacheService { + async getData {} +} + +@Provide() +export class RemoteCacheService implements ICacheService { + async getData {} +} +``` +然后可以定义一个动态服务(工厂),根据当前的用户配置返回不同的实现。 +```typescript +// src/service/dynamicCacheService.ts + +import { providerWrapper, IMidwayContainer, MidwayConfigService } from '@midwayjs/core'; + +export async function dynamicCacheServiceHandler(container: IMidwayContainer) { + // 从容器 API 获取全局配置 + const config = container.get(MidwayConfigService).getConfiguration(); + if (config['redis']['mode'] === 'local') { + return await container.getAsync('localCacheService'); + } else { + return await container.getAsync('remoteCacheService'); + } +} + +providerWrapper([ + { + id: 'dynamicCacheService', + provider: dynamicCacheServiceHandler, + scope: ScopeEnum.Request, // 设置为请求作用域,那么上面传入的容器就为请求作用域容器 + // scope: ScopeEnum.Singleton, // 也可以设置为全局作用域,那么里面的调用的逻辑将被缓存 + } +]); +``` + + +这样在业务中,可以直接来使用了。注意:在注入的时候,方法会**被调用后再注入**。 + + +```typescript +@Provide() +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Inject('dynamicCacheServiceHandler') + cacheService: ICacheService; + + @Get('/') + async home() { + const data = await this.cacheService.getData(); + // ... + } + +} +``` + + +通过 `providerWrapper` 我们将一个原本的函数写法进行了包裹,和现有的依赖注入体系可以融合到一起,让容器能够统一管理。 + + +:::info +注意,动态方法必须 export,才会被依赖注入扫描到,默认为请求作用域(获取的 Container 是请求作用域容器)。 +::: + + +由于我们能将动态方法绑定到依赖注入容器,那么也能将一个回调方法绑定进去,这样获取的方法是可以被执行的,我们可以根据业务的传参来决定返回的结果。 +```typescript +import { providerWrapper, IMidwayContainer } from '@midwayjs/core'; + +export function cacheServiceHandler(container: IMidwayContainer) { + return async (mode: string) => { + if (mode === 'local') { + return await container.getAsync('localCacheService'); + } else { + return await container.getAsync('remoteCacheService'); + } + }; +} + +providerWrapper([ + { + id: 'cacheServiceHandler', + provider: cacheServiceHandler, + scope: ScopeEnum.Singleton, + } +]); + + +@Provide() +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Inject('cacheServiceHandler') + getCacheService; + + @Get('/') + async home() { + const data = await this.getCacheService('local'); + // ... + } + +} +``` + + + +## 静态 API + + +在有些工具类中,我们可以不需要创建 class 实例就能获取到全局的依赖注入容器(**在启动之后**)。 +```typescript +import { getCurrentApplicationContext } from '@midwayjs/core'; + +export const getService = async (serviceName) => { + return getCurrentApplicationContext().getAsync(serviceName); +} +``` + + +获取主框架(**在启动之后**)。 +```typescript +import { getCurrentMainFramework } from '@midwayjs/core'; + +export const framework = () => { + return getCurrentMainFramework(); +} +``` +获取主框架的 app 对象(**在启动之后**)。 +```typescript +import { getCurrentMainApp } from '@midwayjs/core'; + +export const getGlobalConfig = () => { + return getCurrentMainApp().getConfig(); +} +``` + + + +## 启动行为 + +### 自动扫描绑定 + +上面提到,在容器初始化之后,我们会将现有的 class 注册绑定到容器中。 + +```typescript +const container = new MidwayContainer(); +container.bind(UserController); +container.bind(UserService); +``` + +Midway 在启动过程中会自动扫描整个项目目录,自动处理这个行为,使得用户无需手动执行绑定的操作。 + +简单的来说,框架默认会递归扫描整个 `src` 目录下的 ts/js 文件,然后进行 require 操作,当文件导出的为 class,且显式或隐式包含 `@Provide()` 装饰器时,会执行 `container.bind` 逻辑。 + + + +### 忽略扫描 + +一般情况下,我们不应该把非 ts 文件放在 src 下(比如前端代码),特殊场景下,我们可以忽略某些目录,可以在 `@Configuration` 装饰器中配置。 + +示例如下: + +```typescript +// src/configuration.ts +import { App, Configuration, Logger } from '@midwayjs/core'; +// ... + +@Configuration({ + // ... + detectorOptions: { + ignore: [ + '**/web/**' + ] + } +}) +export class MainConfiguration { + // ... +} + +``` + + + + + +## 对象生命周期 + +在依赖注入容器创建和销毁实例的时候,我们可以使用装饰器做一些自定义的操作。 + + + +### 异步初始化 + + +在某些情况下,我们需要一个实例在被其他依赖调用前需要初始化,如果这个初始化只是读取某个文件,那么可以写成同步方式,而如果这个初始化是从远端拿取数据或者连接某个服务,这个情况下,普通的同步代码就非常的难写。 + + +Midway 提供了异步初始化的能力,通过 `@Init` 标签来管理初始化方法。 + +`@Init` 方法目前只能是一个。 + + +```typescript +@Provide() +export class BaseService { + + @Config('hello') + config; + + @Init() + async init() { + await new Promise(resolve => { + setTimeout(() => { + this.config.c = 10; + resolve(); + }, 100); + }); + } + +} +``` + + +等价于 + +```typescript +const service = new BaseService(); +await service.init(); +``` + +:::info +@Init 装饰器标记的方法,一定会以异步方式来调用。一般来说,异步初始化的服务较慢,请尽可能标注为单例(@Scope(ScopeEnum.Singleton))。 +::: + + + +### 异步销毁 + +Midway 提供了在对象销毁前执行方法的能力,通过 `@Destroy` 装饰器来管理方法。 + +`@Destroy` 方法目前只能是一个。 + + +```typescript +@Provide() +export class BaseService { + + @Config('hello') + config; + + @Destroy() + async stop() { + // do something + } +} +``` + + + +## 请求作用域中的上下文对象 + +在请求作用域创建的对象,框架会在对象上挂载一个上下文对象,即使对象未显式声明 `@Inject() ctx` 也能获取当前上下文对象。 + +```typescript +import { REQUEST_OBJ_CTX_KEY } from '@midwayjs/core'; + +@Provide() +export class UserManager { + // ... +} + +@Provide() +export class UserService { + // ... + + @Inject() + userManager: UserManager; + + async invoke() { + const ctx = this.userManager[REQUEST_OBJ_CTX_KEY]; + // ... + } +} +``` + +这个特性在 [拦截器](./aspect) 或者 [自定义方法装饰器](./custom_decorator) 中很有用。 + + + +## 常见的使用错误 + + +### 错误:构造器中获取注入属性 + + +**请不要在构造器中 **获取注入的属性,这会使得拿到的结果为 undefined。原因是装饰器注入的属性,都在实例创建后(new)才会赋值。这种情况下,请使用 `@Init` 装饰器。 +```typescript +@Provide() +export class UserService { + + @Config('userManager') + userManager; + + constructor() { + console.log(this.userManager); // undefined + } + + @Init() + async initMethod() { + console.log(this.userManager); // has value + } + +} +``` + + + +### 关于继承 + + +为了避免属性错乱,请不要在基类上使用 `@Provide` 装饰器。 + + +现阶段,Midway 支持属性装饰器的继承,不支持类和方法装饰器的继承(会有歧义)。 diff --git a/site/versioned_docs/version-3.0.0/context_definition.md b/site/versioned_docs/version-3.0.0/context_definition.md new file mode 100644 index 000000000000..00c214db83ae --- /dev/null +++ b/site/versioned_docs/version-3.0.0/context_definition.md @@ -0,0 +1,114 @@ +# 扩展上下文定义 + +由于 TS 的静态类型分析,我们并不推荐动态去挂载某些属性,动态的挂载会导致 TS 的类型处理非常困难。在某些特殊场景下,如果需要扩展上下文 ctx 属性,比如 Web 场景下中间件,我们可以往上附加一些方法或者属性。 + +```typescript +import { Middleware } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IWebMiddleware { + + resolve() { + return async (ctx: Context, next) => { + + ctx.abc = '123'; + await next(); + + } + } + +} +``` + +但是由于 TypeScript 模块定义的关系,我们无法往现有的模块上去附加定义,所以我们使用了一种新的方法来扩展。 + + + + +## 项目中扩展定义 + + +你可以在 `src/interface.ts` 通过下面的代码,在项目中扩展 Midway 通用的 Context。 + +```typescript +// src/interface.ts + +import '@midwayjs/core'; + +// ... + +declare module '@midwayjs/core' { + interface Context { + abc: string; + } +} +``` + +:::info + +注意,`declare module` 会替代原有的定义,所以请在之前使用 `import` 语法导入模块后再操作。 + +::: + + + +## 组件中扩展定义 + +组件中略有不同,一般来说,组件可能是只能在特定的场景使用。 + +你可以在组件根目录的 `index.d.ts` 通过下面的代码,扩展 Midway 通用的 Context。 + +如果你希望对所有场景的 Context 做扩展。 + +```typescript +// index.d.ts + +// 下面这段可以对所有的 Context 做扩展 +declare module '@midwayjs/core/dist/interface' { + interface Context { + abc: string; + } +} +``` + +如果你只希望对特定场景的 Context 做扩展。 + +```typescript +// index.d.ts + +// 下面这段只 @midwayjs/koa 的 Context 做扩展 +declare module '@midwayjs/koa/dist/interface' { + interface Context { + abc: string; + } +} + +// 下面这段只 @midwayjs/web 的 Context 做扩展 +declare module '@midwayjs/web/dist/interface' { + interface Context { + abc: string; + } +} + +// 下面这段只 @midwayjs/faas 的 Context 做扩展 +declare module '@midwayjs/faas/dist/interface' { + interface Context { + abc: string; + } +} + +// 下面这段只 @midwayjs/express 的 Context 做扩展 +declare module '@midwayjs/express/dist/interface' { + interface Context { + abc: string; + } +} + +``` + +:::caution +- 1、组件中扩展和项目中略有不同(怀疑是 TS 的 bug)。 +- 2、如果组件中使用了项目的扩展方式,那么其余组件的扩展提示会出现问题。 + +::: diff --git a/site/versioned_docs/version-3.0.0/contributing.md b/site/versioned_docs/version-3.0.0/contributing.md new file mode 100644 index 000000000000..ce5fa6a8874f --- /dev/null +++ b/site/versioned_docs/version-3.0.0/contributing.md @@ -0,0 +1,89 @@ +# 向 Midway 贡献 + +Midway 是一款开源框架,欢迎大家为社区贡献力量,本文介绍如何向 Midway 提交 issue,贡献代码,文档等。 + + + +## 报告问题 + +如果你在开发过程中遇到了一些问题,你无法解决需要想开发者问询的,我们强烈建议: + +- 1、先在文档中查找相关的问题 +- 2、如果查找后无法解决,可以提交一个 [Q&A](https://github.com/midwayjs/midway/discussions/new/choose)。 + + + +在提交的内容时,请遵守下列规范。 + +- 1、在标题或内容中清楚地解释你的目的,中文或者英文均可。 +- 2、在内容中描述以下内容 + - 如果是个新需求,请详细描述需求内容,最好有伪代码实现 + - 如果是一个 BUG,请提供复现步骤,错误日志,截图,相关配置,框架版本等可以让开发者快速定位问题的内容 + - 如果可以,请尽可能提供一个最小可复现的代码仓库,方便调试 +- 3、在您报告问题之前,请搜索相关问题。确保您不会打开重复的问题 + + + +开发者会在看到时进行标记问题,回复或者解决问题。 + + + +## 修复代码问题 + +如果你发现框架有一些待修改的问题,可以通过 PR 来提交。 + + + +### PR 流程 + +1、首先在 [midway github](https://github.com/midwayjs/midway) 右上角 fork 一个仓库,到自己的空间下。 + +2、git clone 该仓库到本地或者其他 IDE 环境,进行开发或者修复工作。 + +```bash +# 创建新分支 +$ git checkout -b branch-name +# 安装依赖 +$ npm i +# 构建项目 +$ npm run build + +# 开发并执行测试 +$ npm test + +$ git add . # git add -u to delete files +$ git commit -m "fix(role): role.use must xxx" +$ git push origin branch-name +``` + +3、创建一个 Pull Request,选择将自己的项目分支,合并到目标 midwayjs/midway 的 main 分支。 + +4、系统自动会创建 PR 到 midway 仓库下,在测试通过后,开发者会合并此 PR。 + + + +### 提交规范 + +- 1、一般 PR 使用英文标题 +- 2、提交前缀使用 `fix`,`chore`,`feat` ,`docs`字段,用于快速标示修复的类型。 + + + +## 修复文档问题 + +和普通 PR 类似,如果是单篇文档,可以使用快速编辑的方式提交。 + + + +### 单篇文档快速修复 + +- 1、打开官网需要修复的文档,点击左下角 [Edit this page](#) 链接,会跳转到 Github 对应的文档 +- 2、点击 “笔型” 按钮,进入编辑页面 +- 3、编辑内容后,将提交的标题修改为 `docs: xxxx`,点击提交按钮创建 PR +- 4、等待开发者合并 + + + +### 多篇文档修复 + +和普通 PR 相同,clone 仓库,提交,注意,提交 PR 的标题为 `docs: xxx`。 diff --git a/site/versioned_docs/version-3.0.0/controller.md b/site/versioned_docs/version-3.0.0/controller.md new file mode 100644 index 000000000000..40d5d2ed69fb --- /dev/null +++ b/site/versioned_docs/version-3.0.0/controller.md @@ -0,0 +1,1140 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 路由和控制器 + +在常见的 MVC 架构中,C 即代表控制器,控制器用于负责 **解析用户的输入,处理后返回相应的结果。** + +如图所示,客户端通过 Http 协议请求服务端的控制器,控制器处理结束后响应客户端,这是一个最基础的 ”请求 - 响应“ 流程。 + +![controller](https://img.alicdn.com/imgextra/i1/O1CN01dYitV22ADuagILnp3_!!6000000008170-2-tps-1600-634.png) + + +常见的有: + + +- 在 [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) 接口中,控制器接受用户的参数,从数据库中查找内容返回给用户或者将用户的请求更新到数据库中。 +- 在 HTML 页面请求中,控制器根据用户访问不同的 URL,渲染不同的模板得到 HTML 返回给用户。 +- 在代理服务器中,控制器将用户的请求转发到其他服务器上,并将其他服务器的处理结果返回给用户。 + + + +一般来说,控制器常用于对用户的请求参数做一些校验,转换,调用复杂的业务逻辑,拿到相应的业务结果后进行数据组装,然后返回。 + + +在 Midway 中,控制器 **也承载了路由的能力**,每个控制器可以提供多个路由,不同的路由可以执行不同的操作。 + + +在接下去的示例中,我们将演示如何在控制器中创建路由。 + + +## 路由 + + +控制器文件一般来说在 `src/controller` 目录中,我们可以在其中创建控制器文件。Midway 使用 `@Controller()` 装饰器标注控制器,其中装饰器有一个可选参数,用于进行路由前缀(分组),这样这个控制器下面的所有路由都会带上这个前缀。 + + +同时,Midway 提供了方法装饰器用于标注请求的类型。 + + +比如,我们创建一个首页控制器,用于返回一个默认的 `/` 路由的页面。 +``` +➜ my_midway_app tree +. +├── src +│ └── controller +│ └── home.ts +├── test +├── package.json +└── tsconfig.json +``` +```typescript +// src/controller/home.ts + +import { Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + return "Hello Midwayjs!"; + } +} + +``` +`@Controller` 装饰器告诉框架,这是一个 Web 控制器类型的类,而 `@Get` 装饰器告诉框架,被修饰的 `home` 方法,将被暴露为 `/` 这个路由,可以由 `GET` 请求来访问。 + +整个方法返回了一个字符串,在浏览器中你会收到 `text/plain` 的响应类型,以及一个 `200` 的状态码。 + +:::tip + +路由方法均为 async 方法。 + +::: + + + + +## 路由方法 + + +上面的示例,我们已经创建了一个 **GET** 路由。一般情况下,我们会有其他的 HTTP Method,Midway 提供了更多的路由方法装饰器。 + + +```typescript +// src/controller/home.ts + +import { Controller, Get, Post } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + return 'Hello Midwayjs!'; + } + + @Post('/update') + async updateData() { + return 'This is a post method' + } +} + +``` +Midway 还提供了其他的装饰器, `@Get` 、 `@Post` 、 `@Put()` 、 `@Del()` 、 `@Patch()` 、 `@Options()` 、 `@Head()` 和 `@All()` ,表示各自的 HTTP 请求方法。 + + +`@All` 装饰器比较特殊,表示能接受以上所有类型的 HTTP Method。 + + +你可以将多个路由绑定到同一个方法上。 +```typescript +@Get('/') +@Get('/main') +async home() { + return 'Hello Midwayjs!'; +} +``` + + +## 获取请求参数 + + +接下去,我们将创建一个关于用户的 HTTP API,同样的,创建一个 `src/controller/user.ts` 文件,这次我们会增加一个路由前缀,以及增加更多的请求类型。 + + +我们以用户类型举例,先增加一个用户类型,我们一般会将定义的内容放在 `src/interface.ts` 文件中。 +``` +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ ├── user.ts +│ │ └── home.ts +│ └── interface.ts +├── test +├── package.json +└── tsconfig.json +``` +```typescript +// src/interface.ts +export interface User { + id: number; + name: string; + age: number; +} +``` +再添加一个路由前缀以及对应的控制器。 +```typescript +// src/controller/user.ts + +import { Controller } from "@midwayjs/core"; + +@Controller('/api/user') +export class UserController { + // xxxx +} + +``` + +接下去,我们要针对不同的请求类型,调用不同的处理逻辑。除了请求类型之外,请求的数据一般都是动态的,会在 HTTP 的不同位置来传递,比如常见的 Query,Body 等。 + + + +### 装饰器参数约定 + + +Midway 添加了常见的动态取值的装饰器,我们以 `@Query` 装饰器举例, `@Query` 装饰器会获取到 URL 中的 Query 参数部分,并将它赋值给函数入参。下面的示例,id 会从路由的 Query 参数上拿,如果 URL 为 `/?id=1` ,则 id 的值为 1,同时,这个路由将会返回 `User` 类型的对象。 +```typescript +// src/controller/user.ts + +import { Controller, Get, Query } from "@midwayjs/core"; + +@Controller('/api/user') +export class UserController { + @Get('/') + async getUser(@Query('id') id: string): Promise { + // xxxx + } +} +``` + +`@Query` 装饰器的有参数,可以传入一个指定的字符串 key,获取对应的值,赋值给入参,如果不传入,则默认返回整个 Query 对象。 + +```typescript +// URL = /?id=1 +async getUser(@Query('id') id: string) // id = 1 +async getUser(@Query() queryData) // {"id": "1"} +``` + +Midway 提供了更多从 Query、Body 、Header 等位置获取值的装饰器,这些都是开箱即用,并且适配于不同的上层 Web 框架。 + + +下面是这些装饰器,以及对应的等价框架取值方式。 + +| 装饰器 | Express 对应的方法 | Koa/EggJS 对应的方法 | +| --- | --- | --- | +| @Session(key?: string) | req.session / req.session[key] | ctx.session / ctx.session[key] | +| @Param(key?: string) | req.params / req.params[key] | ctx.params / ctx.params[key] | +| @Body(key?: string) | req.body / req.body[key] | ctx.request.body / ctx.request.body[key] | +| @Query(key?: string) | req.query / req.query[key] | ctx.query / ctx.query[key] | +| @Queries(key?: string) | 无 | 无 / ctx.queries[key] | +| @Headers(name?: string) | req.headers / req.headers[name] | ctx.headers / ctx.headers[name] | + +:::caution +**注意** EggJS 和其他框架不同,`@Queries` 装饰器和 `@Query` **有所区别**。 + +Queries 会将相同的 key 聚合到一起,变为数组。当用户访问的接口参数为 `/?name=a&name=b` 时,@Queries 会返回 `{name: [a, b]}`,而 Query 只会返回 `{name: b}`。 +::: + + + +### Query + +在 URL 中 `?` 后面的部分是一个 Query String,这一部分经常用于 GET 类型的请求中传递参数。例如 + +``` +GET /user?uid=1&sex=male +``` + +就是用户传递过来的参数。 + +**示例:从装饰器获取** + +```typescript +// src/controller/user.ts +import { Controller, Get, Query } from "@midwayjs/core"; + +@Controller('/user') +export class UserController { + @Get('/') + async getUser(@Query('uid') uid: string): Promise { + // xxxx + } +} +``` + +**示例:从 API 获取** + +```typescript +// src/controller/user.ts +import { Controller, Get, Inject } from "@midwayjs/core"; +import { Context } from '@midwayjs/koa'; + +@Controller('/user') +export class UserController { + + @Inject() + ctx: Context; + + @Get('/') + async getUser(): Promise { + const query = this.ctx.query; + // { + // uid: '1', + // sex: 'male', + // } + } +} +``` + +:::caution +**注意** EggJS 和其他框架不同,在 当 Query String 中的 key 重复时,`ctx.query` 只取 key 第一次出现时的值,后面再出现的都会被忽略。 + +比如 `GET /user?uid=1&uid=2` 通过 `ctx.query` 拿到的值是 `{ uid: '1' }`。 + +::: + +### Body + +虽然我们可以通过 URL 传递参数,但是还是有诸多限制: + +- [浏览器中会对 URL 的长度有所限制](http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers),如果需要传递的参数过多就会无法传递。 +- 服务端经常会将访问的完整 URL 记录到日志文件中,有一些敏感数据通过 URL 传递会不安全。 + +在前面的 HTTP 请求报文示例中,我们看到在 header 之后还有一个 body 部分,我们通常会在这个部分传递 POST、PUT 和 DELETE 等方法的参数。一般请求中有 body 的时候,客户端(浏览器)会同时发送 `Content-Type` 告诉服务端这次请求的 body 是什么格式的。Web 开发中数据传递最常用的两类格式分别是 `JSON` 和 `Form`。 + +框架内置了 [bodyParser](https://github.com/koajs/bodyparser) 中间件来对这两类格式的请求 body 解析成 object 挂载到 `ctx.request.body` 上。HTTP 协议中并不建议在通过 GET、HEAD 方法访问时传递 body,所以我们无法在 GET、HEAD 方法中按照此方法获取到内容。 + +**示例:获取单个 body** + +```typescript +// src/controller/user.ts +// POST /user/ HTTP/1.1 +// Host: localhost:3000 +// Content-Type: application/json; charset=UTF-8 +// +// {"uid": "1", "name": "harry"} +import { Controller, Post, Body } from '@midwayjs/core'; + +@Controller('/user') +export class UserController { + @Post('/') + async updateUser(@Body('uid') uid: string): Promise { + // id 等价于 ctx.request.body.uid + } +} +``` + +**示例:获取整个 body ** + +```typescript +// src/controller/user.ts +// POST /user/ HTTP/1.1 +// Host: localhost:3000 +// Content-Type: application/json; charset=UTF-8 +// +// {"uid": "1", "name": "harry"} +import { Controller, Post, Body } from '@midwayjs/core'; + +@Controller('/user') +export class UserController { + @Post('/') + async updateUser(@Body() user: User): Promise { + // user 等价于 ctx.request.body 整个 body 对象 + // => output user + // { + // uid: '1', + // name: 'harry', + // } + } +} +``` + +**示例:从 API 获取** + +```typescript +// src/controller/user.ts +// POST /user/ HTTP/1.1 +// Host: localhost:3000 +// Content-Type: application/json; charset=UTF-8 +// +// {"uid": "1", "name": "harry"} +import { Controller, Post, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/user') +export class UserController { + + @Inject() + ctx: Context; + + @Post('/') + async getUser(): Promise { + const body = this.ctx.request.body; + // { + // uid: '1', + // name: 'harry', + // } + } +} +``` + +**示例:获取 query 和 body 参数** + + +装饰器可以组合使用。 +```typescript +@Post('/') +async updateUser(@Body() user: User, @Query('pageIdx') pageIdx: number): Promise { + // user 从 body 获取 + // pageIdx 从 query 获取 +} +``` +框架对 bodyParser 设置了一些默认参数,配置好之后拥有以下特性: + +- 当请求的 Content-Type 为 `application/json`,`application/json-patch+json`,`application/vnd.api+json` 和 `application/csp-report` 时,会按照 json 格式对请求 body 进行解析,并限制 body 最大长度为 `1mb`。 +- 当请求的 Content-Type 为 `application/x-www-form-urlencoded` 时,会按照 form 格式对请求 body 进行解析,并限制 body 最大长度为 `1mb`。 +- 如果解析成功,body 一定会是一个 Object(可能是一个数组)。 + +:::caution + +常见错误: `ctx.request.body` 和 `ctx.body` 混淆,后者其实是 `ctx.response.body` 的简写。 + +::: + + + +### Router Params + +如果路由上使用 `:xxx` 的格式来声明路由,那么参数可以通过 `ctx.params` 获取到。 + +**示例:从装饰器获取** + +```typescript +// src/controller/user.ts +// GET /user/1 +import { Controller, Get, Param } from '@midwayjs/core'; + +@Controller('/user') +export class UserController { + @Get('/:uid') + async getUser(@Param('uid') uid: string): Promise { + // xxxx + } +} +``` + +**示例:从 API 获取** + +```typescript +// src/controller/user.ts +// GET /user/1 +import { Controller, Get, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/user') +export class UserController { + + @Inject() + ctx: Context; + + @Get('/:uid') + async getUser(): Promise { + const params = this.ctx.params; + // { + // uid: '1', + // } + } +} +``` + + + +### Header + +除了从 URL 和请求 body 上获取参数之外,还有许多参数是通过请求 header 传递的。框架提供了一些辅助属性和方法来获取。 + +- `ctx.headers`,`ctx.header`,`ctx.request.headers`,`ctx.request.header`:这几个方法是等价的,都是获取整个 header 对象。 +- `ctx.get(name)`,`ctx.request.get(name)`:获取请求 header 中的一个字段的值,如果这个字段不存在,会返回空字符串。 +- 我们建议用 `ctx.get(name)` 而不是 `ctx.headers['name']`,因为前者会自动处理大小写。 + +**示例:从装饰器获取** + +```typescript +// src/controller/user.ts +// GET /user/1 +import { Controller, Get, Headers } from '@midwayjs/core'; + +@Controller('/user') +export class UserController { + @Get('/:uid') + async getUser(@Headers('cache-control') cacheSetting: string): Promise { + // no-cache + // ... + } +} +``` + +**示例:从 API 获取** + +```typescript +// src/controller/user.ts +// GET /user/1 +import { Controller, Get, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/user') +export class UserController { + + @Inject() + ctx: Context; + + @Get('/:uid') + async getUser(): Promise { + const cacheSetting = this.ctx.get('cache-control'); + // no-cache + } +} +``` + + + +### Cookie + +HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:[Cookie](https://en.wikipedia.org/wiki/HTTP_cookie)。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。 + +通过 `ctx.cookies`,我们可以在 Controller 中便捷、安全的设置和读取 Cookie。 + +```typescript +import { Inject, Controller, Get, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + // set cookie + this.ctx.cookies.set('foo', 'bar', { encrypt: true }); + // get cookie + this.ctx.cookies.get('foo', { encrypt: true }); + } +} +``` + +Cookie 虽然在 HTTP 中只是一个头,但是通过 `foo=bar;foo1=bar1;` 的格式可以设置多个键值对。 + +Cookie 在 Web 应用中经常承担了传递客户端身份信息的作用,因此有许多安全相关的配置,不可忽视,[Cookie](cookie_session#默认的-cookies) 文档中详细介绍了 Cookie 的用法和安全相关的配置项,可以深入阅读了解。 + + + +### Session + +通过 Cookie,我们可以给每一个用户设置一个 Session,用来存储用户身份相关的信息,这份信息会加密后存储在 Cookie 中,实现跨请求的用户身份保持。 + +框架内置了 [Session](https://github.com/midwayjs/midway/tree/main/packages/session) 插件,给我们提供了 `ctx.session` 来访问或者修改当前用户 Session 。 + +```typescript +import { Inject, Controller, Get, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + // 获取 Session 上的内容 + const userId = this.ctx.session.userId; + const posts = await this.ctx.service.post.fetch(userId); + // 修改 Session 的值 + this.ctx.session.visited = ctx.session.visited ? (ctx.session.visited + 1) : 1; + // ... + } +} +``` + +Session 的使用方法非常直观,直接读取它或者修改它就可以了,如果要删除它,直接将它赋值为 `null`: + +```typescript +ctx.session = null; +``` + +和 Cookie 一样,Session 也有许多安全等选项和功能,在使用之前也最好阅读 [Session](cookie_session#默认的-session) 文档深入了解。 + + + +### 上传的文件 + +上传的文件一般使用 `multipart/form-data` 协议头,由 `@Files` 装饰器获取,由于上传功能由 upload 组件提供,具体可以参考 [upload 组件](./extensions/upload)。 + + + +### 其他的参数 + +还有一些比较常见的参数装饰器,以及它们的对应方法。 + +| 装饰器 | Express 对应的方法 | Koa/EggJS 对应的方法 | +| --- | --- | --- | +| @RequestPath | req.baseurl | ctx.path | +| @RequestIP | req.ip | ctx.ip | + + + +**示例:获取 body 、path 和 ip** + +```typescript +@Post('/') +async updateUser( + @Body('id') id: string, + @RequestPath() p: string, + @RequestIP() ip: string): Promise { + +} +``` + + + +### 自定义请求参数装饰器 + +你可以快速通过`createRequestParamDecorator` 创建自定义请求参数装饰器。 + +```typescript +import { createRequestParamDecorator } from '@midwayjs/core'; + +// 实现装饰器 +export const Token = () => { + return createRequestParamDecorator(ctx => { + return ctx.headers.token; + }); +}; + +// 使用装饰器 +export class UserController { + async invoke(@Token() token: string) { + console.log(token); + } +} +``` + + + + +## 请求参数类型转换 + +如果是简单类型,Midway 会自动将参数转换为用户声明的类型。 + +比如: + +数字类型 + +```ts +@Get('/') +async getUser(@Query('id') id: number): Promise { + console.log(typeof id) // number +} +``` + +布尔类型 + +- 当值为 0,"0", "false" 则转为 false,其余返回 Boolean(value) 的值 + +```ts +@Get('/') +async getUser(@Query('id') id: boolean): Promise { + console.log(typeof id) // boolean +} +``` + +如果是复杂类型,如果指定的类型是 Class,将会自动转换为该类的实例。 + +```typescript +// class +class UserDTO { + name: string; + + getName() { + return this.name; + } +} + +@Get('/') +async getUser(@Query() query: UserDTO): Promise { + // query.getName() +} +``` + +如果不希望被转换,可以使用 Interface。 + +```typescript +interface User { + name: string; +} + +@Get('/') +async getUser(@Query() query: User): Promise { + // ... +} +``` + + + +## 参数校验 + +参数校验功能由 validate 组件提供,具体可以参考 [validate 组件](./extensions/validate)。 + + + +## 设置 HTTP 响应 + +### 设置返回值 + +绝大多数的数据都是通过 body 发送给请求方的,和请求中的 body 一样,在响应中发送的 body,也需要有配套的 Content-Type 告知客户端如何对数据进行解析。 + +- 作为一个 RESTful 的 API 接口 controller,我们通常会返回 Content-Type 为 `application/json` 格式的 body,内容是一个 JSON 字符串。 +- 作为一个 html 页面的 controller,我们通常会返回 Content-Type 为 `text/html` 格式的 body,内容是 html 代码段。 + +在 Midway 中你可以简单的使用 `return` 来返回数据。 + +```typescript +import { Controller, Get, HttpCode } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + // 返回字符串 + return "Hello Midwayjs!"; + + // 返回 json + return { + a: 1, + b: 2, + }; + + // 返回 html + return '

Hello

'; + + // 返回 stream + return fs.createReadStream('./good.png'); + } +} +``` + +也可以使用 koa 原生的 API。 + +```typescript +import { Controller, Get, HttpCode } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + async home() { + // 返回字符串 + this.ctx.body = "Hello Midwayjs!"; + + // 返回 json + this.ctx.body = { + a: 1, + b: 2, + }; + + // 返回 html + this.ctx.body = '

Hello

'; + + // 返回 stream + this.ctx.body = fs.createReadStream('./good.png'); + } +} +``` + +:::caution + +注意:`ctx.body` 是 `ctx.response.body` 的简写,不要和 `ctx.request.body` 混淆了。 + +::: + + + +### 设置状态码 + +默认情况下,响应的**状态码**总是**200**,我们可以通过在处理程序层添加 `@HttpCode` 装饰器或者通过 API 来轻松更改此行为。 + +当发送错误时,如 `4xx/5xx`,可以使用 [异常处理](error_filter) 抛出错误的方式实现。 + +**示例:使用装饰器** + + +```typescript +import { Controller, Get, HttpCode } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + @HttpCode(201) + async home() { + return "Hello Midwayjs!"; + } +} +``` + +**示例:使用 API** + +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.status = 201; + // ... + } +} +``` + +:::info +状态码不能在响应流关闭后(response.end之后)修改。 +::: + + + +### 设置响应头 + +Midway 提供 `@SetHeader` 装饰器或者通过 API 来简单的设置自定义响应头。 + +**示例:使用装饰器** + +```typescript +import { Controller, Get, SetHeader } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + @SetHeader('x-bbb', '123') + async home() { + return "Hello Midwayjs!"; + } +} + +``` +当有多个响应头需要修改的时候,你可以直接传入对象。 + + +```typescript +import { Controller, Get, SetHeader } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + @SetHeader({ + 'x-bbb': '123', + 'x-ccc': '234' + }) + async home() { + return "Hello Midwayjs!"; + } +} + +``` +**示例:使用 API** + +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.set('x-bbb', '123'); + // ... + } +} +``` + +:::info +响应头不能在响应流关闭后(response.end之后)修改。 +::: + +### 重定向 + +如果需要简单的将某个路由重定向到另一个路由,可以使用 `@Redirect` 装饰器。 `@Redirect` 装饰器的参数为一个跳转的 URL,以及一个可选的状态码,默认跳转的状态码为 `302` 。 + +此外,也可以通过 API 来跳转。 + +**示例:使用装饰器** + + +```typescript +import { Controller, Get, Redirect } from '@midwayjs/core'; + +@Controller('/') +export class LoginController { + + @Get('/login_check') + async check() { + // TODO + } + + @Get('/login') + @Redirect('/login_check') + async login() { + // TODO + } + + @Get('/login_another') + @Redirect('/login_check', 302) + async loginAnother() { + // TODO + } +} +``` +**示例:使用 API** + +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.redirect('/login_check'); + // ... + } +} +``` + +:::info +重定向不能在响应流关闭后(response.end之后)修改。 +::: + + + + +### 响应类型 + +虽然浏览器会自动根据内容判断最佳的响应内容,但是我们经常会碰到需要手动设置的情况。我们也提供了 `@ContentType` 装饰器用于设置响应类型。 + +此外,也可以通过 API 来设置。 + +**示例:使用装饰器** + + +```typescript +import { Controller, Get, ContentType } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Get('/') + @ContentType('html') + async login() { + return 'hello world'; + } +} +``` +**示例:使用 API** + +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.type = 'html'; + // ... + } +} +``` + +:::info +响应类型不能在响应流关闭后(response.end之后)修改。 +::: + + + +### 流式响应 + +如果希望以流式返回数据,可以使用 Node.js 原始的 response 对象上的 `write` 和 `end` 方法。 + +```typescript +import { Controller, Get, Inject, sleep } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.status = 200; + this.ctx.set('Transfer-Encoding', 'chunked'); + for (let i = 0; i < 100; i++) { + await sleep(100); + this.ctx.res.write('abc'.repeat(100)); + } + + this.ctx.res.end(); + } +} +``` + + + +### 高级响应处理 + +从 v3.17.0 开始,框架提供了一个新的的 `HttpServerResponse` 来处理返回数据,除了普通的数据之外,还提供了自定义状态模版,文件下载,SSE 等能力支持,具体请查看 [数据响应章节](/docs/data_response) + + + +## 内部重定向 + +从 v3.12.0 开始,框架提供了一个内部重定向 API `ctx.forward(url)`,仅支持 koa/egg 类型。 + +和外部重定向不同的地方在于,内部重定向不会修改浏览器的 URL,只在程序内部流转。 + +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + return this.ctx.forward('/api'); + } + + @Get('/api') + async api() { + return 'abc'; + } +} +``` + +注意,内部重定向有一些规则: + +* 1、重定向会保留原始路由的所有参数,即透传整个 ctx + +* 2、重定向只能在相同的 http method 中进行 + +* 3、重定向不会再执行一遍 Web Middleware,不会执行守卫,但是会执行拦截器和参数装饰器 + + + +## 全局路由前缀 + +需要在 `src/config/config.default` 配置中设置。 + +注意,不同组件在不同的关键字配置下: + + + + +```typescript +// src/config/config.default.ts +export default { + koa: { + globalPrefix: '/v1' + } +}; +``` + + + +```typescript +// src/config/config.default.ts +export default { + egg: { + globalPrefix: '/v1' + } +}; +``` + + + + +```typescript +// src/config/config.default.ts +export default { + express: { + globalPrefix: '/v1' + } +}; +``` + + + + +配置后,所有的路由都会自动增加该前缀。 + +如有特殊路由不需要,可以使用装饰器参数忽略。 + +**示例:Controller 级别忽略** + +```typescript +// 该 Controller 下所有路由都将忽略全局前缀 +@Controller('/api', {ignoreGlobalPrefix: true}) +export class HomeController { + // ... +} +``` + +**示例:路由级别忽略** + +```typescript +@Controller('/') +export class HomeController { + // 该路由不会忽略 + @Get('/', {}) + async homeSet() { + } + + // 该路由会忽略全局前缀 + @Get('/bbc', {ignoreGlobalPrefix: true}) + async homeSet2() { + } +} +``` + + + +## 路由优先级 + + +midway 已经统一对路由做排序,通配的路径将自动降低优先级,在最后被加载。 + + +规则如下: + + +- 1、绝对路径规则优先级最高如 `/ab/cb/e` +- 2、星号只能出现最后且必须在/后面,`如 /ab/cb/**` +- 3、如果绝对路径和通配都能匹配一个路径时,绝对规则优先级高,比如 `/abc/*` 和 `/abc/d`,那么请求 `/abc/d` 时,会匹配到后一个绝对的路由 +- 4、有多个通配能匹配一个路径时,最长的规则匹配,如 `/ab/**` 和 `/ab/cd/**` 在匹配 `/ab/cd/f` 时命中 `/ab/cd/**` +- 5、如果 `/` 与 `/*` 都能匹配 `/` ,但 `/` 的优先级高于 `/*` +- 6、如果都为通配,但是其余权重都一样,比如 `/:page/page` 和 `/page/:page` ,那么两者权重等价,以编码加载顺序为准 + + + +此规则也与 Serverless 下函数的路由规则保持一致。 + + +简单理解为,“明确的路由优先级最高,长的路由优先级高,通配的优先级最低”。 + + +比如: +```typescript +@Controller('/api') +export class APIController { + @Get('/invoke/*') + async invokeAll() { + } + + @Get('/invoke/abc') + async invokeABC() { + } +} +``` +这种情况下,会先注册 `/invoke/abc` ,保证优先级更高。 + + +不同的 Controller 的优先级,我们会以长度进行排序, `/` 根 Controller 我们将会最后加载。 + diff --git a/site/versioned_docs/version-3.0.0/cookie_session.md b/site/versioned_docs/version-3.0.0/cookie_session.md new file mode 100644 index 000000000000..eed13937f699 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/cookie_session.md @@ -0,0 +1,355 @@ +# Cookies 和 Session + +HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。 Cookie 主要用于以下三个方面: + +- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) +- 个性化设置(如用户自定义设置、主题等) +- 浏览器行为跟踪(如跟踪分析用户行为等) + +Cookie 在 Web 应用中经常承担标识请求方身份的功能,所以 Web 应用在 Cookie 的基础上封装了 Session 的概念,专门用做用户身份识别。 + + + +## 适用范围 + +* `@midwayjs/web` 下(即 egg)内置的是 egg 自带的 Cookie,未提供替换能力,不适用本文档 +* `@midwayjs/express` 下(即 express)内置的是 express 自带的 Cookie 库,未提供替换能力,不适用本文档 + + + +## 默认的 Cookies + +Midway 提供了 `@midwayjs/cookies` 模块来操作 Cookie。 + +同时在 `@midwayjs/koa` 中,默认提供了从上下文直接读取、写入 cookie 的方法 + +- `ctx.cookies.get(name, [options])` 读取上下文请求中的 cookie +- `ctx.cookies.set(name, value, [options])` 在上下文中写入 cookie + +示例如下: + +```typescript +import { Inject, Controller, Get, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + // set cookie + this.ctx.cookies.set('foo', 'bar', { encrypt: true }); + // get cookie + this.ctx.cookies.get('foo', { encrypt: true }); + } +} +``` + + + +## 设置 Cookie + +使用 `ctx.cookies.set(key, value, options)` API 来设置 Cookie。 + +设置 Cookie 其实是通过在 HTTP 响应中设置 set-cookie 头完成的,每一个 set-cookie 都会让浏览器在 Cookie 中存一个键值对。在设置 Cookie 值的同时,协议还支持许多参数来配置这个 Cookie 的传输、存储和权限。 + +这些选项包括: + +| 选项 | 类型 | 描述 | 支持版本 | +| ------------------- | ------- | ------------------------------------------------------------ | -------------------------- | +| path | String | 设置键值对生效的 URL 路径,默认设置在根路径上(`/`),也就是当前域名下的所有 URL 都可以访问这个 Cookie。 | | +| domain | String | 设置键值对生效的域名,默认没有配置,可以配置成只在指定域名才能访问。 | | +| expires | Date | 设置这个键值对的失效时间,如果设置了 maxAge,expires 将会被覆盖。如果 maxAge 和 expires 都没设置,Cookie 将会在浏览器的会话失效(一般是关闭浏览器时)的时候失效。 | | +| maxAge | Number | 设置这个键值对在浏览器的最长保存时间。是一个从服务器当前时刻开始的毫秒数。如果设置了 maxAge,expires 将会被覆盖。 | | +| secure | Boolean | 设置键值对 [只在 HTTPS 连接上传输](http://stackoverflow.com/questions/13729749/how-does-cookie-secure-flag-work),框架会帮我们判断当前是否在 HTTPS 连接上自动设置 secure 的值。 | | +| httpOnly | Boolean | 设置键值对是否可以被 js 访问,默认为 true,不允许被 js 访问 | | +| partitioned | Boolean | 设置独立分区状态([CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips))的 Cookie。注意,只有 `secure` 为 true 且 Chrome >=114 版本此配置才会生效 | @midwayjs/cookies >= 1.1.0 | +| removeUnpartitioned | Boolean | 是否删除非独立分区状态的同名 cookie。注意,只有 `partitioned` 为 true 的时候此配置才会生效 | @midwayjs/cookies >= 1.2.0 | +| priority | String | 设置 Cookie 的 [优先级](https://developer.chrome.com/blog/new-in-devtools-81?hl=zh-cn#cookiepriority),可选值为 `Low`、`Medium`、`High`,仅对 Chrome >= 81 版本有效 | @midwayjs/cookies >= 1.1.0 | + +除了这些属性之外,框架另外扩展了 3 个参数: + +| 选项 | 类型 | 描述 | +| --------- | ------- | ------------------------------------------------------------ | +| overwrite | Boolean | 设置 key 相同的键值对如何处理,如果设置为 true,则后设置的值会覆盖前面设置的,否则将会发送两个 set-cookie 响应头。 | +| signed | Boolean | 设置是否对 Cookie 进行签名,如果设置为 true,则设置键值对的时候会同时对这个键值对的值进行签名,后面取的时候做校验,可以防止前端对这个值进行篡改。默认为 true。 | +| encrypt | Boolean | 设置是否对 Cookie 进行加密,如果设置为 true,则在发送 Cookie 前会对这个键值对的值进行加密,客户端无法读取到 Cookie 的明文值。默认为 false。 | + +在设置 Cookie 时,我们需要考虑这个 Cookie 是否需要被前端获取,失效时间多久等等。 + +示例: + +```typescript +import { Inject, Controller, Get, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.ctx.cookies.set('cid', 'hello world', { + domain: 'localhost', // 写cookie所在的域名 + path: '/index', // 写cookie所在的路径 + maxAge: 10 * 60 * 1000, // cookie有效时长 + expires: new Date('2017-02-15'), // cookie失效时间 + httpOnly: false, // 是否只用于http请求中获取 + overwrite: false, // 是否允许重写 + }); + ctx.body = 'cookie is ok'; + } +} +``` + +**默认的配置下,Cookie 是加签不加密的,浏览器可以看到明文,js 不能访问,不能被客户端(手工)篡改。** + +如果想要 Cookie 在浏览器端可以被 js 访问并修改: + +```typescript +ctx.cookies.set(key, value, { + httpOnly: false, + signed: false, +}); +``` + +如果想要 Cookie 在浏览器端不能被修改,不能看到明文: + +```typescript +ctx.cookies.set(key, value, { + httpOnly: true, // 默认就是 true + encrypt: true, // 加密传输 +}); +``` + + + +## 获取 Cookie + +使用 `ctx.cookies.get(key, options)` API 来获取 Cookie。 + +由于 HTTP 请求中的 Cookie 是在一个 header 中传输过来的,通过框架提供的这个方法可以快速的从整段 Cookie 中获取对应的键值对的值。上面在设置 Cookie 的时候,我们可以设置 `options.signed` 和 `options.encrypt` 来对 Cookie 进行签名或加密,因此对应的在获取 Cookie 的时候也要传相匹配的选项。 + +- 如果设置的时候指定为 signed,获取时未指定,则不会在获取时对取到的值做验签,导致可能被客户端篡改。 +- 如果设置的时候指定为 encrypt,获取时未指定,则无法获取到真实的值,而是加密过后的密文。 + +如果要获取前端或者其他系统设置的 Cookie,需要指定参数 `signed` 为 `false`,避免对它做验签导致获取不到 Cookie 的值。 + +```typescript +ctx.cookies.get('frontend-cookie', { + signed: false, +}); +``` + + + +## Cookie 秘钥 + +由于我们在 Cookie 中需要用到加解密和验签,所以需要配置一个秘钥供加密使用。 + +默认脚手架会在配置文件 `src/config/config.default.ts` 中自动生成一个秘钥,也可以自己修改。 + +```typescript +// src/config/config.default +export default { + keys: ['key1','key2'], +} +``` + +keys 默认是一个字符串,可以分隔配置多个 key。Cookie 在使用这个配置进行加解密时: + +- 加密和加签时只会使用第一个秘钥。 +- 解密和验签时会遍历 keys 进行解密。 + +如果我们想要更新 Cookie 的秘钥,但是又不希望之前设置到用户浏览器上的 Cookie 失效,可以将新的秘钥配置到 keys 最前面,等过一段时间之后再删去不需要的秘钥即可。 + + + +## 默认的 Session + +默认的 `@midwayjs/koa` ,内置了 Session 组件,给我们提供了 `ctx.session` 来访问或者修改当前用户 Session 。 + +```typescript +import { Inject, Controller, Get, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + // 获取 Session 上的内容 + const userId = this.ctx.session.userId; + const posts = await this.ctx.service.post.fetch(userId); + // 修改 Session 的值 + this.ctx.session.visited = ctx.session.visited ? (ctx.session.visited + 1) : 1; + // ... + } +} +``` + +Session 的使用方法非常直观,直接读取它或者修改它就可以了,如果要删除它,直接将它赋值为 null: + +```typescript +ctx.session = null; +``` + +需要 **特别注意** 的是:设置 session 属性时需要避免以下几种情况(会造成字段丢失,详见 [koa-session](https://github.com/koajs/session/blob/master/lib/session.js#L37-L47) 源码) + +- 不要以 `_` 开头 +- 不能为 `isNew` + +``` +// ❌ 错误的用法 +ctx.session._visited = 1; // --> 该字段会在下一次请求时丢失 +ctx.session.isNew = 'HeHe'; // --> 为内部关键字, 不应该去更改 + +// ✔️ 正确的用法 +ctx.session.visited = 1; // --> 此处没有问题 +``` + +Session 的实现是基于 Cookie 的,默认配置下,用户 Session 的内容加密后直接存储在 Cookie 中的一个字段中,用户每次请求我们网站的时候都会带上这个 Cookie,我们在服务端解密后使用。Session 的默认配置如下: + +```typescript +export default { + session: { + maxAge: 24 * 3600 * 1000, // 1天 + key: 'MW_SESS', + httpOnly: true, + }, + // ... +} +``` + +可以看到这些参数除了 `key` 都是 Cookie 的参数,`key` 代表了存储 Session 的 Cookie 键值对的 key 是什么。在默认的配置下,存放 Session 的 Cookie 将会加密存储、不可被前端 js 访问,这样可以保证用户的 Session 是安全的。 + + + +## 函数下的 Session + +在函数弹性容器的场景下,默认未内置 Session 模块,如果需要可以手动添加。 + +```json +{ + "dependencies": { + "@midwayjs/session": "^3.0.0", + // ... + }, +} +``` + +在 configuration 中引入组件。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as faas from '@midwayjs/faas'; +import * as session from '@midwayjs/session'; + +@Configuration({ + imports: [ + faas, + session, + // ... + ] +}) +export class MainConfiguration { + // ... +} +``` + + + +## Session 示例 + +### 修改用户 Session 失效时间 + +虽然在 Session 的配置中有一项是 maxAge,但是它只能全局设置 Session 的有效期,我们经常可以在一些网站的登陆页上看到有 **记住我** 的选项框,勾选之后可以让登陆用户的 Session 有效期更长。这种针对特定用户的 Session 有效时间设置我们可以通过 `ctx.session.maxAge=` 来实现。 + +```typescript +import { Inject, Controller, Post, Body, Provide, FORMAT } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { UserService } from './service/user.service'; + +@Controller('/') +export class UserController { + @Inject() + ctx: Context; + + @Inject() + userService: UserService; + + @Post('/') + async login(@Body() data) { + const { username, password, rememberMe } = data; + const user = await this.userService.loginAndGetUser(username, password); + + // 设置 Session + this.ctx.session.user = user; + // 如果用户勾选了 `记住我`,设置 30 天的过期时间 + if (rememberMe) { + this.ctx.session.maxAge = FORMAT.MS.ONE_DAY * 30; + } + } +} +``` + + + +### 延长用户 Session 有效期 + +默认情况下,当用户请求没有导致 Session 被修改时,框架都不会延长 Session 的有效期,但是在有些场景下,我们希望用户如果长时间都在访问我们的站点,则延长他们的 Session 有效期,不让用户退出登录态。框架提供了一个 `renew` 配置项用于实现此功能,它会在发现当用户 Session 的有效期仅剩下最大有效期一半的时候,重置 Session 的有效期。 + +```typescript +// src/config/config.default.ts +export default { + session: { + renew: true, + // ... + }, + // ... +} +``` + + + +### 调整 SameSite 配置以允许跨域访问 + +默认情况下,框架不会设置 Session Cookie 的 SameSite 选项。从 Chrome 84 版本开始,SameSite 选项为空的 Cookie 默认将不会在跨域请求时发送,即默认按照 SameSite=Lax 处理。一般情况下,如果用户都是直接访问你的应用,这不会有问题。如果你的应用需要支持跨域访问,比如被其他应用 iframe 嵌入,或者允许配置 CORS 跨域请求,则需要调整 SameSite 选项,将其设置为更为宽松的 SameSite=None: + +```typescript +// src/config/config.default.ts +export default { + session: { + sameSite: 'none', + // 需要指定 Secure,否则 SameSite=None 无效 + secure: true, + // ... + }, + // ... +} +``` + +可以阅读 [SameSite Cookie 说明](https://web.dev/articles/samesite-cookies-explained?hl=zh-cn) 了解更多 SameSite 选项。 + + + +## 自定义 Session Store + +过多的将数据放在 Session 中并不太合理,大部分情况下,我们只需要在 Session 中存储一些 Id,来保证安全性。虽然我们觉得 Cookie 作为存储 Session 已经足够,但是在某些极端情况下,还是需要使用例如 Redis 来存储 Session 的情况。 + +不同的上层框架使用了不同的 Session 方案,下面列举了一些 Session 替换方案 + +- [@midwayjs/koa 方案](https://github.com/midwayjs/midway/tree/main/packages/session#custom-session-store) +- [@midwayjs/express 方案](https://github.com/midwayjs/midway/tree/main/packages/express-session) +- [@midwayjs/web(egg)方案](https://github.com/eggjs/egg-session) + + + + + diff --git a/site/versioned_docs/version-3.0.0/cusom_response.md b/site/versioned_docs/version-3.0.0/cusom_response.md new file mode 100644 index 000000000000..8f484f4db37e --- /dev/null +++ b/site/versioned_docs/version-3.0.0/cusom_response.md @@ -0,0 +1,8 @@ +# 自定义数据响应 + +在大多数正常的逻辑中,返回数据只需要 `return` 相应的对象。 + +```typescript + +``` + diff --git a/site/versioned_docs/version-3.0.0/custom_decorator.md b/site/versioned_docs/version-3.0.0/custom_decorator.md new file mode 100644 index 000000000000..3a92838731d3 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/custom_decorator.md @@ -0,0 +1,562 @@ +# 自定义装饰器 + +在新版本中,Midway 提供了由框架支持的自定义装饰器能力,它包括几个常用功能: + +- 定义可继承的属性装饰器 +- 定义可包裹方法,做拦截的方法装饰器 +- 定义修改参数的参数装饰器 + +我们考虑到了装饰器当前在标准中的阶段以及后续风险,Midway 提供的自定义装饰器方式及其配套能力由框架实现,以尽可能的规避后续规范变化带来的问题。 + +一般,我们推荐将自定义装饰器放到 `src/decorator` 目录中。 + +比如: + +``` +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ ├── user.controller.ts +│ │ └── home.controller.ts +│ ├── interface.ts +│ ├── decorator ## 自定义装饰器 +│ │ └── user.decorator.ts +│ └── service +│ └── user.service.ts +├── test +├── package.json +└── tsconfig.json +``` + + + +## 装饰器 API + +Midway 内部有一套标准的装饰器管理 API,用来将装饰器对接依赖注入容器,实现扫描和扩展,这些 API 方法我们都从 `@midwayjs/core` 包进行导出。 + +通过装饰器高级 API,我们可以自定义装饰器,并且将元数据附加其中,内部的各种装饰器都是通过该能力实现的。 + +常见的扩展 API 有: + +**装饰器** + +- `saveModule` 用于保存某个类到某个装饰器 +- `listModule` 获取所有绑定到某类型装饰器的 class + +**元信息存取 (对应** [**reflect-metadata**](https://www.npmjs.com/package/reflect-metadata)**)** + +- `saveClassMetadata` 保存元信息到 class +- `attachClassMetadata` 附加元信息到 class +- `getClassMetadata` 从 class 获取元信息 +- `savePropertyDataToClass` 保存属性的元信息到 class +- `attachPropertyDataToClass` 附加属性的元信息到 class +- `getPropertyDataFromClass` 从 class 获取属性元信息 +- `listPropertyDataFromClass` 列出 class 上保存的所有的属性的元信息 +- `savePropertyMetadata` 保存属性元信息到属性本身 +- `attachPropertyMetadata` 附加属性元信息到属性本身 +- `getPropertyMetadata` 从属性上获取保存的元信息 + +**快捷操作** + +- `getProviderUUId`获取 class provide 出来的 uuid,对应某个类,不会变 +- `getProviderName` 获取 provide 时保存的 name,一般为类名小写 + +- `getProviderId` 获取 class 上 provide 出来的 id,一般为类名小写,也可能是自定义的 id +- `isProvide` 判断某个类是否被 @Provide 修饰过 +- `getObjectDefinition` 获取对象定义(ObjectDefiniton) +- `getParamNames` 获取一个函数的所有参数名 +- `getMethodParamTypes` 获取某个方法的参数类型,等价于 `Reflect.getMetadata(design:paramtypes)` +- `getPropertyType` 获取某个属性的类型,等价于 `Reflect.getMetadata(design:type)` +- `getMethodReturnTypes` 获取方法返回值类型,等价于 `Reflect.getMetadata(design:returntype)` + + + +## 类装饰器 + +一般类装饰器都会和其他装饰器配合使用,用来标注某个类属于特定的一种场景,比如 `@Controller` 表示了类属于 Http 场景的入口。 + +我们举一个例子,定义一个类装饰器 @Model ,标识 class 是一个模型类,然后进一步操作。 + +首先创建一个装饰器文件,比如 `src/decorator/model.decorator.ts` 。 + +```typescript +import { Scope, ScopeEnum, saveClassMetadata, saveModule, Provide } from '@midwayjs/core'; + +// 提供一个唯一 key +export const MODEL_KEY = 'decorator:model'; + +export function Model(): ClassDecorator { + return (target: any) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + saveModule(MODEL_KEY, target); + // 保存一些元数据信息,任意你希望存的东西 + saveClassMetadata( + MODEL_KEY, + { + test: 'abc', + }, + target + ); + // 指定 IoC 容器创建实例的作用域,这里注册为请求作用域,这样能取到 ctx + Scope(ScopeEnum.Request)(target); + + // 调用一下 Provide 装饰器,这样用户的 class 可以省略写 @Provide() 装饰器了 + Provide()(target); + }; +} +``` + +上面只是定义了这个装饰器,我们还要实现相应的功能,midway v2 开始有生命周期的概念,可以在 `configuration` 中的生命周期中执行。 + +```typescript +// src/configuration.ts + +import { listModule, Configuration, App, Inject } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { MODEL_KEY } from './decorator/model.decorator'; + +@Configuration({ + imports: [koa], +}) +export class MainConfiguration { + @App() + app: koa.Application; + + async onReady() { + // ... + + // 可以获取到所有装饰了 @Model() 装饰器的 class + const modules = listModule(MODEL_KEY); + for (let mod of modules) { + // 实现自定义能力 + // 比如,拿元数据 getClassMetadata(mod) + // 比如,提前初始化 app.applicationContext.getAsync(mod); + } + } +} +``` + +最后,我们要使用这个装饰器。 + +```typescript +import { Model } from '../decorator/model.decorator'; + +// Model 的作用是我们自己的逻辑能被执行(保存的元数据) +@Model() +export class UserModel { + // ... +} +``` + + + +## 属性装饰器 + +Midway 提供了 `createCustomPropertyDecorator` 方法,用于创建自定义属性装饰器,框架的 `@Logger` ,`@Config` 等装饰器都是这样创建而来的。 + +和 TypeScript 中定义的装饰器不同的是,Midway 提供的属性装饰器,可以在继承中使用。 + +我们举个例子,假如现在有一个内存缓存,我们的属性装饰器用于获取缓存数据,下面是一些准备工作。 + +```typescript +// 简单的缓存类 +import { Configuration, Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MemoryStore extends Map { + save(key, value) { + this.set(key, value); + } + + get(key) { + return this.get(key); + } +} + +// src/configuration.ts +// 入口实例化,并保存一些数据 +import { Configuration, App, Inject } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + imports: [koa], +}) +export class MainConfiguration { + @App() + app: koa.Application; + + @Inject() + store: MemoryStore; + + async onReady() { + // ... + + // 初始化一些数据 + store.save('aaa', 1); + store.save('bbb', 1); + } +} +``` + +我们来实现一个简单的 `@MemoryCache()` 装饰器。属性装饰器的实现分为两部分: + +- 1、定义一个装饰器方法,一般只保存元数据 +- 2、定义一个实现,在装饰器逻辑执行前即可 + +下面是定义装饰器方法的部分。 + +```typescript +// src/decorator/memoryCache.decorator.ts +import { createCustomPropertyDecorator } from '@midwayjs/core'; + +// 装饰器内部的唯一 id +export const MEMORY_CACHE_KEY = 'decorator:memory_cache_key'; + +export function MemoryCache(key?: string): PropertyDecorator { + return createCustomPropertyDecorator(MEMORY_CACHE_KEY, { + key, + }); +} +``` + +在装饰器的方法执行之前(一般在初始化的地方)去实现。实现装饰器,我们需要用到内置的 `MidwayDecoratorService` 服务。 + +```typescript +import { Configuration, Inject, Init, MidwayDecoratorService } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { MEMORY_CACHE_KEY, MemoryStore } from 'decorator/memoryCache.decorator'; + +@Configuration({ + imports: [koa], +}) +export class MainConfiguration { + @App() + app: koa.Application; + + @Inject() + store: MemoryStore; + + @Inject() + decoratorService: MidwayDecoratorService; + + @Init() + async init() { + // ... + + // 实现装饰器 + this.decoratorService.registerPropertyHandler(MEMORY_CACHE_KEY, (propertyName, meta) => { + return this.store.get(meta.key); + }); + } +} +``` + +`registerPropertyHandler` 方法包含两个参数,第一个是之前装饰器定义的唯一 id,第二个是装饰器实现的回调方法。 + +`propertyName` 是装饰器装饰的方法名,meta 是装饰器的使用时的参数。 + +然后我们就能使用这个装饰器了。 + +```typescript +import { MemoryCache } from 'decorator/memoryCache.decorator'; + +// ... +export class UserService { + @MemoryCache('aaa') + cacheValue; + + async invoke() { + console.log(this.cacheValue); + // => 1 + } +} +``` + + + +## 方法装饰器 + +Midway 提供了 `createCustomMethodDecorator` 方法,用于创建自定义方法装饰器。 + +和 TypeScript 中定义的装饰器不同的是,Midway 提供的方法装饰器,由拦截器统一实现,和其他拦截方式不冲突,并且更加简单。 + +我们以打印方法执行时间为例。 + +和属性装饰器相同,我们的定义与实现是分离的。 + +下面是定义装饰器方法的部分。 + +```typescript +// src/decorator/logging.decorator.ts +import { createCustomMethodDecorator } from '@midwayjs/core'; + +// 装饰器内部的唯一 id +export const LOGGING_KEY = 'decorator:logging_key'; + +export function LoggingTime(formatUnit = 'ms'): MethodDecorator { + // 我们传递了一个可以修改展示格式的参数 + return createCustomMethodDecorator(LOGGING_KEY, { formatUnit }); +} +``` + +实现的部分,同样需要使用框架内置的 `DecoratorService` 服务。 + +```typescript +//... + +function formatDuring(value, formatUnit: string) { + // 这里返回时间格式化 + if (formatUnit === 'ms') { + return `${value} ms`; + } else if (formatUnit === 'min') { + // return xxx + } +} + +@Configuration({ + imports: [koa], +}) +export class MainConfiguration { + @App() + app: koa.Application; + + @Inject() + decoratorService: MidwayDecoratorService; + + @Logger() + logger; + + async onReady() { + // ... + + // 实现方法装饰器 + this.decoratorService.registerMethodHandler(LOGGING_KEY, (options) => { + return { + around: async (joinPoint: JoinPoint) => { + // 拿到格式化参数 + const format = options.metadata.formatUnit || 'ms'; + + // 记录开始时间 + const startTime = Date.now(); + + // 执行原方法 + const result = await joinPoint.proceed(...joinPoint.args); + + const during = formatDuring(Date.now() - startTime, format); + + // 打印执行时间 + this.logger.info(`Method ${joinPoint.methodName} invoke during ${during}`); + + // 返回执行结果 + return result; + }, + }; + }); + } +} +``` + +`registerMethodHandler` 方法的第一个参数是装饰器定义的 id,第二个参数是回调的实现,参数为 options 对象,包含: + +| 参数 | 类型 | 描述 | +| -------------------- | ------------- | ---------------------- | +| options.target | new (...args) | 装饰器修饰所在的类 | +| options.propertyName | string | 装饰器修饰所在的方法名 | +| options.metadata | {} | 装饰器本身的参数 | + +回调的实现,需要返回一个由拦截器处理的方法,key 为拦截器的 `before`,`around`,`afterReturn`,`afterThrow`,`after` 这几个可拦截的生命周期。 + +由于方法装饰器本身是拦截器实现的,所以具体的拦截方法可以查看 [拦截器](aspect) 部分。 + +使用装饰器如下: + +```typescript +// ... +export class UserService { + @LoggingTime() + async getUser() { + // ... + } +} + +// 执行时 +// output => Method "getUser" invoke during 4ms +``` + +:::caution + +注意,被装饰的方法必须为 async 方法。 + +::: + + + +## 无需实现的方法装饰器 + +默认情况下,自定义的方法装饰器必须有一个实现,否则运行期会报错。 + +在某些特殊情况,希望有一个无需实现的装饰器,比如只需要存储元数据而不做拦截。 + +可以在定义装饰器的时候,增加一个 impl 参数。 + +```typescript +// src/decorator/logging.decorator.ts +import { createCustomMethodDecorator } from '@midwayjs/core'; + +// 装饰器内部的唯一 id +export const LOGGING_KEY = 'decorator:logging_key'; + +export function LoggingTime(): MethodDecorator { + // 最后一个参数告诉框架,无需指定实现 + return createCustomMethodDecorator(LOGGING_KEY, {}, false); +} +``` + +## 参数装饰器 + +Midway 提供了 `createCustomParamDecorator` 方法,用于创建自定义参数装饰器。 + +参数装饰器,一般用于修改参数值,提前预处理数据等,Midway 的 `@Query` 等请求系列的装饰器都基于其实现。 + +和其他装饰器相同,我们的定义与实现是分离的,我们以获取参数中的用户(ctx.user)来举例。 + +下面是定义装饰器方法的部分。 + +```typescript +// src/decorator/logging.decorator.ts +import { createCustomParamDecorator } from '@midwayjs/core'; + +// 装饰器内部的唯一 id +export const USER_KEY = 'decorator:user_key'; + +export function User(): ParameterDecorator { + return createCustomParamDecorator(USER_KEY, {}); +} +``` + +实现的部分,同样需要使用框架内置的 `DecoratorService` 服务。 + +```typescript +//... + +@Configuration({ + imports: [koa], +}) +export class MainConfiguration { + @App() + app: koa.Application; + + @Inject() + decoratorService: MidwayDecoratorService; + + @Logger() + logger; + + async onReady() { + // ... + + // 实现参数装饰器 + this.decoratorService.registerParameterHandler(USER_KEY, (options) => { + // originArgs 是原始的方法入参 + // 这里第一个参数是 ctx,所以取 ctx.user + return options.originArgs[0]?.user ?? {}; + }); + } +} +``` + +`registerParameterHandler` 方法的第一个参数是装饰器定义的 id,第二个参数是回调的实现,参数为 options 对象,包含: + +| 参数 | 类型 | 描述 | +| ----------------------- | --------------- | ---------------------- | +| options.target | new (...args) | 装饰器修饰所在的类 | +| options.propertyName | string | 装饰器修饰所在的方法名 | +| options.metadata | {} \| undefined | 装饰器本身的参数 | +| options.originArgs | Array | 方法原始的参数 | +| options.originParamType | | 方法原始的参数类型 | +| options.parameterIndex | number | 装饰器修饰的参数索引 | + +使用装饰器如下: + +```typescript +// ... +export class UserController { + + @Inject() + userService: UserService; + + @Inject() + ctx: Context; + + async getUser() { + return await this.getUser(ctx); + } +} + +export class UserService { + async getUser(@User() user: string) { + console.log(user); + // => xxx + } +} +``` + + +:::tip + +注意,为了方法调用的正确性,如果参数装饰器中报错,框架会使用原始的参数来调用方法,不会直接抛出异常。 + +你可以在开启 `NODE_DEBUG=midway:debug` 环境变量时找到这个错误。 + +::: + +:::caution + +注意,被装饰的方法必须为 async 方法。 + +::: + + + +## 方法装饰器获取上下文 + +在请求链路上,如果自定义了装饰器要获取上下文往往比较困难,如果代码没有显式的注入上下文,装饰器中获取会非常困难。 + +在 Midway 的依赖注入的请求作用域中,我们将上下文绑定到了每个实例上,从实例的特定属性 `REQUEST_OBJ_CTX_KEY` 上即可获取当前的上下文,从而进一步对请求做操作。 + +比如在我们自定义实现的方法装饰器中: + +```typescript +import { REQUEST_OBJ_CTX_KEY } from '@midwayjs/core'; +//... + +export class MainConfiguration { + @App() + app: koa.Application; + + @Inject() + decoratorService: MidwayDecoratorService; + + @Logger() + logger; + + async onReady() { + // ... + + // 实现方法装饰器 + this.decoratorService.registerMethodHandler(LOGGING_KEY, (options) => { + return { + around: async (joinPoint: JoinPoint) => { + // 装饰器所在的实例 + const instance = joinPoint.target; + const ctx = instance[REQUEST_OBJ_CTX_KEY]; + // ctx.xxxx + // ... + }, + }; + }); + } +} +``` diff --git a/site/versioned_docs/version-3.0.0/custom_error.md b/site/versioned_docs/version-3.0.0/custom_error.md new file mode 100644 index 000000000000..3f009cbb37d5 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/custom_error.md @@ -0,0 +1,165 @@ +# 自定义错误 + +在 Node.js 中,每个异常都是内置的 Error 类型的实例。 + +通过扩展标准 Error,Midway 提供了内置的错误类型,额外增加了一些属性。 + +```typescript +export class MidwayError extends Error { + // ... +} +``` + +现阶段,所有 Midway 框架提供的错误,都是该错误类抛出的实例。 + +MidwayError 包括几个属性: + +- name 错误的名字,比如 Error,TypeError 等,在自定义错误中,为自定义错误的类名 +- message 错误的消息 +- stack 错误的堆栈 +- code 自定义错误码 +- cause 错误的来源 + + + +我们可以通过简单的实例化并且抛出来使用,比如: + +```typescript +import { MidwayError } from '@midwayjs/core'; + +// ... + +async findAll() { + throw new MidwayError('my custom error'); +} +``` + +也可以在业务中自定义一些错误。 + +常见的,我们会把异常统一定义到 error 目录中。 + +``` +➜ my_midway_app tree +. +├── src +│ └── error +│ ├── customA.error.ts +│ └── customB.error.ts +├── test +├── package.json +└── tsconfig.json +``` + +如果业务有一些复用的异常,比如固定的错误 + +```typescript +// src/error/custom.error.ts +import { MidwayError } from '@midwayjs/core'; + +export class CustomError extends MidwayError { + constructor() { + super('my custom error', 'CUSTOM_ERROR_CODE_10000'); + } +} +``` + +然后在业务中抛出使用。 + +```typescript +import { CustomError } from './error/custom.error'; + +// ... + +async findAll() { + throw new CustomError(); +} + +``` + +上面的 `CUSTOM_ERROR_CODE_10000` 为错误的错误码,一般我们会为不同的错误分配不同的错误码和错误消息,以方便排查问题。 + + + +## 自定义错误码 + +框架提供了一种通用的注册错误码的机制,错误码后期可以方便的排错,统计。 + +在业务的错误定义,以及组件错误定义的时候非常有用。 + +错误码一般是个枚举值,比如: + +```typescript +const CustomErrorEnum = { + UNKNOWN: 10000, + COMMON: 10001, + PARAM_TYPE: 10002, + // ... +}; +``` + +在编码中,我们会提供固定的错误码,并且希望在 SDK 或者组件中不冲突,这就需要框架来支持。 + +Midway 提供了 `registerErrorCode` 方法,用于向框架注册不重复的错误码,并且进行一定的格式化。 + +比如,在框架内部,我们有如下的定义: + +```typescript +import { registerErrorCode } from '@midwayjs/core'; + +export const FrameworkErrorEnum = registerErrorCode('midway', { + UNKNOWN: 10000, + COMMON: 10001, + PARAM_TYPE: 10002, + // ... +} as const); +``` + +`registerErrorCode` 包含两个参数: + +- 错误分组,比如上面的 `midway` ,就是框架内置错误组名,在一个应用中,这个组名不应该重复 +- 错误枚举对象,以错误名为 key,错误码为 value + + + +方法会返回一个错误枚举值,枚举值会以错误名作为 key,错误分组加错误码作为 value。 + +比如: + +```typescript +FrameworkErrorEnum.UNKNOWN +// => output: MIDWAY_10000 + +FrameworkErrorEnum.COMMON +// => output: MIDWAY_10001 +``` + +这样,当错误中出现 `MIDWAY_10000` 的错误码时,我们就知道是什么错误了,配合文档就可以沉淀所有的错误。 + +在错误定义时,直接使用这个错误码枚举即可。 + +```typescript +export class MidwayParameterError extends MidwayError { + constructor(message?: string) { + super(message ?? 'Parameter type not match', FrameworkErrorEnum.PARAM_TYPE); + } +} + +// user code +async findAll(data) { + if (!data.user) { + throw new MidwayParameterError(); + } + // ... +} + +// output +// 2022-01-02 14:02:29,124 ERROR 14259 MidwayParameterError: Parameter type not match +// at APIController.findAll (.... +// at /Users/harry/project/midway-v3/packages/core/src/common/webGenerator.ts:38:57 +// at processTicksAndRejections (node:internal/process/task_queues:96:5) { +// code: 'MIDWAY_10002', +// cause: undefined, +// } + +``` + diff --git a/site/versioned_docs/version-3.0.0/data_listener.md b/site/versioned_docs/version-3.0.0/data_listener.md new file mode 100644 index 000000000000..d973b064e840 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/data_listener.md @@ -0,0 +1,122 @@ +# 数据订阅 + +在某些场景下,我们希望订阅某个数据,并且在一段时间后更新它,这种类似订阅的方式,我们称之为 ”数据订阅“,常见的远程数据获取等,都可以应用这个模式。 + +Midway 提供了 `DataListener` 的抽象,用于方便的创建这种模式的代码。 + +## 实现数据订阅 + +我们以一个简单的 **内存数据更新** 的需求为例。 + +数据订阅在 midway 中也是一个普通的类,比如我们也可以把他放到 `src/listener/memory.listner.ts` 中。 + +我们只需要继承内置的 `DataListener` 类,同时,一般数据订阅类为单例。 + +`DataListener` 包含一个泛型类型,需要声明该数据订阅返回的数据类型。 + +比如: + +```typescript +// src/listener/memory.listner.ts +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; +import { DataListener } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MemoryDataListener extends DataListener { + // 初始化数据 + initData() { + return 'hello' + Date.now(); + } + + // 更新数据 + onData(setData) { + setInterval(() => { + setData('hello' + Date.now()); + }, 1000); + } +} +``` + +`DataListener` 类有两个必须实现的方法: + +- `initData` 数据的初始化方法 +- `onData` 数据订阅更新的方法 + +示例中,我们初始化了数据,同时实现了数据更新的方法,每隔 1 秒钟,我们会使用 `setData` 来更新内置的数据。 + +此外,大部分的数据订阅会使用到定时器,或者其他外部的 sdk,我们需要考虑好关闭和清理资源的情况。 + +代码中提供了 `destroyListener` 方法来处理。 + +比如上面的示例代码,我们需要关闭定时器。 + +```typescript +// src/listener/memory.listner.ts +import { Provide, Scope, ScopeEnum, DataListener } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MemoryDataListener extends DataListener { + private intervalHandler; + + // 初始化数据 + initData() { + return 'hello' + Date.now(); + } + + // 更新数据 + onData(setData) { + this.intervalHandler = setInterval(() => { + setData('hello' + Date.now()); + }, 1000); + } + + // 清理资源 + async destroyListener() { + // 关闭定时器 + clearInterval(this.intervalHandler); + // 其他清理, close sdk 等等 + } + +} +``` + +上面的 `initData` 方法可以异步获取数据。 + +```typescript +// ... +export class MemoryDataListener extends DataListener { + async initData() { + // ... + } +} +``` + + + +## 使用数据订阅 + +我们可以在任意的代码中使用它,在业务中通过 `getData` 方法来获取当前的数据,不需要考虑数据变化的情况。 + +比如: + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { MemoryDataListener } from '../listener/memory.listner.ts'; + +@Provide() +export class UserService { + + @Inject() + memoryDataListener: MemoryDataListener; + + async getUserHelloData() { + const helloData = this.memoryDataListener.getData(); + // helloData => helloxxxxxxxx + // ... + } +} +``` + +数据订阅模式可以方便的将变化的数据隐藏在普通类中,而透出不变化的 API,使得标准的业务代码在逻辑和流程上都变的简洁。 diff --git a/site/versioned_docs/version-3.0.0/data_response.md b/site/versioned_docs/version-3.0.0/data_response.md new file mode 100644 index 000000000000..85e551cac672 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/data_response.md @@ -0,0 +1,449 @@ +# 数据响应 + +从 v3.17.0 开始,框架添加了 `ServerResponse` 和 `HttpServerResponse` 的实现。 + +通过这个功能,可以定制服务端的响应成功和失败时的通用格式,规范整个返回逻辑。 + + + +## Http 通用响应 + +在 koa 场景下,一般都会处理一些逻辑,最后返回一个结果。在此过程中,会出现返回成功和失败的情况。 + +最为常见实现会在 `ctx` 增加一些方法,包括数据后返回。 + +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + try { + // ... + return this.ctx.ok(/*...*/); + } catch (err) { + return this.ctx.fail(/*...*/); + } + } +} +``` + +也有人会在 Web 中间件中处理成功的返回,在错误过滤器中处理失败的返回。 + +为了解决这类代码难以统一维护的问题,框架提供了一套统一返回的方案。 + +我们以最为常见的返回 JSON 数据为例。 + +通过创建 `HttpServerResponse` 实例后,调用 `json()` 方法,链式返回数据。 + +```typescript +import { Controller, Get, Inject, HttpServerResponse } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/success') + async home() { + return new HttpServerResponse(this.ctx).success().json({ + // ... + }); + } + + @Get('/fail') + async home2() { + return new HttpServerResponse(this.ctx).fail().json({ + // ... + }); + } +} +``` + +默认情况下,`HttpServerResponse` 在成功和失败的场景上会提供 JSON 的通用包裹结构。 + +比如在成功的场景下,接收到的数据如下。 + +```json +{ + success: 'true', + data: //... +} +``` + +而在失败的场景下,接收到的数据如下。 + +```typescript +{ + success: 'false', + message: //... +} +``` + +注意,`json()` 方法是数据设置的方法,必须在最后一个调用。 + + + +### 常用的响应格式 + +`HttpServerResponse` 需要传递一个当前请求的上下文对象 `ctx` 才能实例化。 + +```typescript +const serverResponse = new HttpServerResponse(this.ctx); +``` + +之后以链式的形式进行调用。 + +```typescript +// json +serverResponse.json({ + a: 1, +}); +// text +serverResponse.text('abcde'); +// blob +serverResponse.blob(Buffer.from('hello world')); +``` + +除了设置数据的方法,还提供了一些其他的快捷方法可以组合使用。 + +```typescript +// status +serverResponse.status(200).text('abcde'); +// header +serverResponse.header('Content-Type', 'text/html').text('
hello
'); +// headers +serverResponse.headers({ + 'Content-Type': 'text/plain', + 'Content-Length': '100' +}).text('a'.repeat(100)); + +``` + + + +### 响应模版 + +针对不同的设置数据的方法,框架提供了不同模版以供用户自定义。 + +比如 `json()` 方法的模版如下。 + +```typescript +class ServerResponse { + // ... + static JSON_TPL = (data: Record, isSuccess: boolean): unknown => { + if (isSuccess) { + return { + success: 'true', + data, + }; + } else { + return { + success: 'false', + message: data || 'fail', + }; + } + }; +} +``` + +我们可以将全局的模版进行覆盖达到自定义的目的。 + +```typescript +HttpServerResponse.JSON_TPL = (data, isSuccess) => { + if (isSuccess) { + // ... + } else { + // ... + } +}; +``` + +也可以通过继承,自定义不同的响应模版,这样可以不影响全局的默认模板。 + +```typescript +class CustomServerResponse extends HttpServerResponse {} +CustomServerResponse.JSON_TPL = (data, isSuccess) => { + if (isSuccess) { + // ... + } else { + // ... + } +}; +``` + +在使用时,创建实例即可。 + +```typescript +// ... + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + return new CustomServerResponse(this.ctx).success().json({ + // ... + }); + } +} +``` + +此外,针对 `text` ,`blob` 方法的模版均可以覆盖。 + +```typescript +HttpServerResponse.TEXT_TPL = (data, isSuccess) => { /*...*/}; +HttpServerResponse.BLOB_TPL = (data, isSuccess) => { /*...*/}; +``` + + + +### 数据流式响应 + +使用内置的 `HttpServerResponse` 中的 `stream` 方法来处理流式数据返回。 + +```typescript +import { Controller, Get, Inject, sleep, HttpServerResponse } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + const res = new HttpServerResponse(this.ctx).stream(); + setTimeout(() => { + for (let i = 0; i < 100; i++) { + await sleep(100); + res.send('abc'.repeat(100)); + } + + res.end(); + }, 1000); + return res; + } +} +``` + +通过 `STEAM_TPL` 可以修改数据的返回结构 + +```typescript +HttpServerResponse.STREAM_TPL = (data) => { /*...*/}; +``` + +注意,这个模版只处理成功的数据。 + + + +### 文件流式响应 + +从 v3.17.0 开始,可以通过 `HttpServerResponse` 简单处理文件下载。 + +传递一个文件路径即可,默认会使用 `application/octet-stream` 响应头返回。 + +```typescript +import { Controller, Get, Inject, sleep, HttpServerResponse } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + const filePath = join(__dirname, '../../package.json'); + return new HttpServerResponse(this.ctx).file(filePath); + } +} +``` + +如需返回不同的类型,可以通过第二个参数指定类型。 + +```typescript +import { Controller, Get, Inject, sleep, HttpServerResponse } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + const filePath = join(__dirname, '../../package.json'); + return new HttpServerResponse(this.ctx).file(filePath, 'application/json'); + } +} +``` + +通过 `FILE_TPL` 可以修改返回结构。 + +```typescript +HttpServerResponse.FILE_TPL = (data: Readable, isSuccess: boolean) => { /*...*/}; +``` + + + +### SSE 响应 + +从 v3.17.0 开始,框架提供了内置的 SSE (Server-Sent Events)支持。 + +SSE 的数据定义如下,你需要按下面的格式返回。 + +```typescript +export interface ServerSendEventMessage { + data?: string | object; + event?: string; + id?: string; + retry?: number; +} +``` + +通过 `HttpServerResponse` 定义一个返回实例。 + +```typescript +import { Controller, Get, Inject, sleep, HttpServerResponse } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + const res = new HttpServerResponse(this.ctx).sse(); + // ... + return res; + } +} +``` + +可以通过 `send` 和 `sendEnd` 进行数据传递。 + +```typescript +const res = new HttpServerResponse(this.ctx).sse(); + +res.send({ + data: 'abcde' +}); + +res.sendEnd({ + data: 'end' +}); +``` + +调用 `sendEnd` 后,请求将被关闭。 + +也可以通过 `sendError` 发送错误。 + +```typescript +const res = new HttpServerResponse(this.ctx).sse(); + +res.sendError(new Error('test error')); +``` + +通过 `SSE_TPL` 可以修改返回结构。 + +```typescript +import { ServerSendEventMessage } from '@midwayjs/core'; + +HttpServerResponse.FILE_TPL = (data: ServerSendEventMessage) => { /*...*/}; +``` + +注意,这个模版只处理成功的数据,不会处理 `sendError` 的情况,且返回也必须是 `ServerSendEventMessage` 格式。 + + + +## 基础数据响应 + +除了 Http 场景之外,框架提供了基础的 `ServerResponse` 类,用于其他的场景。 + +`ServerResponse` 包含 `json`,`text`,`blob` 三种数据返回方法,以及 `success` 和 `fail` 这两个设置状态的方法。 + +行为和 `HttpServerResponse` 一致。 + +通过继承、覆盖等行为,可以非常简单的处理响应值。 + +比如我们对不同的用户做返回区分。 + +```typescript +// src/response/api.ts +export class UserServerResponse extends HttpServerResponse {} +UserServerResponse.JSON_TPL = (data, isSuccess) => { + if (isSuccess) { + return { + status: 200, + ...data, + }; + } else { + return { + status: 500, + message: 'limit exceed' + }; + } +}; + +export class AdminServerResponse extends HttpServerResponse {} +AdminServerResponse.JSON_TPL = (data, isSuccess) => { + if (isSuccess) { + return { + status: 200, + router: data.router, + ...data + }; + } else { + return { + status: 500, + message: 'interal error', + ...data + }; + } +}; +``` + +使用返回。 + +```typescript +import { Controller, Get, Inject, sleep, HttpServerResponse } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { UserServerResponse, AdminServerResponse } from '../response/api'; + +@Controller('/') +export class HomeController { + @Inject() + ctx: Context; + + @Get('/') + async home() { + // ... + if (this.ctx.user === 'xxx') { + return new AdminServerResponse(this.ctx).json({ + router: '/', + dbInfo: { + // ... + }, + userInfo: { + role: 'admin', + }, + status: 'ok', + }); + } + return new UserServerResponse(this.ctx).json({ + status: 'ok', + }); + } +} +``` + diff --git a/site/versioned_docs/version-3.0.0/data_source.md b/site/versioned_docs/version-3.0.0/data_source.md new file mode 100644 index 000000000000..249649115ad2 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/data_source.md @@ -0,0 +1,364 @@ +# 数据源管理 + +在使用数据库包过程中,我们经常会有多库连接和管理的需求,不同数据库的连接池管理,连接状态,以及使用的方式都有一定的差异。 + +虽然我们可以使用服务工厂来进行抽象,但是不管是语义,还是部分功能,和服务工厂还是略有不同,比如实体类的加载等能力,这都是数据源特有的。 + +为此,Midway 提供了 `DataSourceManager` 的抽象,方便数据源的管理。 + +我们以 `mysql2` 来举例,实现一个 `mysql2` 的连接池管理类。 + +下面是 `mysql2` 官方的示例,作为准备工作。 + +```typescript +// get the client +const mysql = require('mysql2'); + +// create the connection to database +const connection = mysql.createConnection({ + host: 'localhost', + user: 'root', + database: 'test' +}); + +// simple query +connection.query( + 'SELECT * FROM `table` WHERE `name` = "Page" AND `age` > 45', + function(err, results, fields) { + console.log(results); // results contains rows returned by server + console.log(fields); // fields contains extra meta data about results, if available + } +); +``` + +和服务工厂类似,我们需要实现一些固定的方法。 + +- 1、创建数据源的方法 +- 2、检查连接的方法 + + + +## 实现数据源管理器 + +数据源管理器在 midway 中也是一个普通的导出类,比如我们也可以把他放到 `src/manager/mysqlDataSourceManager.ts` 中。 + + + +### 1、实现创建数据源接口 + +我们只需要继承内置的 `DataSourceManager` 类,就能实现一个数据源管理器。 + +`DataSourceManager` 包含一个泛型类型,需要声明该数据源的数据类型。 + +```typescript +import { Provide, Scope, ScopeEnum, DataSourceManager } from '@midwayjs/core'; +import * as mysql from 'mysql2'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MySqlDataSourceManager extends DataSourceManager { + // ... +} + +``` + +由于是抽象类,我们需要实现其中的几个基本方法。 + +```typescript +import { Provide, Scope, ScopeEnum, DataSourceManager } from '@midwayjs/core'; +import * as mysql from 'mysql2'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MySqlDataSourceManager extends DataSourceManager { + // 创建单个实例 + protected async createDataSource(config: any, dataSourceName: string): Promise { + return mysql.createConnection(config); + } + + getName(): string { + return 'mysql'; + } + + async checkConnected(dataSource: mysql.Connection): Promise { + // 伪代码 + return dataSource.status === 'connected'; + } + + async destroyDataSource(dataSource: mysql.Connection): Promise { + if (await this.checkConnected(dataSource)) { + await dataSource.destroy(); + } + } +} + +``` + + + +### 2、提供初始化配置 + +我们可以利用 `@Init` 装饰器和 `@Config` 装饰器提供初始化配置。 + +```typescript +import { Provide, Scope, ScopeEnum, Init, Config, DataSourceManager } from '@midwayjs/core'; +import * as mysql from 'mysql2'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class MySqlDataSourceManager extends DataSourceManager { + + @Config('mysql') + mysqlConfig; + + @Inject() + baseDir: string; + + @Init() + async init() { + // 需要注意的是,这里第二个参数需要传入一个实体类扫描地址 + await this.initDataSource(this.mysqlConfig, this.baseDir); + } + + // ... +} + + +``` + +在 `src/config/config.default` 中,我们可以提供多数据源的配置,来创建多个数据源。 + +比如: + +```typescript +// config.default.ts +export const mysql = { + dataSource: { + dataSource1: { + host: 'localhost', + user: 'root', + database: 'test' + }, + dataSource2: { + host: 'localhost', + user: 'root', + database: 'test' + }, + dataSource3: { + host: 'localhost', + user: 'root', + database: 'test' + }, + } + // 其他配置 +} +``` + +数据源天然就是为了多个实例而设计的,和服务工厂不同,没有单个和多个的配置区别。 + + + +## 实体绑定 + +数据源最重要的一环是实体类,每个数据源都可以拥有自己的实体类。比如 typeorm 等 orm 框架,都是基于此来设计的。 + + + +### 1、显式关联实体类 + +实体类一般是和表结构相同的类。 + +比如: + +```typescript +// src/entity/user.entity.ts +// 这里是伪代码,装饰器需要自行实现 +@Entity() +export class SimpleUser { + @Column() + name: string; +} + +@Entity() +export class User { + @Column() + name: string; + + @Column() + age: number; +} +``` + +数据源管理器通过固定的配置,将这些实体类和数据源进行绑定。 + +```typescript +// config.default.ts +import { User, SimpleUser } from '../entity/user.entity'; + +export default { + mysql: { + dataSource: { + dataSource1: { + host: 'localhost', + user: 'root', + database: 'test', + entities: [User] + }, + dataSource2: { + host: 'localhost', + user: 'root', + database: 'test', + entities: [SimpleUser] + }, + // ... + } + } +} +``` + +每个数据源的 `entities` 配置,都可以添加各自的实体类。 + + + +### 2、目录扫描关联实体 + +在某些情况下,我们也可以通过通配的路径来替代,比如: + +```typescript +// config.default.ts +import { User, SimpleUser } from '../entity/user.entity'; + +export default { + mysql: { + dataSource: { + dataSource1: { + host: 'localhost', + user: 'root', + database: 'test', + entities: [ + User, + SimpleUser, + 'entity', // 特定目录(等价于目录通配) + '**/abc/**', // 仅获取包含 abc 字符的目录下的文件 + 'abc/**/*.ts', // 特定目录 + 通配 + 'abc/*.entity.ts', // 匹配后缀 + '**/*.entity.ts', // 通配加后缀匹配 + '**/*.{j,t}s', // 后缀匹配 + ] + }, + // ... + // ... + } + } +} +``` + +:::caution + +注意 + +- 1、填写目录字符串时,以 initDataSource 方法的第二个参数作为相对路径查找,默认为 baseDir(src 或者 dist) +- 2、如果匹配后缀,entities 的路径注意包括 js 和 ts 后缀,否则编译后会找不到实体 +- 3、字符串路径的写法不支持 [单文件构建部署](./deployment#单文件构建部署)(bundle模式) + +::: + + + +### 2、根据实体获取数据源 + +一般我们的 API 都是在数据源对象上,比如 `connection.query`。 + +所以在很多时候,比如自定义装饰器,都需要一个从实体获取到数据源对象的方法。 + +```typescript +// 下面为伪代码 +import { SimpleUser } from '../entity/user.entity'; + +class UserService { + // 这里一般会注入一个实体类对应的 Model,包含增删改查方法 + @InjectEntityModel(SimpleUser) + userModel; + +} +``` + +如果在实体类仅对应一个数据源的情况下,我们可以通过 `getDataSourceNameByModel` 来获取数据源。 + +```typescript +this.mysqlDataSourceManager.getDataSourceNameByModel(SimpleUser); + +// => dataSource1 +``` + +多个的情况下,该方法获取的数据源不一定准确,会拿到最后设置的一个数据源。 + +这种时候一般需要用户手动指定数据源,比如: + +```typescript +// 下面为伪代码 +import { SimpleUser } from '../entity/user.entity'; + +class UserService { + @InjectEntityModel(SimpleUser, 'dataSource2') + userModel; + +} +``` + +也可以通过 `defaultDataSourceName` 配置显式指定默认的数据源。 + +```typescript +// config.default.ts +export const mysql = { + dataSource: { + dataSource1: { + // ... + }, + dataSource2: { + // ... + }, + dataSource3: { + // ... + }, + } + defaultDataSourceName: 'dataSource2', +} +``` + + + +## 获取数据源 + +通过注入数据源管理器,我们可以通过其上面的方法来拿到数据源。 + +```typescript +import { MySqlDataSourceManager } from './manager/mysqlDataSourceManager'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + mysqlDataSourceManager: MySqlDataSourceManager; + + async invoke() { + + const dataSource = this.mysqlDataSourceManager.getDataSource('dataSource1'); + // TODO + + } +} +``` + +此外,还有一些其他方法。 + +```typescript +// 数据源是否存在 +this.mysqlDataSourceManager.hasDataSource('dataSource1'); +// 获取所有的数据源名 +this.mysqlDataSourceManager.getDataSourceNames(); +// 数据源是否连接 +this.mysqlDataSourceManager.isConnected('dataSource1') +``` + diff --git a/site/versioned_docs/version-3.0.0/debugger.md b/site/versioned_docs/version-3.0.0/debugger.md new file mode 100644 index 000000000000..dcdae41438b5 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/debugger.md @@ -0,0 +1,79 @@ +# 调试 + +本章节介绍如何在常用编辑器中调试 Midway 项目。 + +## 在 VSCode 中调试 + +### 方法一:使用 JavaScript Debug Teminal + +在 VSCode 的终端下拉出,隐藏着一个 `JavaScript Debug Terminal` ,点击它,创建出来的终端将自带调试能力。 +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01HWzQEu1cQ6C7q9OYh_!!6000000003594-2-tps-1030-364.png) + +输入任意的命令都将自动开启 Debug,比如输入 `npm run dev` 后。 +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01nnkbOQ1YN79M1svVV_!!6000000003046-2-tps-1500-570.png) + + + +### 方法二:配置调试文件 + +创建一个 vscode 的启动文件。 +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01WzgZwN23WVMLYP4Xs_!!6000000007263-2-tps-645-344.png) +随便选一个,会创建 `.vscode/launch.json` 文件, +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01pP7ntf1HRNMmTeGBT_!!6000000000754-2-tps-655-231.png) + + +将下面内容复制进去。 + +```json +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [{ + "name": "Midway Local", + "type": "node", + "request": "launch", + "cwd": "${workspaceRoot}", + "runtimeExecutable": "npm", + "windows": { + "runtimeExecutable": "npm.cmd" + }, + "runtimeArgs": [ + "run", + "dev" + ], + "env": { + "NODE_ENV": "local" + }, + "console": "integratedTerminal", + "protocol": "auto", + "restart": true, + "port": 7001, + "autoAttachChildProcesses": true + }] +} + +``` + +启动断点即可。 +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01AGHSI51zZvrKgS9xx_!!6000000006729-2-tps-1470-1020.png) + + + +## 在 WebStorm/Idea 中调试 + +开始配置 IDE。 +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01bmrjiW1frz9dLpdEZ_!!6000000004061-2-tps-1110-692.png) + +配置 npm 命令。 +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01e4yJnU1QT3MOImlpR_!!6000000001976-2-tps-620-946.png) + +选择你的 `package.json` 后,下拉选择 `Scrips` ,其中是你 `package.json` 中配置好的 `scripts` 中的命令,选择你要的命令,比如 `dev` 或者 `test` 等即可 。 +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01DBqmwD1rtbwqpuQZe_!!6000000005689-2-tps-1500-1017.png) + +在代码上断点后执行调试即可。 +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01sGzfeH1iLPpzSIWSg_!!6000000004396-2-tps-1327-907.png) + + + diff --git a/site/versioned_docs/version-3.0.0/decorator_index.md b/site/versioned_docs/version-3.0.0/decorator_index.md new file mode 100644 index 000000000000..90301e167c3f --- /dev/null +++ b/site/versioned_docs/version-3.0.0/decorator_index.md @@ -0,0 +1,101 @@ +# 现有装饰器索引 + +Midway 提供了很多装饰器能力,这些装饰器分布在不同的包,也提供了不同的功能,本章节提供一个快速反查的列表。 + +## @midwayjs/core + +| 装饰器 | 修饰位置 | 描述 | +| ------------------ | ------------ | ----------------------------------------- | +| @Provide | Class | 暴露一个 class,让 IoC 容器能够获取元数据 | +| @Inject | Property | 注入一个 IoC 容器中的对象 | +| @Scope | Class | 指定作用域 | +| @Init | Method | 标注对象初始化时自动执行的方法 | +| @Destroy | Method | 标注对象销毁时执行的方法 | +| @Async | Class | 【已废弃】表明为异步函数 | +| @Autowire | Class | 【已废弃】标识类为自动注入属性 | +| @Autoload | Class | 让类可以自加载执行 | +| @Configuration | Class | 标识一个容器入口配置类 | +| @Aspect | Class | 标识拦截器 | +| @Validate | Method | 标识方法,需要被验证 | +| @Rule | Property | 标识 DTO 的校验规则 | +| @App | Property | 注入当前应用实例 | +| @Config | Property | 获取配置 | +| @Logger | Property | 获取日志实例 | +| @Controller | Class | 标识为一个 Web 控制器 | +| @Get | Method | 注册为一个 GET 类型的路由 | +| @Post | Method | 注册为一个 POST 类型的路由 | +| @Del | Method | 注册为一个 DELETE 类型的路由 | +| @Put | Method | 注册为一个 PUT 类型的路由 | +| @Patch | Method | 注册为一个 PATCH 类型的路由 | +| @Options | Method | 注册为一个 OPTIONS 类型的路由 | +| @Head | Method | 注册为一个 HEAD 类型的路由 | +| @All | Method | 注册为一个全类型的路由 | +| @Session | Parameter | 从参数获取 ctx.session | +| @Body | Parameter | 从参数获取 ctx.request.body | +| @Query | Parameter | 从参数获取 ctx.query | +| @Param | Parameter | 从参数获取 ctx.param | +| @Headers | Parameter | 从参数获取 ctx.headers | +| @File | Parameter | 从参数获取第一个上传文件 | +| @Files | Parameter | 从参数获取所有的上传文件 | +| @Fields | Parameter | 从参数获取表单 Field(上传时) | +| @Redirect | Method | 修改响应跳转 | +| @HttpCode | Method | 修改响应状态码 | +| @SetHeader | Method | 修改响应头 | +| @ContentType | Method | 修改响应头中的 Content-Type 字段 | +| @Schedule | Class | 标识为一个 egg 定时任务 | +| @Plugin | Property | 获取 egg 插件 | +| @Provider | Class | 暴露微服务提供者(生产者) | +| @Consumer | Class | 暴露微服务调用者(消费者) | +| @GrpcMethod | Method | 标识暴露的 gRPC 方法 | +| @Func | Class/Method | 【已废弃】标识为一个函数入口 | +| @Handler | Method | 【已废弃】配合标记函数 | +| @ServerlessTrigger | Method | 标识一个函数触发器 | +| @Task | Method | 定义一个分布式任务 | +| @TaskLocal | Method | 定义一个本地任务 | +| @Queue | Class | 定义一个自触发的任务 | + + + +## @midwayjs/typeorm + +| 装饰器 | 修饰位置 | 作用 | +| --------------------- | -------- | ---------------- | +| @EntityModel | Class | 定义一个实体对象 | +| @InjectEntityModel | Property | 注入一个实体对象 | +| @EventSubscriberModel | Class | 定义事件订阅 | + + + +## @midwayjs/validate + +| 装饰器 | 修饰位置 | 描述 | +| --------- | -------- | ---------------------- | +| @Rule | Property | 定义一个规则 | +| @Validate | Method | 标识一个需要校验的方法 | + + + +## @midwayjs/swagger + +| 装饰器 | 修饰位置 | 描述 | +| ----------------------- | ----------------- | ---- | +| `@ApiBody` | Method | | +| `@ApiExcludeEndpoint` | Method | | +| `@ApiExcludeController` | Class | | +| `@ApiHeader` | Class/Method | | +| `@ApiHeaders` | Class/Method | | +| `@ApiOperation` | Method | | +| `@ApiProperty` | Property | | +| `@ApiPropertyOptional` | Property | | +| `@ApiResponseProperty` | Property | | +| `@ApiQuery` | Method | | +| `@ApiResponse` | Method | | +| `@ApiTags` | Controller/Method | | +| `@ApiExtension` | Method | | +| `@ApiBasicAuth` | Controller | | +| `@ApiBearerAuth` | Controller | | +| `@ApiCookieAuth` | Controller | | +| `@ApiOAuth2` | Controller | | +| `@ApiSecurity` | Controller | | +| `@ApiParam` | Method | | +| `@ApiParam` | Method | | diff --git a/site/versioned_docs/version-3.0.0/deployment.md b/site/versioned_docs/version-3.0.0/deployment.md new file mode 100644 index 000000000000..bdaa075ca907 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/deployment.md @@ -0,0 +1,828 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 启动和部署 + +Midway 提供了一个轻量的启动器,用于启动你的应用。我们为应用提供了多种部署模式,你既可以将应用按照传统的样子,部署到任意的服务器上(比如自己购买的服务器),也可以将应用构建为一个 Serverless 应用,Midway 提供跨多云的部署方式。 + + +## 本地开发 + + +这里列举的主要是本地使用 `dev` 命令开发的方式,有两种。 + + +### 快速启动单个服务 + +在本地研发时,Midway 在 `package.json` 中提供了一个 `dev` 命令启动框架,比如: + + + + + +```json +{ + "scripts": { + "dev": "mwtsc --watch --run @midwayjs/mock/app.js", + } +} +``` + +这是一个最精简的命令,他有如下特性: + + +- 1、使用 `mwtsc` 工具构建代码,成功后通过 `@midwayjs/mock` 包中的 `app.js` 文件读取构建后的代码启动项目 +- 2、使用内置的 API(@midwayjs/core 的 `initializeGlobalApplicationContext`)创建一个服务,不经过 `bootstrap.js` +- 3、单进程运行 + + + + + +```json +{ + "script": { + "dev": "midway-bin dev --ts" + } +} +``` + +这是一个最精简的命令,他有如下特性: + + +- 1、使用 `--ts` 指定 TypeScript(ts-node)环境启动 +- 2、使用内置的 API(@midwayjs/core 的 `initializeGlobalApplicationContext`)创建一个服务,不经过 `bootstrap.js` +- 3、单进程运行 + + + + + +在命令行运行下面的命令即可执行。 +```bash +$ npm run dev +``` + + + +### 指定入口启动服务 + +由于本地的 dev 命令普通情况下和 `bootstrap.js` 启动文件初始化参数不同,有些用户担心本地开发和线上开发不一致,比如测试链路等。 + +这个时候我们可以直接传递一个入口文件给 `dev` 命令,直接使用入口文件启动服务。 + + + + + +```json +{ + "scripts": { + "dev": "mwtsc --watch --run bootstrap.js", + }, +} +``` + + + + + +```json +{ + "script": { + "dev": "midway-bin dev --ts --entryFile=bootstrap.js" + } +} +``` + + + + + + + +## 部署到服务器 + + +### 部署后和本地开发的区别 + + +在部署后,有些地方和本地开发有所区别。 + + +**1、node 环境的变化** + +最大的不同是,服务器部署后,会直接使用 node 来启动项目。 + +* 如果使用了 `mwtsc` 开发项目,差距不是很大 +* 如果使用了 `@midwayjs/cli`,将不会使用 `ts-node` 来启动项目,这意味着不再读取 `*.ts` 文件 + +**2、加载目录的变化** + + +服务器部署后,只会加载构建后的 `dist` 目录,而本地开发则是加载 `src` 目录。 + +| | 本地 | 服务器 | +| --- | --- | --- | +| appDir | 项目根目录 | 项目根目录 | +| baseDir | 项目根目录下的 src 目录 | 项目根目录下的 dist 目录 | + +**3、环境的变化** + + +服务器环境,一般使用 `NODE_ENV=production` ,很多库都会在这个环境下提供性能更好的方式,例如启用缓存,报错处理等。 + + +**4、日志文件** + + +一般服务器环境,日志不会打印到项目的 logs 目录,而是其他不会受到项目更新影响的目录,比如 `home/admin/logs` ,这样固定的目录,也方便其他工具采集日志。 + + +### 部署的流程 + + +整个部署分为几个部分,由于 Midway 是 TypeScript 编写,比传统 JavaScript 代码增加了一个构建的步骤,整个部署的过程如下。 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01wSpCuM27pWGTDeDyK_!!6000000007846-2-tps-2212-242.png) +由于部署和平台、环境非常相关,下面我们都将以 Linux 来演示,其他平台可以视情况参考。 + + +### 编译代码和安装依赖 + +由于 Midway 项目是 TypeScript 编写,在部署前,我们先进行编译。在示例中,我们预先写好了构建脚本,执行 `npm run build` 即可,如果没有,在 `package.json` 中添加下面的 `build` 命令即可。 + + + + + +```typescript +{ + "scripts": { + "build": "mwtsc --cleanOutDir", + }, +} +``` + + + + + +```json +{ + "scripts": { + "build": "midway-bin build -c" + }, +} +``` + + + + + + + +:::info +虽然不是必须,但是推荐大家先执行测试和 lint。 +::: + + +一般来说,部署构建的环境和本地开发的环境是两套,我们推荐在一个干净的环境中构建你的应用。 + + +下面的代码,是一个示例脚本,你可以保存为 `build.sh` 执行。 + +```bash +## 服务器构建(已经下载好代码) +$ npm install # 安装开发期依赖 +$ npm run build # 构建项目 +$ npm prune --production # 移除开发依赖 + +## 本地构建(已经安装好 dev 依赖) +$ npm run build +$ npm prune --production # 移除开发依赖 +``` + +:::info +一般安装依赖会指定 `NODE_ENV=production` 或 `npm install --production` ,在构建正式包的时候只安装 dependencies 的依赖。因为 devDependencies 中的模块过大而且在生产环境不会使用,安装后也可能遇到未知问题。 +::: + + +执行完构建后,会出现 Midway 构建产物 `dist` 目录。 +```text +➜ my_midway_app tree +. +├── src +├── dist # Midway 构建产物目录 +├── node_modules # Node.js 依赖包目录 +├── test +├── bootstrap.js # 部署启动文件 +├── package.json +└── tsconfig.json +``` + + + +### 构建时别名(alias path)的问题 + +别名是前端工具带来的习惯,而非 Node.js 的标准能力,目前使用有两种可选的方式: + +* 1、使用 Node.js 自带的 [子路径导入方案](https://nodejs.org/dist/latest/docs/api/packages.html#subpath-imports) +* 2、使用 [额外工具](/docs/faq/alias_path) 在编译时处理 + + + +### 打包压缩 + + +构建完成后,你可以简单的打包压缩,上传到待发布的环境。 + +:::caution + +一般来说服务器运行必须包含的文件或者目录有 `package.json`,`bootstrap.js`,`dist`,`node_modules`。 + +::: + + + + +### 上传和解压 + + +有很多种方式可以上传到服务器,比如常见的 `ssh/FTP/git` 等。也可以使用 [OSS](https://www.aliyun.com/product/oss) 等在线服务进行中转。 + + +### 启动项目 + +Midway 构建出来的项目是单进程的,不管是采用 `fork` 模式还是 `cluster` 模式,单进程的代码总是很容易的兼容到不同的体系中,因此非常容易被社区现有的 pm2/forever 等工具所加载, + + +我们这里以 pm2 来演示如何部署。 + + +项目一般都需要一个入口文件,比如,我们在根目录创建一个 `bootstrap.js` 作为我们的部署文件。 +``` +➜ my_midway_app tree +. +├── src +├── dist # Midway 构建产物目录 +├── test +├── bootstrap.js # 部署启动文件 +├── package.json +└── tsconfig.json +``` + + +Midway 提供了一个简单方式以满足不同场景的启动方式,只需要安装我们提供的 `@midwayjs/bootstrap` 模块(默认已自带)。 + +```bash +$ npm install @midwayjs/bootstrap --save +``` + +然后在入口文件中写入代码,注意,这里的代码使用的是 `JavaScript` 。 + +```javascript +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap.run(); +``` + +虽然启动文件的代码很简单,但是我们依旧需要这个文件,在后续的链路追踪等场景中需要用到。 + +注意,这里不含 http 的启动端口,如果你需要,可以参考文档 修改。 + +- [修改 koa 端口](extensions/koa#修改端口) + +这个时候,你已经可以直接使用 `NODE_ENV=production node bootstrap.js` 来启动代码了,也可以使用 pm2 来执行启动。 + +我们一般推荐使用工具来启动 Node.js 项目,下面有一些文档可以进阶阅读。 + +- [pm2 使用文档](extensions/pm2) +- [cfork 使用文档](extensions/cfork) + + + +### 启动参数 + +在大多数情况下,不太需要在 Bootstrap 里配置参数,但是依旧有一些可配置的启动参数选项,通过 `configure` 方法传入。 + +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap + .configure({ + imports: [/*...*/] + }) + .run(); +``` + + + +| 属性 | 类型 | 描述 | +| -------------- |--------------------------------------------------------------------------------------| ------------------------------------------------------------ | +| appDir | string | 可选,项目根目录,默认为 `process.cwd()` | +| baseDir | string | 可选,项目代码目录,研发时为 `src`,部署时为 `dist` | +| imports | Component[] | 可选,显式的组件引用 | +| moduleDetector | 'file' \| IFileDetector \| false | 可选,使用的模块加载方式,默认为 `file` ,使用依赖注入本地文件扫描方式,可以显式指定一个扫描器,也可以关闭扫描 | +| logger | Boolean \| ILogger | 可选,bootstrap 中使用的 logger,默认为 consoleLogger | +| ignore | string[] | 可选,依赖注入容器扫描忽略的路径,moduleDetector 为 false 时无效 | +| globalConfig | Array\<\{ [environmentName: string]: Record\ }> \| Record\ | 可选,全局传入的配置,如果传入对象,则直接以对象形式合并到当前的配置中,如果希望传入不同环境的配置,那么,以数组形式传入,结构和 `importConfigs` 一致。 | + + + +**示例,传入全局配置(对象)** + +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap + .configure({ + globalConfig: { + customKey: 'abc' + } + }) + .run(); +``` + + + +**示例,传入分环境的配置** + +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap + .configure({ + globalConfig: [{ + default: {/*...*/}, + unittest: {/*...*/} + }] + }) + .run(); +``` + + + + + + +## 使用 Docker 部署 + +### 编写 Dockerfile,构建镜像 + + +步骤一:在当前目录下新增Dockerfile + +```dockerfile +FROM node:18 + +WORKDIR /app + +ENV TZ="Asia/Shanghai" + +COPY . . + +# 如果各公司有自己的私有源,可以替换registry地址 +RUN npm install --registry=https://registry.npm.taobao.org + +RUN npm run build + +# 如果端口更换,这边可以更新一下 +EXPOSE 7001 + +CMD ["npm", "run", "start"] +``` + + +步骤二: 新增 `.dockerignore` 文件(类似 git 的 ignore 文件),可以把 `.gitignore` 的内容拷贝到 `.dockerignore` 里面 + + +步骤三:当使用 pm2 部署时,请将命令修改为 `pm2-runtime start` ,pm2 行为请参考 [pm2 容器部署说明](https://www.npmjs.com/package/pm2#container-support)。 + + +步骤四:构建 docker 镜像 + +```bash +$ docker build -t helloworld . +``` + +步骤五:运行 docker 镜像 + +```bash +$ docker run -itd -P helloworld +``` + +运行效果如下: +![image.png](https://cdn.nlark.com/yuque/0/2020/png/187105/1608882492099-49160b6a-601c-4f08-ba65-b95a1335aedf.png#height=33&id=BtUCB&margin=%5Bobject%20Object%5D&name=image.png&originHeight=45&originWidth=1024&originalType=binary&ratio=1&size=33790&status=done&style=none&width=746) + +然后大写的 `-P` 由于给我们默认分配了一个端口,所以我们访问可以访问 `32791` 端口(这个 `-P` 是随机分配,我们也可以使用 `-p 7001:7001` 指定特定端口) + +![image.png](https://cdn.nlark.com/yuque/0/2020/png/187105/1608882559686-031bcf0d-2185-42cd-a838-80f008777395.png#height=94&id=dfag9&margin=%5Bobject%20Object%5D&name=image.png&originHeight=188&originWidth=578&originalType=binary&ratio=1&size=24488&status=done&style=none&width=289) + +后续:发布 docker 镜像 +* 推送构建好的镜像到DockerHub,请参考[官方文档](https://docs.docker.com/get-started/04_sharing_app/) +* 推送到自建的镜像仓库(以Harbor为例),请参考[Harbor文档](https://goharbor.io/docs/2.5.0/working-with-projects/working-with-images/pulling-pushing-images/) + + +**优化** + +我们看到前面我们打出来的镜像有1个多G,可优化的地方: +- 1、我们可以采用更精简的 docker image 的基础镜像:例如 node:18-alpine, +- 2、其中的源码最终也打在了镜像中,其实这块我们可以不需要。 + +我们可以同时结合 docker 的 multistage 功能来做一些优化,这个功能请注意要在 `Docker 17.05` 版本之后才能使用。 + + +```dockerfile +FROM node:18 AS build + +WORKDIR /app + +COPY . . + +RUN npm install + +RUN npm run build + +FROM node:18-alpine + +WORKDIR /app + +COPY --from=build /app/dist ./dist +# 把源代码复制过去, 以便报错能报对行 +COPY --from=build /app/src ./src +COPY --from=build /app/bootstrap.js ./ +COPY --from=build /app/package.json ./ + +RUN apk add --no-cache tzdata + +ENV TZ="Asia/Shanghai" + +RUN npm install --production + +# 如果端口更换,这边可以更新一下 +EXPOSE 7001 + +CMD ["npm", "run", "start"] +``` + +当前示例的结果只有 `207MB`。相比原有的 `1.26G` 省了很多的空间。 + +### 结合 Docker-Compose 运行 + +在 docker 部署的基础上,还可以结合 docker-compose 配置项目依赖的服务,实现快速部署整个项目。 + +下面以 midway 结合 redis 为例,使用 docker-compose 快速部署整个项目。 + + +**步骤一:编写Dockerfile** + +按照上文使用 Docker 部署的方式[编写Dockerfile](deployment#编写-dockerfile构建镜像) + + +**步骤二:编写docker-compose.yml** + +新增 `docker-compose.yml` 文件,内容如下:(此处模拟我们的 midway 项目需要使用 redis) + +```yaml +# 项目的根目录,与Dockerfile文件同级 +version: "3" +services: + web: + build: . + ports: + - "7001:7001" + links: + - redis + depends_on: + - redis + redis: + image: redis + +``` + +**步骤三:修改配置** + +修改 redis 的配置文件,内容如下:( 配置redis,请参考 [redis组件](extensions/redis) ) + +```javascript +// src/config/config.default.ts +export default { + // ... + redis: { + client: { + port: 6379, // redis容器的端口 + host: "redis", // 这里与docker-compose.yml文件中的redis服务名称一致 + password: "", //默认没有密码,请自行修改为redis容器配置的密码 + db: 0, + }, + }, +} + +``` + +**步骤四:构建** + +使用命令: + +```bash +$ docker-compose build +``` + +**步骤五:运行** + +```bash +$ docker-compose up -d +``` + +![](https://cdn.nlark.com/yuque/0/2020/png/187105/1608884158660-02bd2d3c-08b4-4ecc-a4dd-a18d4b9d2c12.png) + +**后续** + +更多关于 docker-compose ,请参考[官方文档](https://docs.docker.com/compose/) + + + +## 单文件构建部署 + +在某些场景,将项目构建为单文件,部署的文件可以更小,可以更容易的分发部署,在一些场景下特别的高效,如: + +- Serverless 场景,单文件可以更快的部署 +- 私密场景,单文件可以更容易的做加密混淆 + +Midway 从 v3 开始支持将项目构建为单文件。 + +不支持的情况有: + +- egg 项目(@midwayjs/web) +- 入口处 `importConfigs` 使用的路径形式引入配置的应用,组件 +- 未显式依赖的包,或者包里有基于约定的文件 + + + +### 前置依赖 + +单文件构建有一些前置依赖需要安装。 + +```bash +## 用于生成入口 +$ npm i @midwayjs/bundle-helper --save-dev + +## 用于构建单文件 +## 装到全局 +$ npm i @vercel/ncc -g +## 或者装到项目(推荐) +$ npm i @vercel/ncc --save-dev +``` + + + +### 代码调整 + +有一些可能的调整,列举如下: + +#### 1、配置格式调整 + +必须将项目引入的配置调整为 [对象模式](./env_config)。 + +Midway 的官方组件都已经调整为该模式,如果有自己编写的组件,也请调整为该模式才能构建为单文件。 + +:::tip + +Midway v2/v3 均支持配置以 "对象模式" 加载。 + +::: + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; + +import * as DefaultConfig from './config/config.default'; +import * as LocalConfig from './config/config.local'; + +@Configuration({ + importConfigs: [ + { + default: DefaultConfig, + local: LocalConfig + } + ] +}) +export class MainConfiguration { +} +``` + + + +#### 2、默认导出的情况 + +由于 ncc 构建器的默认行为,请 **不要** 在依赖注入相关的代码中使用默认导出。 + +比如: + +```typescript +export default class UserSerivce { + // ... +} +``` + +编译后会导致 `UserSerivce` 无法注入。 + + + +#### 3、数据源 entities 相关 + +数据源依赖的扫描路径也是不支持的。 + +```typescript +export default { + typeorm: { + dataSource: { + default: { + // ... + entities: [ + '/abc', // 不支持 + ] + }, + } +} +``` + +如果 entities 特别多,可以编写一个 js 文件,扫描出 entities 后,生成一个文件到目录下,在每次构建时执行。 + + + +### 修改入口文件 + +修改入口 `bootstrap.js` 为下列代码。 + +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); + +// 显式以组件方式引入用户代码 +Bootstrap.configure({ + // 这里引用的是编译后的入口,本地开发不走这个文件 + imports: require('./dist/index'), + // 禁用依赖注入的目录扫描 + moduleDetector: false, +}).run() + +``` + + + +### 构建 + +单文件构建的编译需要几个步骤: + +- 1、将项目 ts 文件构建为 js +- 2、使用额外编译器,将所有的 js 文件打包成一个文件 + +我们可以将上面的流程编写为下面的两条命令,放在 `package.json` 的 `scripts` 字段中。 + +```json + "scripts": { + // ... + "bundle": "bundle && npm run build && ncc build bootstrap.js -o build", + "bundle_start": "NODE_ENV=production node ./build/index.js" + }, +``` + +包含三个部分 + +- `bundle` 是将所有的项目代码以组件的形式导出,并生成一个 `src/index.ts` 文件,该命令是 `@midwayjs/bundle-helper` 提供的 +- `npm run buid` 是基础的 ts 项目构建,将 `src/**/*.ts` 构建为 `dist/**/*.js` +- `ncc build bootstrap.js -o build` 以 `bootstrap.js` 为入口构建为一个单文件,最终生成到 `build/index.js` + + + +编写完成后,执行命令。 + +```bash +$ npm run bundle +``` + +:::tip + +注意,构建过程中可能有错误,比如 ts 定义错误,入口生成语法不正确等情况,需要手动修复。 + +::: + +编译完成后,启动项目。 + +```bash +$ npm run bundle_start +``` + +如果启动访问没问题,那么你就可以拿着构建的 build 目录做分发了。 + + + +## 二进制文件部署 + +将 Node.js 打包为一个单独的可执行文件,部署时直接拷贝执行即可,这种方式包含了 node 运行时,业务代码,有利于保护知识产权。 + +常见的将 Node.js 打包为可执行文件的工具有 `pkg`、`nexe`、`node-packer`、`enclose` 等,下面我们将以最为常见的 `pkg` 包作为示例。 + + + +### 前置依赖 + +二进制文件部署有一些前置依赖需要安装。 + +```bash +## 用于生成入口 +$ npm i @midwayjs/bundle-helper --save-dev + +## 用于构建二进制文件 +## 装到全局 +$ npm i pkg -g +## 或者装到项目(推荐) +$ npm i pkg --save-dev +``` + + + +### 代码调整 + +和 [单文件构建部署](./deployment#单文件构建部署) 的调整相同,请参考上面的文档。 + + + +### 修改入口文件 + +和 [单文件构建部署](./deployment#单文件构建部署) 的调整相同,请参考上面的文档。 + + + +### 构建 + +首先需要对 pkg 进行配置,主要内容在 `package.json` 的 `bin` 和 `pkg` 字段下。 + +- `bin` 我们指定为入口文件,即 `bootstrap.js` +- `pkg.scripts` 构建后的目录,使用了 glob 的语法包括了 `dist` 下的所有 js 文件 +- `pkg.asserts` 如果有一些静态资源文件,可以在这里配置 +- `pkg.targets` 构建的平台产物,是下列选项的组合(示例中我指定了 mac + node18): + - **nodeRange** (node8), node10, node12, node14, node16 or latest + - **platform** alpine, linux, linuxstatic, win, macos, (freebsd) + - **arch** x64, arm64, (armv6, armv7) +- `pkg.outputPath` 构建产物的地址,为了和 ts 输出分开,我们选择了 build 目录 + + + +`package.json` 参考示例: + +```json +{ + "name": "my-midway-project", + // ... + "devDependencies": { + // ... + "@midwayjs/bundle-helper": "^1.2.0", + "pkg": "^5.8.1" + }, + "scripts": { + // ... + "pkg": "pkg . -d > build/pkg.log", + "bundle": "bundle && npm run build" + }, + "bin": "./bootstrap.js", + "pkg": { + "scripts": "dist/**/*.js", + "assets": [], + "targets": [ + "node18-macos-arm64" + ], + "outputPath": "build" + }, + // ... +} + +``` + +更为细节的部分请参考 [pkg文档](https://github.com/vercel/pkg)。 + +:::tip + +上面的实例中,pkg 命令的 `-d` 参数是为了输出调试信息到特定文件,可以自行删减。 + +::: + + + +二进制文件构建的编译需要几个步骤: + +- 1、生成 `src/index.ts` 入口文件,将项目 ts 文件构建为 js +- 2、使用 pkg,生成特定平台的构建产物 + +我们可以执行命令。 + +```bash +$ npm run bundle +$ npm run pkg +``` + +正确的话,我们可以在 `build` 目录下看到一个 `my-midway-project` 文件(我们的 `package.json` 的 `name` 字段),双击它即可执行。 + + + +## 部署失败的问题 + +部署后由于和环境相关,情况更为复杂,如果部署到服务器之后碰到了问题,请查看 [服务器启动失败排查](/docs/ops/ecs_start_err) 。 diff --git a/site/versioned_docs/version-3.0.0/env_config.md b/site/versioned_docs/version-3.0.0/env_config.md new file mode 100644 index 000000000000..a14b3313b63a --- /dev/null +++ b/site/versioned_docs/version-3.0.0/env_config.md @@ -0,0 +1,670 @@ +# 多环境配置 + +配置是我们常用的功能,而且在不同的环境,经常会使用不同的配置信息。 + +本篇我们来介绍 Midway 如何加载不同环境的业务配置。 + + + +## 配置文件 + +最为简单的就是使用框架提供的业务配置文件能力。 + +该能力可以在所有业务代码和组件中使用,贯穿整个 Midway 生命周期。 + +配置文件可以以两种格式导出,**对象形式** 和 **函数形式**。 + +:::tip + +经过我们的实践,**对象形式** 会更加的简单友好,可以规避许多错误用法。 + +大部分文档中我们都将以此形式进行展示。 + +::: + + + +### 配置文件目录 + +我们可以自定义一个目录,在其中放入配置文件。 + +比如 `src/config` 目录。 + +``` +➜ my_midway_app tree +. +├── src +│ ├── config +│ │ ├── config.default.ts +│ │ ├── config.prod.ts +│ │ ├── config.unittest.ts +│ │ └── config.local.ts +│ ├── interface.ts +│ └── service +├── test +├── package.json +└── tsconfig.json +``` + +配置文件的名字有一些特定的约定。 + +`config.default.ts` 为默认的配置文件,所有环境都会加载这个配置文件。 + +其余的文件名,使用 `config.环境` 作为文件名,具体环境的概念请查看 [运行环境](environment)。 + +配置不是 **必选项**,请酌情添加自己需要的环境配置。 + + + + +### 对象形式 + + +配置文件导出的格式为 object,比如: + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + keys: '1639994056460_8009', + koa: { + port: 7001, + }, +} as MidwayConfig; +``` + + + +### 函数形式 + + +配置文件为一个带有 `appInfo` 参数的函数。这个函数在框架初始化时会被自动执行,将返回值合并进完整的配置对象。 + +```typescript +// src/config/config.default.ts +import { MidwayAppInfo, MidwayConfig } from '@midwayjs/core'; + +export default (appInfo: MidwayAppInfo): MidwayConfig => { + return { + keys: '1639994056460_8009', + koa: { + port: 7001, + }, + view: { + root: path.join(appInfo.appDir, 'view'), + }, + }; +} + +``` + +这个函数的参数为 `MidwayAppInfo` 类型,值为以下内容。 + + +| **appInfo** | **说明** | +| ----------- | ------------------------------------------------------------ | +| pkg | package.json | +| name | 应用名,同 pkg.name | +| baseDir | 应用代码的 src (本地开发)或者 dist (上线后)目录 | +| appDir | 应用代码的目录 | +| HOME | 用户目录,如 admin 账户为 /home/admin | +| root | 应用根目录,只有在 local 和 unittest 环境下为 baseDir,其他都为 HOME。 | + + + +### 配置文件定义 + +Midway 提供了 `MidwayConfig` 作为统一的配置项定义,所有的组件都会将定义合并到此配置项定义中。每当一个组件被开启(在 `configuration.ts` 中被 `imports` ),`MidwayConfig` 就会自动包含该组件的配置定义。 + +为此,请尽可能使用文档推荐的格式,以达到最佳的使用效果。 + +每当启用一个新组件时,配置定义都会自动加入该组件的配置项,通过这个行为,也可以变相的检查是否启用了某个组件。 + +比如,我们启用了 view 组件的效果。 + +![](https://img.alicdn.com/imgextra/i2/O1CN013sHGlA1o3uQ4Pg0nO_!!6000000005170-2-tps-1416-572.png) + +:::tip + +为什么不使用普通 key 导出形式而使用对象? + +1、用户在不了解配置项的情况下,依旧需要查看文档了解每项含义,除了第一层有一定的提示作用外,后面的层级提示没有很明显的效率提升 + +2、key 导出的形式在过深的结构下展示没有优势 + +3、key 导出可能会出现重复,但是代码层面不会有警告或者报错,难以排查,这一点对象形式较为友好 + +::: + + + +### 对象形式加载配置文件 + + +框架提供了加载不同环境的配置文件的功能,需要在 `src/configuration.ts` 文件中开启。 + + +配置加载有两种方式,**对象形式** 和 **指定目录形式** 加载。 + +从 Midway v3 开始,我们将以 **对象形式** 作为主推的配置加载形式。 + +在单文件构建、ESM 等场景下,只支持这种标准的模块加载方式来加载配置。 + +每个环境的配置文件 **必须显式指定添加**,后续框架会根据实际的环境进行合并。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; + +import * as DefaultConfig from './config/config.default'; +import * as LocalConfig from './config/config.local'; + +@Configuration({ + importConfigs: [ + { + default: DefaultConfig, + local: LocalConfig + } + ] +}) +export class MainConfiguration { +} +``` +`importConfigs` 中的数组中传递配置对象,每个对象的 key 为环境,值为环境对应的配置值,midway 在启动中会根据环境来加载对应的配置。 + + + +### 指定目录、文件加载配置 + +指定加载一个目录,目录里所有的 `config.*.ts` 都会被扫描加载。 + +ESM,单文件部署等方式不支持目录配置加载。 + +:::info +`importConfigs` 这里只是指定需要加载的文件,实际运行时会**自动选择当前的环境**来找对应的文件后缀。 +::: + + +配置文件的规则为: + + +- 1、可以指定一个目录,推荐传统的 `src/config` 目录,也可以指定一个文件 +- 2、文件指定无需 ts 后缀 +- 3、配置文件 **必须显式指定添加** + + + +**示例:指定目录** + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; + +@Configuration({ + importConfigs: [ + join(__dirname, './config/'), + ] +}) +export class MainConfiguration { +} +``` + + +**示例:指定特定文件** + + +手动指定一批文件时,这个时候如果文件不存在,则会报错。 + + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; + +@Configuration({ + importConfigs: [ + join(__dirname, './config/config.default'), + join(__dirname, './config/config.local'), + join(__dirname, './config/custom.local') // 可以使用自定义的命名,只要中间部分带环境就行 + ] +}) +export class MainConfiguration { +} +``` + + +也可以使用项目外的配置,但是请使用绝对路径,以及 `*.js` 后缀。 + + +比如目录结构如下(注意 `customConfig.default.js` 文件): + +``` + base-app + ├── package.json + ├── customConfig.default.js + └── src + ├── configuration.ts + └── config + └── config.default.ts +``` + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; + +@Configuration({ + importConfigs: [ + join(__dirname, './config/'), + join(__dirname, '../customConfig.default'), + ] +}) +export class MainConfiguration { +} +``` + + + + + +### 配置加载顺序 + + +配置存在优先级(应用代码 > 组件),相对于此运行环境的优先级会更高。 + + +比如在 prod 环境加载一个配置的加载顺序如下,后加载的会覆盖前面的同名配置。 + + +```typescript +-> 组件 config.default.ts +-> 应用 config.default.ts +-> 组件 config.prod.ts +-> 应用 config.prod.ts +``` + + + +### 配置合并规则 + + +默认会加载 `**/config.defaut.ts` 的文件以及 `**/config.{环境}.ts` 文件。 + + +比如,下面的代码在 `local` 环境会查找 `config.default.*` 和 `config.local.*` 文件,如果在其他环境,则只会查找 `config.default.*` 和 `config.{当前环境}.*` ,如果文件不存在,则不会加载,也不会报错。 +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; + +@Configuration({ + importConfigs: [ + join(__dirname, './config/'), + ] +}) +export class MainConfiguration { +} +``` + + +为了向前兼容,我们对某些特殊环境的配置读取做了一些处理。这里环境的值指的是根据 `NODE_ENV` 和 `MIDWAY_SERVER_ENV` 的值综合得出的 [结果](environment#AxjGQ)。 + +| **环境的值** | **读取的配置文件** | +| --- | --- | +| prod | *.default.ts + *.prod.ts | +| production | *.default.ts + *.production.ts + *.prod.ts | +| unittest | *.default.ts + *.unittest.ts | +| test | *.default.ts + *.test.ts + *.unittest.ts | + +除了上述表格外,其余都是 `*.default.ts + *.{当前环境}.ts` 的值。 + + +此外,配置的合并使用 [extend2](https://github.com/eggjs/extend2) 模块进行深度拷贝,[extend2](https://github.com/eggjs/extend2) fork 自 [extend](https://github.com/justmoon/node-extend),处理数组时会存在差异。 + +```javascript +const a = { + arr: [ 1, 2 ], +}; +const b = { + arr: [ 3 ], +}; +extend(true, a, b); +// => { arr: [ 3 ] } +``` +根据上面的例子,框架直接覆盖数组而不是进行合并。 + + + +## 获取配置 + + +Midway 会将配置都保存在内部的配置服务中,整个结构是一个对象,在 Midway 业务代码使用时,使用 `@Config` 装饰器注入。 + + + +### 单个配置值 + + +默认情况下,会根据装饰器的字符串参数值,从配置对象中获取。 + + +```typescript +import { Config } from '@midwayjs/core'; + +export class IndexHandler { + + @Config('userService') + userConfig; + + async handler() { + console.log(this.userConfig); // { appname: 'test'} + } +} +``` + + + +### 深层级别配置值 + + +如果配置对象的值在对象的深处,那么可以用级联的方式获取。 + + +比如数据源为: + + +```json +{ + "userService": { + "appname": { + "test": { + "data": "xxx" + } + } + } +} +``` +则可以写复杂的获取表达式来获取值,示例如下。 +```typescript +import { Config } from '@midwayjs/core'; + +export class IndexHandler { + + @Config('userService.appname.test.data') + data; + + async handler() { + console.log(this.data); // xxx + } +} + +``` + + + +### 整个配置对象 + + +也可以通过 `ALL` 这个特殊属性,来获取整个配置的对象。 +```typescript +import { Config, ALL } from '@midwayjs/core'; + +export class IndexHandler { + + @Config(ALL) + allConfig; + + async handler() { + console.log(this.allConfig); // { userService: { appname: 'test'}} + } +} +``` + + + +## 修改配置 + +在编码过程中,我们有一些可以动态修改配置的地方,用在不同的场景。 + + + +### 生命周期中修改 + + +midway 新增了一个异步配置加载的生命周期,可以在配置加载后执行。 + +```typescript +// src/configuration.ts +import { Configuration, IMidwayContainer } from '@midwayjs/core'; +import { join } from 'path'; +import { RemoteConfigService } from '../service/remote'; // 自定义的获取远端配置服务 + +@Configuration({ + importConfigs: [ + join(__dirname, './config/'), + ] +}) +export class MainConfiguration { + + async onConfigLoad(container: IMidwayContainer) { + // 这里你可以修改全局配置 + const remoteConfigService = await container.getAsync(RemoteConfigService); + const remoteConfig = await remoteConfigService.getData(); + + // 这里的返回值会和全局的 config 做合并 + // const remoteConfig = { + // typeorm: { + // dataSource: { + // default: { + // type: "mysql", + // host: "localhost", + // port: 3306, + // username: "root", + // password: "123456", + // database: "admin", + // synchronize: false, + // logging: false, + // entities: "/**/**.entity.ts", + // dateStrings: true + // } + // } + // } + // } + return remoteConfig; + } +} +``` + +:::caution + +`onConfigLoad` 生命周期会在 egg 插件(若有)初始化之后执行,不能用于覆盖 egg 插件的配置。 + +::: + + + +### 启动时修改 + +可以在启动代码之前,使用 Bootstrap 的 `configure` 方法添加配置。 + +`configure` 方法可以传递一个 `globalConfig` 的属性,可以在应用启动前传递一个全局配置。 + +如果传递数组,则可以区分环境。 + +```typescript +// bootstrap.js +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap + .configure({ + globalConfig: [ + { + default: { + abc: '123' + }, + unittest: { + abc: '321' + } + } + ] + }) + .run(); + +// in unittest, app.getConfig('abc') => '321' +``` + +如果传递对象,则直接覆盖。 + +```typescript +// bootstrap.js +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap + .configure({ + globalConfig: { + abc: 'text' + } + }) + .run(); + +// app.getConfig('abc') => 'text' +``` + + + +### 使用 API 修改 + + +其他场景的修改配置,可以使用 midway 提供的 [API](./built_in_service#midwayconfigservice)。 + + + +## 环境变量和配置 + + +社区有一些库,比如 `dotenv` 可以加载 `.env` 文件注入到环境中,从而将一些秘钥放在环境中,在 Midway 中可以直接依赖它使用。 +```bash +$ npm i dotenv --save +``` +可以在项目根目录增加 `.env` 文件,比如下面的内容: +``` +OSS_SECRET=12345 +OSS_ACCESSKEY=54321 +``` +我们可以在入口中初始化,比如 `bootstrap.js` 或者 `configuration` 。 +```typescript +import { Configuration } from '@midwayjs/core'; +import * as dotenv from 'dotenv'; + +// load .env file in process.cwd +dotenv.config(); + +@Configuration({ + //... +}) +export class MainConfiguration { + async onReady(container) { + + } +} + +``` + + +我们可以在环境配置中使用了。 +```typescript +// src/config/config.default + +export const oss = { + accessKey: process.env.OSS_ACCESSKEY, // 54321 + secret: process.env.OSS_SECRET // 12345 +} +``` + + + +## 常见错误 + + +配置未生效的可能性很多,排查思路如下: + +- 1、检查 configuration 文件中是否显式配置 `importConfigs` 相关的文件或者目录 +- 2、检查应用启动的环境,是否和配置文件一致,比如 prod 的配置肯定不会在 local 出现 +- 3、检查是否将普通导出和方法回调导出混用,比如下面的混用的情况 + + + +### 1、在构造器(constructor)中获取 @Config 注入的值 + + +**请不要在构造器中 **获取 `@Config()` 注入的属性,这会使得拿到的结果为 undefined。原因是装饰器注入的属性,都在实例创建后(new)才会赋值。这种情况下,请使用 `@Init` 装饰器。 +```typescript +@Provide() +export class UserService { + + @Config('redisConfig') + redisConfig; + + constructor() { + console.log(this.redisConfig); // undefined + } + + @Init() + async initMethod() { + console.log(this.redisConfig); // has value + } + +} +``` + + + +### 2、回调和导出写法混用 + +**下面是错误用法。** + +```typescript +export default (appInfo) => { + const config = {}; + + // xxx + return config; +}; + +export const keys = '12345'; +``` + +`export const` 定义的值会被忽略。 + + + +### 3、export default 和 export const 混用 + +**下面是错误用法。** + +```typescript +export default { + keys: '12345', +} + +export const anotherKey = '54321'; +``` +位于后面的配置将会被忽略。 + +### 4、export= 和其他混用 + +`export=` 混用的情况,如果后面有其他配置,会忽略 `export=` 的值。 + +```typescript +export = { + a: 1 +} +export const b = 2; +``` + +编译后结果: + +```typescript +export const b = 2; +``` + diff --git a/site/versioned_docs/version-3.0.0/environment.md b/site/versioned_docs/version-3.0.0/environment.md new file mode 100644 index 000000000000..4831cf21290c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/environment.md @@ -0,0 +1,79 @@ +# 运行环境 + +Node.js 应用一般通过 `NODE_ENV` 来获取环境变量,来满足不同环境下的不同需求。比如在 `production` 环境下,开启缓存,优化性能,而在 `development` 环境下,会打开所有的日志开关,输出详细的错误信息等等。 + + + +## 指定运行环境 + + +由于在一些情况下 `NODE_ENV` 会被一些工具包拦截注入,所以在 Midway 体系下,我们会根据 `MIDWAY_SERVER_ENV` 优先获取环境,而 `NODE_ENV` 作为第二优先级获取。 + + +我们可以通过启动时增加环境变量来指定。 + +```bash +MIDWAY_SERVER_ENV=prod npm start // 第一优先级 +NODE_ENV=local npm start // 第二优先级 +``` +在 windows 环境,我们需要使用 [cross-env](https://www.npmjs.com/package/cross-env) 模块以达到同样的效果。 +```bash +cross-env MIDWAY_SERVER_ENV=prod npm start // 第一优先级 +cross-env NODE_ENV=local npm start // 第二优先级 +``` + + + +## 代码中获取环境 + + +Midway 在 app 对象上提供了 `getEnv()` 方法获取环境,面对不同的上层框架,Midway 都做了相应的处理,保使得在不同场景下,都拥有 `getEnv()` 方法。。 + + +```typescript +import { Application } from '@midwayjs/koa'; + +// process.env.MIDWAY_SERVER_ENV=prod + +@Provide() +export class UserService { + + @App() + app: Application; + + async invoke() { + console.log(this.app.getEnv()); // prod + } +} +``` + + +如果 `NODE_ENV` 和 `MIDWAY_SERVER_ENV` 都没有赋值,那么默认情况下,方法的返回值为 `prod` 。 + +:::info +注意,你不能直接通过 `NODE_ENV` 和 `MIDWAY_SERVER_ENV` 来获取环境,这两个值都有可能为空,且 Midway 不会反向设置它。如需获取环境,请通过 app.getEnv() 获取其他框架提供的 API 方法获取。 +::: + + + +## 常见的环境变量值 + +一般来说,每个公司都有一些自己的环境变量值,下面是一些常见的环境变量值以及他们对应的说明。 + +| 值 | 说明 | +| --- | --- | +| local | 本地开发环境 | +| dev/daily/development | 日常开发环境 | +| pre/prepub | 预生产环境 | +| prod/production | 生产环境 | +| test/unittest | 单元测试环境 | +| benchmark | 性能测试环境 | + + + +## 依赖注入容器中获取环境 + + +在依赖注入容器初始化的过程中,Midway 默认初始化了一个 `EnvironmentService` 服务用来解析环境,并在整个生命周期中,持续保持这个服务对象。 + +具体请查看 [环境服务](./built_in_service#midwayenvironmentservice)。 diff --git a/site/versioned_docs/version-3.0.0/error_code.md b/site/versioned_docs/version-3.0.0/error_code.md new file mode 100644 index 000000000000..3040a4e2a0e8 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/error_code.md @@ -0,0 +1,289 @@ +# 框架错误码 + +以下是框架内置的错误,随着时间推移,我们会不断增加。 + +| 错误码 | 错误名 | 错误描述 | +| ------------ | ------------------------------------- | ---------------------------- | +| MIDWAY_10000 | 占位使用 | 未知错误 | +| MIDWAY_10001 | MidwayCommonError | 未分类的错误 | +| MIDWAY_10002 | MidwayParameterError | 参数类型错误 | +| MIDWAY_10003 | MidwayDefinitionNotFoundError | 依赖注入定义未找到 | +| MIDWAY_10004 | MidwayFeatureNoLongerSupportedError | 功能不再支持 | +| MIDWAY_10005 | MidwayFeatureNotImplementedError | 功能未实现 | +| MIDWAY_10006 | MidwayConfigMissingError | 配置项丢失 | +| MIDWAY_10007 | MidwayResolverMissingError | 依赖注入属性 resovler 未找到 | +| MIDWAY_10008 | MidwayDuplicateRouteError | 路由重复 | +| MIDWAY_10009 | MidwayUseWrongMethodError | 使用了错误的方法 | +| MIDWAY_10010 | MidwaySingletonInjectRequestError | 作用域混乱 | +| MIDWAY_10011 | MidwayMissingImportComponentError | 组件未导入 | +| MIDWAY_10012 | MidwayUtilHttpClientTimeoutError | http client 调用超时 | +| MIDWAY_10013 | MidwayInconsistentVersionError | 使用了不正确的依赖版本 | +| MIDWAY_10014 | MidwayInvalidConfigError | 无效的配置 | +| MIDWAY_10015 | MidwayDuplicateClassNameError | 重复的类名 | +| MIDWAY_10016 | MidwayDuplicateControllerOptionsError | 重复的控制器参数 | + + + +## MIDWAY_10001 + +**问题描述** + +最通用的框架错误,在不分类的情况下会抛出,一般会将错误的详细内容写入错误信息 + +**解决方案** + +排错以错误信息为准。 + + + +## MIDWAY_10002 + +**问题描述** + +方法的参数传入错误,可能类型不对或者参数格式有误。 + +**解决方案** + +参考方法定义或者文档传入参数。 + + + +## MIDWAY_10003 + +**问题描述** + +一般出现在启动或者动态从容器中获取某个类的时候,如果该类未在容器中注册,就会报出 `xxx is not valid in current context`错误。 + +**解决方案** + +可能的情况,比如在业务代码或者组件使用中: + +```typescript +// ... + +export class UserService {} + +// ... +@Controller() +export class HomeController { + @Inject() + userService: UserService; +} +``` + +如果 `UserService` 没有写 `@Provide` 或者隐式含有 `@Provide` 的装饰器,就会出现上述错误。 + +一般的报错是类似下面这个样子。 + +``` +userService in class HomeController is not valid in current context +``` + +那么,意味着 `HomeController` 中的 `userService` 属性未在容器中找到,你可以顺着这个线索往下排查。 + + + +## MIDWAY_10004 + +**问题描述** + +使用的废弃的功能。 + +**解决方案** + +不使用该功能。 + + + +## MIDWAY_10005 + +**问题描述** + +使用的方法或者功能暂时未实现。 + +**解决方案** + +不使用该功能。 + + + +## MIDWAY_10006 + +**问题描述** + +未提供需要的配置项。 + +**解决方案** + +排查配置对应的环境,是否包含该配置,如果没有,在配置文件中增加该配置即可。 + + + +## MIDWAY_10007 + +**问题描述** + +未找到容器注入的解析类型,当前版本不会出现该错误。 + +**解决方案** + +无。 + + + +## MIDWAY_10008 + +**问题描述** + +检查到重复的路由。 + +**解决方案** + +移除重复的路由部分。 + + + +## MIDWAY_10009 + +**问题描述** + +使用了错误的方法。 + +**解决方案** + +当你在同步的 get 方法中包含了一个异步调用,则会提示使用 `getAsync` 方法,修改即可。 + + + +## MIDWAY_10010 + +**问题描述** + +当在单例中注入了一个未显式声明的请求作用域实例则会出现此错误,错误原因为 [作用域降级](./container#作用域降级)。 + +比如下面的代码,就会抛出此错误: + +```typescript +// ... +@Provide() +export class UserService {} + +// ... +@Provide() +@Scope(ScopeEnum.Singleton) +export class LoginService { + @Inject() + userService: UserService; +} +``` + +经常在 `configuration` 或者中间件文件中出现该问题。 + +该错误是为了规避作用域自动降级,缓存了实例数据带来的风险。 + +**解决方案** + +- 1、如果你是错误的在单例中注入了请求作用域实例,请修改请求作用域代码为单例 +- 2、如果你希望在单例中注入请求作用域来使用,并且能够清楚的知道作用域降级带来的后果(被缓存),请显式在类上声明作用域选项(表示允许降级)。 + +```typescript +@Provide() +@Scope(ScopeEnum.Request, { allowDowngrade: true }) +export class UserService {} +``` + + + +## MIDWAY_10011 + +**问题描述** + +当组件未在 `configuration` 文件中 `imports`,就使用了组件中的类,就会出现此错误。 + +**解决方案** + +显式在 `src/configuration` 中的 `imports` 部分中显式引入组件。 + + + +## MIDWAY_10012 + +**问题描述** + +内置的 Http Client 超时会抛出此错误。 + +**解决方案** + +正常的超时错误,检查为何超时,做好错误处理即可。 + + + +## MIDWAY_10013 + +**问题描述** + +当安装的组件和框架版本不匹配会抛出此错误。 + +一般会出现在框架发布了新版本之后,当项目开启了 lock 文件,使用了老版本的框架版本,并且安装了一个新组件之后。 + +**解决方案** + +删除 lock 文件,重新安装依赖。 + + + +## MIDWAY_10014 + +**问题描述** + +当配置文件中存在 `export default` 和 `export const` 两种导出方式后会抛出该错误。 + +**解决方案** + +请勿两种导出方式混用。 + + + +## MIDWAY_10015 + +**问题描述** + +当启动开启了重复类名检查(conflictCheck),如果代码扫描时在依赖注入容器中发现相同的类名,则会抛出该错误。 + +```typescript +// src/configuration.ts +@Configuration({ + // ... + conflictCheck: true, +}) +export class MainConfiguration { + // ... +} +``` + +**解决方案** + +修改类名,或者关闭重复类名检查。 + + + +## MIDWAY_10016 + +**问题描述** + +当添加了不同的控制器,使用了相同的 `prefix`,并且添加了不同的 `options`,比如中间件,会抛出该错误。 + +**解决方案** + +将相同 `prefix` 的控制器代码进行合并,或者移除所有的 `options`。 + + + + + + + + + + + diff --git a/site/versioned_docs/version-3.0.0/error_filter.md b/site/versioned_docs/version-3.0.0/error_filter.md new file mode 100644 index 000000000000..18ef2e117248 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/error_filter.md @@ -0,0 +1,322 @@ +# 异常处理 + +Midway 提供了一个内置的异常处理器,负责处理应用程序中所有未处理的异常。当您的应用程序代码抛出一个异常处理时,该处理器就会捕获该异常,然后等待用户处理。 + +异常处理器的执行位置处于中间件之后,所以它能拦截所有的中间件和业务抛出的错误。 + +![err_filter](https://img.alicdn.com/imgextra/i2/O1CN013pvSjT1nWvsLRE4vo_!!6000000005098-2-tps-2000-524.png) + + + +## Http 异常 + +在 Http 请求中,Midway 提供了通用的 `MidwayHttpError` 类型的异常,其继承于标准的 `MidwayError`。 + +```typescript +export class MidwayHttpError extends MidwayError { + // ... +} +``` + +我们可以在请求的过程中抛出该错误,由于错误中包含状态码,Http 程序将会自动返回该状态码。 + +比如,下面的代码,抛出了包含 400 状态码的错误。 + +```typescript +import { MidwayHttpError } from '@midwayjs/core'; + +// ... + +async findAll() { + throw new MidwayHttpError('my custom error', HttpStatus.BAD_REQUEST); +} + +// got status: 400 +``` + +但是一般我们很少这么做,大多数的业务的错误都是复用的,错误消息也基本是固定的,为了减少重复定义,我们可以自定义一些异常类型。 + +比如自定义一个状态码为 400 的 Http 异常,可以如下定义错误。 + +```typescript +// src/error/custom.error.ts +import { HttpStatus } from '@midwayjs/core'; + +export class CustomHttpError extends MidwayHttpError { + constructor() { + super('my custom error', HttpStatus.BAD_REQUEST); + } +} +``` + +然后在业务中抛出使用。 + +```typescript +import { CustomHttpError } from './error/custom.error'; + +// ... + +async findAll() { + throw new CustomHttpError(); +} +``` + + + +## 异常处理器 + +内置的异常处理器用于标准的请求响应场景,它可以捕获所有请求中抛出的错误。 + +通过 `@Catch` 装饰器我们可以定义某一类异常的处理程序,我们可以轻松的捕获某一类型的错误,做出处理,也可以捕获全局的错误,返回统一的格式。 + +同时,框架也提供了一些默认的 Http 错误,放在 `httpError` 这个对象下。 + +比如捕获抛出的 `InternalServerErrorError` 错误。 + +我们可以将这一类异常处理器放在 `filter` 目录,比如 `src/filter/internal.filter.ts`。 + +```typescript +// src/filter/internal.filter.ts +import { Catch, httpError, MidwayHttpError } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Catch(httpError.InternalServerErrorError) +export class InternalServerErrorFilter { + async catch(err: MidwayHttpError, ctx: Context) { + + // ... + return 'got 500 error, ' + err.message; + } +} +``` + +`catch` 方法的参数为当前的错误,以及当前应用该异常处理器的上下文 `Context` 。我们可以简单的将响应的数据返回。 + +如果不写参数,那么会捕获所有的错误,不管是不是 HttpError,只在要请求中抛出的错误,都会被这里捕获。 + +```typescript +// src/filter/all.filter.ts +import { Catch } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Catch() +export class AllErrorFilter { + async catch(err: Error, ctx: Context) { + // ... + } +} +``` + +定义的异常处理器只是一段普通的代码,我们还需要将它应用到我们某个框架的 app 中,比如 http 协议的 app。 + +我们可以在 `src/configuration.ts` 中将错误处理过滤器应用上,由于参数可以是数组,我们可以应用多个错误处理器。 + +```typescript +// src/configuration.ts +import { Configuration, App, Catch } from '@midwayjs/core'; +import { join } from 'path'; +import * as koa from '@midwayjs/koa'; +import { InternalServerErrorFilter } from './filter/internal.filter'; + +@Configuration({ + imports: [ + koa + ], +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + this.app.useFilter([InternalServerErrorFilter]); + } +} + +``` + +:::info + +注意,某些非 Midway 的中间件或者框架内部设置的状态码,由于未使用错误抛出的形式,所以拦截不到,如果在业务中返回 400 以上的状态,请尽可能使用标准的抛出错误的形式,方便拦截器做处理。 + +::: + + + +### 404 处理 + +框架内部,如果未匹配到路由,会抛出一个 `NotFoundError` 的异常。通过异常处理器,我们可以自定义其行为。 + +比如跳转到某个页面,或者返回特定的结果: + +```typescript +// src/filter/notfound.filter.ts +import { Catch, httpError, MidwayHttpError } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Catch(httpError.NotFoundError) +export class NotFoundFilter { + async catch(err: MidwayHttpError, ctx: Context) { + // 404 错误会到这里 + ctx.redirect('/404.html'); + + // 或者直接返回一个内容 + return { + message: '404, ' + ctx.path + } + } +} +``` + + + +### 500 处理 + +当不传递装饰器参数时,将捕获所有的错误。 + +比如,捕获所有的错误,并返回特定的 JSON 结构,示例如下。 + +```typescript +// src/filter/default.filter.ts + +import { Catch } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Catch() +export class DefaultErrorFilter { + async catch(err: Error, ctx: Context) { + + // ... + return { + status: err.status ?? 500, + message: err.message; + } + } + +} +``` + +我们可以在 `src/configuration.ts` 中将错误处理过滤器应用上,由于参数可以是数组,我们可以应用多个错误处理器。 + +```typescript +import { Configuration, App, Catch } from '@midwayjs/core'; +import { join } from 'path'; +import * as koa from '@midwayjs/koa'; +import { DefaultErrorFilter } from './filter/default.filter'; +import { NotFoundFilter } from './filter/notfound.filter'; + +@Configuration({ + imports: [ + koa + ], +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + this.app.useFilter([NotFoundFilter, DefaultErrorFilter]); + } +} + +``` + +使用异常处理器不需要考虑顺序,通用的错误处理器一定是最后被匹配,且一个 app 上有且只能有一个通用的错误处理器。 + + + +## 派生异常处理 + +默认情况下,异常只会进行绝对匹配。 + +有时候我们需要去捕获所有的派生类,这个时候需要额外设置。 + +```typescript +import { Catch, MidwayError } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +class CustomError extends MidwayError {} + +class CustomError2 extends MidwayError {} + +// 这里会捕获所有的子类 +@Catch([MidwayError], { + matchPrototype: true +}) +class TestFilter { + catch(err, ctx) { + // ... + } +} +``` + +通过配置 `matchPrototype` 可以匹配所有的派生的类。 + + + +## 异常日志 + +Midway 内置了默认的异常处理行为。 + +如果 **没有匹配** 到异常处理器,都会被兜底的异常中间件拦截,记录。 + +反过来说,如果自定义了异常处理器,那么错误就会被当成正常的业务逻辑,请注意,这个时候底层抛出的异常就会作为业务正常的处理逻辑,而 **不会** 被日志记录。 + +你可以自行在异常处理器中打印日志。 + +```typescript +import { Catch } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Catch() +export class DefaultErrorFilter { + async catch(err: Error, ctx: Context) { + + // ... + ctx.logger.error(err); + // ... + return 'got 500 error, ' + err.message; + } +} +``` + + + +## 内置的 Http 异常 + +下面这些框架内置的 Http 异常,都可以从 `@midwayjs/core` 中找到并使用,每个异常都已经包含默认的错误消息和状态码。 + +- `BadRequestError` +- `UnauthorizedError` +- `NotFoundError` +- `ForbiddenError` +- `NotAcceptableError` +- `RequestTimeoutError` +- `ConflictError` +- `GoneError` +- `PayloadTooLargeError` +- `UnsupportedMediaTypeError` +- `UnprocessableEntityError` +- `InternalServerErrorError` +- `NotImplementedError` +- `BadGatewayError` +- `ServiceUnavailableError` +- `GatewayTimeoutError` + +比如: + +```typescript +import { httpError } from '@midwayjs/core'; + +// ... + +async findAll() { + // something wrong + throw new httpError.InternalServerErrorError(); +} + +// got status: 500 + +``` + diff --git a/site/versioned_docs/version-3.0.0/esm.md b/site/versioned_docs/version-3.0.0/esm.md new file mode 100644 index 000000000000..ba53d7354f2c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/esm.md @@ -0,0 +1,152 @@ +# ESModule 使用指南 + +在过去的几年中,Node.js一直致力于支持运行 ECMAScript模块 (ESM)。这是一个很难支持的功能,因为 Node.js 生态系统的基础是建立在一个不同的模块系统,称为 CommonJS (CJS)。 + +两个模块系统之间的互操作带来了巨大的挑战,并具有许多功能差异。 + +自 Node.js v16 之后,ESM 的支持相对已经稳定,TypeScript 的一些配合功能也相继落地。 + +在此基础上,Midway 支持了 ESM 格式的文件加载,业务也可以使用这种全新的模块加载方式来构建自己的业务。 + +:::caution + +在没有了解 ESM 前,不建议用户使用。 + +::: + +推荐阅读: + +* [TypeScript 官方 ESM 指南](https://www.typescriptlang.org/docs/handbook/esm-node.html) +* [Node.js 官方 ESM 文档](https://nodejs.org/api/esm.html) + + + +## 脚手架 + +由于改动较多,Midway 提供了全新的 ESM 格式的脚手架,如有 ESM 的需求,我们推荐用户重新创建后再来开发业务。 + +```bash +$ npm init midway@latest -y +``` + +选择 esm 分组中的脚手架。 + + + +## 和 CJS 项目的差异 + +### 1、package.json 的变化 + + `package.json` 中的 type 必须设置为 `module`。 + +```json +{ + "name": "my-package", + "type": "module", + // ... + "dependencies": { + } +} +``` + + + +### 2、tsconfig.json 中的变化 + +`compilerOptions` 编译相关的选项需要设置为 `Node16` 或者 `NodeNext`。 + +```json +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Node16", + "esModuleInterop": true, + // ... + } +} +``` + + + +### 3、工具链的变化 + +由于原有开发工具链仅支持 CJS 代码,且社区的部分模块并没有做好 ESM 的支持,Midway 在 ESM 模式下,使用新的工具链。 + +* 开发命令,使用 mwtsc (仅做了 tsc 必要的包裹) +* 测试和覆盖率命令,使用 mocha + ts-node,同时测试代码和测试的配置都有所调整 +* 构建命令,使用 tsc + +一些不再支持的功能 + +* alias path,请用 Node.js 自带的 [子路径导出](https://nodejs.org/api/packages.html#subpath-exports) 代替 +* 构建时非 js 文件的拷贝,将非代码文件放到 src 外部,或者在 build 时添加自定义命令 + +具体差异可以参考 [脚手架](https://github.com/midwayjs/midway-boilerplate/blob/master/v3/midway-framework-koa-esm/boilerplate/_package.json) 进行核对。 + + + +### 4、一些代码差异 + +下面快速列出一些开发中 ESM 和 CJS 的差异。 + + + +1、ts 中,import 的文件必须指定后缀名,且后缀名为 js。 + +```typescript +import { helper } from "./foo.js"; // works in ESM & CJS +``` + + + +2、你不能再使用 `module.exports` 或者 `exports.` 来导出。 + +```typescript +// ./foo.ts +export function helper() { + // ... +} +// ./bar.ts +import { helper } from "./foo"; // only works in CJS +``` + + + +3、你不能在代码中使用 `require` + +只能使用 `import` 关键字。 + + + +4、你不能在代码中使用 `__dirname`,`__filename` 等和路径相关关键字 + +```typescript +// ESM solution +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(fileURLToPath(import.meta.url)) +``` + +所有配置的部分,必须使用对象模式。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import DefulatConfig from './config/config.default.js'; +import UnittestConfig from './config/config.unittest.js'; + +@Configuration({ + importConfigs: [ + { + default: DefulatConfig, + unittest: UnittestConfig, + }, + ], +}) +export class MainConfiguration { + // ... +} +``` + diff --git a/site/versioned_docs/version-3.0.0/extensions/alinode.md b/site/versioned_docs/version-3.0.0/extensions/alinode.md new file mode 100644 index 000000000000..bce5e62a3877 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/alinode.md @@ -0,0 +1,83 @@ +# Alinode + +## 准备工作 + +需要接入的应用是要部署在独立的服务获取云环境,可以接入互联网服务。 + +## 创建服务 + +**第一步** + +登录阿里云,点击开通 [阿里云的 Node.js 性能平台](https://www.aliyun.com/product/nodejs) 的服务。 + +**第二步** + +创建新应用,获取 APP ID 和 App Secret。 + + +## 安装监控依赖 + +**第一步** + +安装 Node.js 性能平台所需的组件 + +```bash +# 安装版本管理工具 tnvm,安装过程出错参考:https://github.com/aliyun-node/tnvm +$ wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash +$ source ~/.bashrc + +# tnvm ls-remote alinode # 查看需要的版本 +$ tnvm install alinode-v6.5.0 # 安装需要的版本 +$ tnvm use alinode-v6.5.0 # 使用需要的版本 + +$ npm install @alicloud/agenthub -g # 安装 agenthub +``` + +这里有三个部分 + +- 1、安装 tnvm(alinode 源) +- 2、使用 tnvm 安装 alinode(替代默认的 node) +- 3、安装 alinode 需要的数据采集器 + +安装完成后,可以检查一下,需要确保 `which node` 和 `which agenthub` 的路径中包括 `.tnvm` 即可。 + +```bash +$ which node +/root/.tnvm/versions/alinode/v3.11.4/bin/node + +$ which agenthub +/root/.tnvm/versions/alinode/v3.11.4/bin/agenthub +``` + +将 `创建新应用` 中获得的 `App ID` 和 `App Secret` 按如下所示保存为 `yourconfig.json`。比如放在项目根目录。 + +```typescript +{ + "appid": "****", + "secret": "****", +} +``` + +启动插件: + +```typescript +agenthub start yourconfig.json +``` + +## 启动 node 服务 + +在安装了服务器中,启动 Node 服务时,需要加入 ENABLE_NODE_LOG=YES 环境变量。 + +比如: + +```bash +$ NODE_ENV=production ENABLE_NODE_LOG=YES node bootstrap.js +``` + +## Docker 容器的方法 + +关于 docker 容器的方法可以查看 [文档](https://help.aliyun.com/document_detail/66027.html?spm=a2c4g.11186623.6.580.261ba70feI6mWt)。 + +## 其他 + +更多内容可以查看阿里云 Node.js 性能平台的 [文档](https://help.aliyun.com/document_detail/60338.html?spm=a2c4g.11186623.6.548.599312e6IkGO9v)。 diff --git a/site/versioned_docs/version-3.0.0/extensions/axios.md b/site/versioned_docs/version-3.0.0/extensions/axios.md new file mode 100644 index 000000000000..d14b52213f96 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/axios.md @@ -0,0 +1,433 @@ +# HTTP 请求 + + + +## 简单的 HTTP 请求 + +Midway 内置了一个简单的 HTTP 请求客户端,无需引入三方包即可使用。 + +默认 Get 请求,返回数据为 Buffer。 + +内置的 Http 客户端只提供最简单的能力,仅满足大部分的前端接口数据获取,如需复杂的功能,比如文件上传等,请使用其他的客户端,如 fetch,axios,got 等。 + + + +### 简单方法形式 + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +const result = await makeHttpRequest('http://127.1:7001/'); + +// Buffer.isBuffer(result.data) => true +``` + +Get 请求,带上 Query,返回类型为 JSON。 + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +const result = await makeHttpRequest('http://127.1:7001/', { + data: { + a: 1, + b: 2 + }, + dataType: 'json', // 返回的数据格式 +}); + +// typeof result.data => 'object' +// result.data.url => /?a=1&b=2 +``` + +可以指定类型 + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +const result = await makeHttpRequest('http://127.1:7001/', { + method: 'GET', + dataType: 'json', +}); +``` + +返回 text 格式。 + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +const result = await makeHttpRequest('http://127.1:7001/', { + method: 'GET', + dataType: 'text', +}); +``` + +POST 请求并返回 JSON。 + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +const result = await makeHttpRequest('http://127.1:7001/', { + method: 'POST', + data: { + a: 1, + b: 2 + }, + dataType: 'json', + contentType:'json', // 发送的 post 为 json +}); + +// result.data ... +``` + +:::caution +注意,请不要在请求中直接返回 result 对象,result 对象是标准的 httpResponse,在大部分场景下无法被直接序列化,会抛出对象循环的错误。 +::: + +设置请求超时时间。 + +```typescript +import { makeHttpRequest } from '@midwayjs/core'; + +let err; +// 超时会报错,注意 catch +try { + const result = await makeHttpRequest('http://127.1:7001/', { + method: 'GET', + dataType: 'text', + timeout: 500, + }); +} catch (e) { + err = e; +} +``` + + + +### 实例形式 + +```typescript +import { HttpClient } from '@midwayjs/core'; + +const httpclient = new HttpClient(); +const result = await httpclient.request('http://127.1:7001/'); + +// Buffer.isBuffer(result.data) => true +``` + +和方法形式参数相同。 + +```typescript +import { HttpClient } from '@midwayjs/core'; + +const httpclient = new HttpClient(); +const result = await httpclient.request('http://127.1:7001/', { + method: 'POST', + data: { + a: 1, + b: 2 + }, + dataType: 'json', + contentType:'json', // 发送的 post 为 json +}); + +// result.data ... +``` + +示例形式,可以复用创建出的对象,并且每次请求,都可以带上一些固定的参数,比如 header。 + +```typescript +import { HttpClient } from '@midwayjs/core'; + +const httpclient = new HttpClient({ + headers: { + 'x-timeout': '5' + }, + method: 'POST', + timeout: 2000 +}); + +// 每次都会带上 headers +const result = await httpclient.request('http://127.1:7001/'); + +``` + + + + + +## Axios 支持 + +Midway 包裹了 [axios](https://github.com/axios/axios) 包,使得在代码中可以简单的使用 axios 接口。 + +和 axios 的一些关系如下: + +- 接口完全一致 +- 适配依赖注入写法,完整的类型定义 +- 方便实例管理和配置统一 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + + +### 安装依赖 + +```bash +$ npm i @midwayjs/axios@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/axios": "^3.0.0", + // ... + }, +} +``` + + + +### 引入组件 + + +首先,引入 组件,在 `configuration.ts` 中导入: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as axios from '@midwayjs/axios'; +import { join } from 'path' + +@Configuration({ + imports: [ + axios // 导入 axios 组件 + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + +然后在业务代码中即可注入使用。 + + + +### 使用默认 Axios 实例 + + +接口和 [axios](https://github.com/axios/axios) 一致。 + + +```typescript +axios.request(config) +axios.get(url[, config]) +axios.delete(url[, config]) +axios.head(url[, config]) +axios.options(url[, config]) +axios.post(url[, data[, config]]) +axios.put(url[, data[, config]]) +axios.patch(url[, data[, config]]) +axios.postForm(url[, data[, config]]) +axios.putForm(url[, data[, config]]) +axios.patchForm(url[, data[, config]]) +``` + + +使用示例: +```typescript +import { HttpService } from '@midwayjs/axios'; + +@Provide() +export class UserService { + + @Inject() + httpService: HttpService; + + async invoke() { + const url = 'https://midwayjs.org/resource/101010100.json'; + const result = await this.httpService.get(url); + // TODO result + } +} +``` + + + +### 配置默认 Axios 实例 + + +HttpService 实例等价于 `axios.create` ,所以可以有一些配置参数,这些参数了 axios 本身的参数相同,我们可以在 `src/config.default.ts` 中配置它。 + + +比如: +```typescript +export default { + // ... + axios: { + default: { + // 所有实例复用的配置 + }, + clients: { + // 默认实例的配置 + default: { + baseURL: 'https://api.example.com', + // `headers` are custom headers to be sent + headers: { + 'X-Requested-With': 'XMLHttpRequest' + }, + timeout: 1000, // default is `0` (no timeout) + + // `withCredentials` indicates whether or not cross-site Access-Control requests + // should be made using credentials + withCredentials: false, // default + }, + } + } +} +``` +更多的参数可以参考 [axios global config](https://github.com/axios/axios#config-defaults)。 + + + +### 创建不同实例 + +和其他的服务多实例相同,配置不同的 key 即可。 + +```typescript +export default { + // ... + axios: { + default: { + // 所有实例复用的配置 + }, + clients: { + default: { + // 默认实例 + }, + customAxios: { + // 自定义实例 + } + } + } +} +``` + +使用方式如下: + +```typescript +import { HttpServiceFactory, HttpService } from '@midwayjs/axios'; +import { InjectClient } from '@midwayjs/core'; + +@Provide() +export class UserService { + + @InjectClient(HttpServiceFactory, 'customAxios') + customAxios: HttpService; + + async invoke() { + const url = 'https://midwayjs.org/resource/101010100.json'; + const result = await this.customAxios.get(url); + // TODO result + } +} +``` + + + +### 配置全局拦截器 + +如果使用的是默认的 Axios 实例,可以如下配置。 + +```javascript +import { Configuration, IMidwayContainer } from '@midwayjs/core'; +import * as axios from '@midwayjs/axios'; +import { join } from 'path'; + +@Configuration({ + imports: [ + axios // 导入 axios 组件 + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { + + async onReady(container: IMidwayContainer) { + const httpService = await container.getAsync(axios.HttpService); + httpService.interceptors.request.use( + config => { + // Do something before request is sent + return config; + }, + error => { + // Do something with request error + return Promise.reject(error); + } + ); + } +} +``` + +如果要给其他实例配置,可以参考下面的代码。 + +```typescript +import { Configuration, IMidwayContainer } from '@midwayjs/core'; +import * as axios from '@midwayjs/axios'; +import { join } from 'path'; + +@Configuration({ + imports: [ + axios // 导入 axios 组件 + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { + + async onReady(container: IMidwayContainer) { + const httpServiceFactory = await container.getAsync(axios.HttpServiceFactory); + const customAxios = httpServiceFactory.get('customAxios'); + customAxios.interceptors.request.use( + config => { + //... + }, + error => { + //... + } + ); + } +} +``` + +### 直接使用 Axios + +`@midayjs/axios`导出了原始的`axios`实例,在非应用环境中可以直接使用。 + +```typescript +import { Axios } from '@midwayjs/axios'; +import { ReadStream, createWriteStream } from 'fs'; +import { finished } from 'stream/promises'; + +async function download(url: string, filename: string) { + const writer = await createWriteStream(filename); + const res = Axios.get(url, { + responseType: 'stream', + }); + res.data.pipe(writer); + await finished(writer); + return res; +} +``` diff --git a/site/versioned_docs/version-3.0.0/extensions/bull.md b/site/versioned_docs/version-3.0.0/extensions/bull.md new file mode 100644 index 000000000000..1b25a9dd71ef --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/bull.md @@ -0,0 +1,879 @@ +# 任务队列 + +队列是一种强大的设计模式,可帮助您应对常见的应用程序扩展和性能挑战。队列可以帮助您解决的一些问题。 + +示例如下: + +- 平滑处理峰值。可以在任意时间启动资源密集型任务,然后将这些任务添加到队列中,而不是同步执行。让任务进程以受控方式从队列中提取任务。也可以轻松添加新的队列消费者以扩展后端任务处理。 +- 分解可能会阻塞 Node.js 事件循环的单一任务。比如用户请求需要像音频转码这样的 CPU 密集型工作,就可以将此任务委托给其他进程,从而释放面向用户的进程以保持响应。 +- 提供跨各种服务的可靠通信渠道。例如,您可以在一个进程或服务中排队任务(作业),并在另一个进程或服务中使用它们。在任何流程或服务的作业生命周期中完成、错误或其他状态更改时,您都可以收到通知(通过监听状态事件)。当队列生产者或消费者失败时,它们的状态被保留,并且当节点重新启动时任务处理可以自动重新启动。 + +Midway 提供了 @midwayjs/bull 包作为 [Bull](https://github.com/OptimalBits/bull) 之上的抽象/包装器,[Bull](https://github.com/OptimalBits/bull) 是一种流行的、受良好支持的、高性能的基于 Node.js 的队列系统实现。该软件包可以轻松地将 Bull Queues 以友好的方式集成到您的应用程序中。 + +Bull 使用 Redis 来保存作业数据,在使用 Redis 时,Queue 架构是完全分布式,和平台无关。例如,您可以在一个(或多个)节点(进程)中运行一些 Queue 生产者、消费者,而在其他节点上的运行其他生产者和消费者。 + +本章介绍 @midwayjs/bull 包。我们还建议阅读 [Bull 文档](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md) 以了解更多背景和具体实施细节。 + +:::tip + +- 1、从 v3.6.0 开始,原有任务调度 `@midwayjs/task` 模块废弃,如果查询历史文档,请参考 [这里](../legacy/task)。 +- 2、bull 是一个分布式任务管理系统,必须依赖 redis + +::: + + + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ❌ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ✅ | +| 包含独立日志 | ✅ | + + + +## 安装组件 + +```bash +$ npm i @midwayjs/bull@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/bull": "^3.0.0", + // ... + }, +} +``` + + + +## 使用组件 + +将 bull 组件配置到代码中。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as bull from '@midwayjs/bull'; + +@Configuration({ + imports: [ + // ... + bull + ] +}) +export class MainConfiguration { + //... +} +``` + + + +## 一些概念 + +Bull 将整个队列分为三个部分 + +- 1、Queue 队列,管理任务 +- 2、Job,每个任务对象,可以对任务进行启停控制 +- 3、Processor,任务处理,实际的逻辑执行部分 + + + +## 基础配置 + +bull 是一个分布式任务管理器,强依赖于 redis,在 `config.default.ts` 文件中配置。 + +```typescript +// src/config/config.default.ts +export default { + // ... + bull: { + // 默认的队列配置 + defaultQueueOptions: { + redis: `redis://127.0.0.1:32768`, + } + }, +} +``` + +有账号密码情况: + +```typescript +// src/config/config.default.ts +export default { + // ... + bull: { + defaultQueueOptions: { + redis: { + port: 6379, + host: '127.0.0.1', + password: 'foobared', + }, + } + }, +} +``` + +所有的队列都会复用该配置。 + + + +## 编写任务处理器 + +使用 `@Processor` 装饰器装饰一个类,用于快速定义一个任务处理器(这里我们不使用 Job,避免后续的歧义)。 + +`@Processor` 装饰器需要传递一个 Queue (队列)的名字,在框架启动时,如果没有名为 `test` 的队列,则会自动创建。 + +比如,我们在 `src/queue/test.queue.ts` 文件中编写如下代码。 + +```typescript +// src/queue/test.queue.ts +import { Processor, IProcessor } from '@midwayjs/bull'; + +@Processor('test') +export class TestProcessor implements IProcessor { + async execute() { + // ... + } +} +``` + +在启动时,框架会自动查找并初始化上述处理器代码,同时自动创建一个名为 `test` 的 Queue。 + + + + + +## 执行任务 + +当定义完 Processor 之后,由于并未指定 Processor 如何执行,我们还需要手动执行它。 + +通过获取对应的队列,我们可以很方便的来执行任务。 + + + +### 手动执行任务 + +比如,我们可以在项目启动后执行。 + +```typescript +import { Configuration, Inject } from '@midwayjs/core'; +import * as bull from '@midwayjs/bull'; + +@Configuration({ + imports: [ + // ... + bull + ] +}) +export class MainConfiguration { + + @Inject() + bullFramework: bull.Framework; + + //... + + async onServerReady() { + // 获取 Processor 相关的队列 + const testQueue = this.bullFramework.getQueue('test'); + // 立即执行这个任务 + await testQueue?.runJob(); + } +} +``` + + + +### 增加执行参数 + +我们也可以在执行时,附加一些默认参数。 + +```typescript +@Processor('test') +export class TestProcessor implements IProcessor { + async execute(params) { + // params.aaa => 1 + } +} + + +// invoke +const testQueue = this.bullFramework.getQueue('test'); +// 立即执行这个任务 +await testQueue?.runJob({ + aaa: 1, + bbb: 2, +}); +``` + + + +### 任务状态和管理 + +执行 `runJob` 后,我们可以获取到一个 `Job` 对象。 + +```typescript +// invoke +const testQueue = this.bullFramework.getQueue('test'); +const job = await testQueue?.runJob(); +``` + +通过这个 Job 对象,我们可以做进度管理。 + +```typescript +// 更新进度 +await job.progress(60); +// 获取进度 +const progress = await job.process(); +// => 60 +``` + +获取任务状态。 + +```typescript +const state = await job.getState(); +// state => 'delayed' 延迟状态 +// state => 'completed' 完成状态 +``` + +更多的 Job API,请查看 [文档](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md)。 + + + +### 延迟执行 + +执行任务时,也有一些额外的选项。 + +比如,延迟 1s 执行。 + +```typescript +const testQueue = this.bullFramework.getQueue('test'); +// 立即执行这个任务 +await testQueue?.runJob({}, { delay: 1000 }); +``` + + + +### 中间件和错误处理 + +Bull 组件包含可以独立启动的 Framework,有着自己的 App 对象和 Context 结构。 + +我们可以对 bull 的 App 配置独立的中间件和错误过滤器。 + +```typescript +@Configuration({ + imports: [ + // ... + bull + ] +}) +export class MainConfiguration { + + @App('bull') + bullApp: bull.Application; + + //... + + async onReady() { + this.bullApp.useMiddleare( /*中间件*/); + this.bullApp.useFilter( /*过滤器*/); + } +} +``` + + + +### 上下文 + +任务处理器执行是在请求作用域中,其有着特殊的 Context 对象结构。 + +```typescript +export interface Context extends IMidwayContext { + jobId: JobId; + job: Job, + from: new (...args) => IProcessor; +} +``` + +我们可以直接从 ctx 中访问当前的 Job 对象。 + +```typescript +// src/queue/test.queue.ts +import { Processor, IProcessor, Context } from '@midwayjs/bull'; + +@Processor('test') +export class TestProcessor implements IProcessor { + + @Inject() + ctx: Context; + + async execute() { + // ctx.jobId => xxxx + } +} +``` + + + +### 更多任务选项 + +除了上面的 delay 之外,还有更多的执行选项。 + +| 选项 | 类型 | 描述 | +| ---------------- | --------------------- | ------------------------------------------------------------ | +| priority | number | 可选的优先级值。范围从 1(最高优先级)到 MAX_INT(最低优先级)。请注意,使用优先级对性能有轻微影响,因此请谨慎使用。 | +| delay | number | 等待可以处理此作业的时间量(毫秒)。请注意,为了获得准确的延迟,服务器和客户端都应该同步它们的时钟。 | +| attempts | number | 在任务完成之前尝试尝试的总次数。 | +| repeat | RepeatOpts | 根据 cron 规范的重复任务配置,更多可以查看 [RepeatOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd),以及下面的重复任务介绍。 | +| backoff | number \| BackoffOpts | 任务失败时自动重试的回退设置。请参阅 [BackoffOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd)。 | +| lifo | boolean | 如果为 true,则将任务添加到队列的右端而不是左端(默认为 false)。 | +| timeout | number | 任务因超时错误而失败的毫秒数。 | +| jobId | number \| string | 覆盖任务 id - 默认情况下,任务 id 是唯一整数,但您可以使用此设置覆盖它。如果您使用此选项,则由您来确保 jobId 是唯一的。如果您尝试添加一个 id 已经存在的任务,它将不会被添加。 | +| removeOnComplete | boolean \| number | 如果为 true,则在成功完成后删除任务。如果设置数字,则为指定要保留的任务数量。默认行为是任务信息保留在已完成列表中。 | +| removeOnFail | boolean \| number | 如果为 true,则在所有尝试后都失败时删除任务。如果设置数字,指定要保留的任务数量。默认行为是将任务信息保留在失败列表中。 | +| stackTraceLimit | number | 限制将在堆栈跟踪中记录的堆栈跟踪行的数量。 | + + + +## 重复执行的任务 + +除了手动执行的方式,我们也可以通过 `@Processor` 装饰器的参数,快速配置任务的重复执行。 + +```typescript +import { Processor, IProcessor } from '@midwayjs/bull'; +import { FORMAT } from '@midwayjs/core'; + +@Processor('test', { + repeat: { + cron: FORMAT.CRONTAB.EVERY_PER_5_SECOND + } +}) +export class TestProcessor implements IProcessor { + @Inject() + logger; + + async execute() { + // ... + } +} +``` + + + +## 常用 Cron 表达式 + +关于 Cron 表达式,格式如下。 + +```typescript +* * * * * * +┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ | +│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) +│ │ │ │ └───── month (1 - 12) +│ │ │ └────────── day of month (1 - 31) +│ │ └─────────────── hour (0 - 23) +│ └──────────────────── minute (0 - 59) +└───────────────────────── second (0 - 59, optional) +``` + + + +常见表达式: + +- 每隔5秒执行一次:`*/5 * * * * *` +- 每隔1分钟执行一次:`0 */1 * * * *` +- 每小时的20分执行一次:`0 20 * * * *` +- 每天 0 点执行一次:`0 0 0 * * *` +- 每天的两点35分执行一次:`0 35 2 * * *` + +可以使用 [在线工具](https://cron.qqe2.com/) 执行确认下一次执行的时间。 + +Midway 在框架侧提供了一些常用的表达式,放在 `@midwayjs/core` 中供大家使用。 + +```typescript +import { FORMAT } from '@midwayjs/core'; + +// 每分钟执行的 cron 表达式 +FORMAT.CRONTAB.EVERY_MINUTE +``` + + + +内置的还有一些其他的表达式。 + +| 表达式 | 对应时间 | +| ------------------------------ | --------------- | +| CRONTAB.EVERY_SECOND | 每秒钟 | +| CRONTAB.EVERY_MINUTE | 每分钟 | +| CRONTAB.EVERY_HOUR | 每小时整点 | +| CRONTAB.EVERY_DAY | 每天 0 点 | +| CRONTAB.EVERY_DAY_ZERO_FIFTEEN | 每天 0 点 15 分 | +| CRONTAB.EVERY_DAY_ONE_FIFTEEN | 每天 1 点 15 分 | +| CRONTAB.EVERY_PER_5_SECOND | 每隔 5 秒 | +| CRONTAB.EVERY_PER_10_SECOND | 每隔 10 秒 | +| CRONTAB.EVERY_PER_30_SECOND | 每隔 30 秒 | +| CRONTAB.EVERY_PER_5_MINUTE | 每隔 5 分钟 | +| CRONTAB.EVERY_PER_10_MINUTE | 每隔 10 分钟 | +| CRONTAB.EVERY_PER_30_MINUTE | 每隔 30 分钟 | + + + +## 高级配置 + + + +### 清理之前的任务 + +在默认情况下,框架会自动清理前一次未调度的 **重复执行任务**,保持每一次的重复执行的任务队列为最新。如果在某些环境不需要清理,可以单独关闭。 + +比如你不需要清理重复: + +```typescript +// src/config/config.prod.ts +export default { + // ... + bull: { + clearRepeatJobWhenStart: false, + }, +} +``` + +:::tip + +如果不清理,如果前一次队列为 10s 执行,现在修改为 20s 执行,则两个定时都会存储在 Redis 中,导致代码重复执行。 + +在日常的开发中,如果不清理,很容易出现代码重复执行这个问题。但是在集群部署的场景,多台服务器轮流重启的情况下,可能会导致定时任务被意外清理,请评估开关的时机。 + +::: + + + +也可以在启动时手动清理所有任务。 + +```typescript +// src/configuration.ts +import { Configuration, App, Inject } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { join } from 'path'; +import * as bull from '@midwayjs/bull'; + +@Configuration({ + imports: [koa, bull], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration { + @App() + app: koa.Application; + + @Inject() + bullFramework: bull.Framework; + + async onReady() { + // 在这个阶段,装饰器队列还未创建,使用 API 提前手动创建队列,装饰器会复用同名队列 + const queue = this.bullFramework.createQueue('user'); + // 通过队列手动执行清理 + await queue.obliterate({ force: true }); + } +} +``` + + + + + +### 清理任务历史记录 + +当开启 Redis 后,默认情况下,bull 会记录所有的成功和失败的任务 key,这可能会导致 redis 的 key 暴涨,我们可以配置成功或者失败后清理的选项。 + +默认情况下 + +- 成功时保留的任务记录为 3 条 +- 失败保留的任务记录为 10 条 + +也可以通过参数进行配置。 + +比如在装饰器配置。 + +```typescript +import { FORMAT } from '@midwayjs/core'; +import { IProcessor, Processor } from '@midwayjs/bull'; + +@Processor('user', { + repeat: { + cron: FORMAT.CRONTAB.EVERY_MINUTE, + }, + removeOnComplete: 3, // 成功后移除任务记录,最多保留最近 3 条记录 + removeOnFail: 10, // 失败后移除任务记录 +}) +export class UserService implements IProcessor { + execute(data: any) { + // ... + } +} + +``` + +也可以在全局 config 中配置。 + +```typescript +// src/config/config.default.ts +export default { + // ... + bull: { + defaultQueueOptions: { + // 默认的任务配置 + defaultJobOptions: { + // 保留 10 条记录 + removeOnComplete: 10, + }, + }, + }, +} +``` + + + + + +### Redis 集群 + +可以使用 bull 提供的 `createClient` 方式来接入自定义的 redis 实例,这样你可以接入 Redis 集群。 + +比如: + +```typescript +// src/config/config.default +import Redis from 'ioredis'; + +const clusterOptions = { + enableReadyCheck: false, // 一定要是false + retryDelayOnClusterDown: 300, + retryDelayOnFailover: 1000, + retryDelayOnTryAgain: 3000, + slotsRefreshTimeout: 10000, + maxRetriesPerRequest: null // 一定要是null +} + +const redisClientInstance = new Redis.Cluster([ + { + port: 7000, + host: '127.0.0.1' + }, + { + port: 7002, + host: '127.0.0.1' + }, +], clusterOptions); + +export default { + bull: { + defaultQueueOptions: { + createClient: (type, opts) => { + return redisClientInstance; + }, + // 这些任务存储的 key,都是相同开头,以便区分用户原有 redis 里面的配置 + prefix: '{midway-bull}', + }, + } +} +``` + + + +## 队列管理 + +队列是廉价的,每个 Job 都会绑定一个队列,在一些情况下,我们也可以手动对队列进行管理操作。 + + + +### 手动创建队列 + +除了使用 `@Processor` 简单定义队列,我们还可以使用 API 进行创建。 + +```typescript +import { Configuration, Inject } from '@midwayjs/core'; +import * as bull from '@midwayjs/bull'; + +@Configuration({ + imports: [ + // ... + bull + ] +}) +export class MainConfiguration { + + @Inject() + bullFramework: bull.Framework; + + async onReady() { + const testQueue = this.bullFramework.createQueue('test', { + redis: { + port: 6379, + host: '127.0.0.1', + password: 'foobared', + }, + prefix: '{midway-bull}', + }); + + // ... + } +} +``` + +通过 `createQueue` 手动创建队列后,队列依旧会自动保存。如果在启动时 `@Processor` 使用了该队列名,则会自动使用已经创建好的队列。 + +比如: + +```typescript +// 会自动使用上面手动创建的同名队列 +@Processor('test') +export class TestProcessor implements IProcessor { + async execute(params) { + } +} +``` + + + +### 获取队列 + +我们可以简单的根据队列名获取队列。 + +```typescript + const testQueue = bullFramework.getQueue('test'); +``` + +也可以通过装饰器来获取。 + +```typescript +import { InjectQueue, BullQueue } from '@midwayjs/bull'; +import { Provide } from '@midwayjs/core'; + +@Provide() +export class UserService { + @InjectQueue('test') + testQueue: BullQueue; + + async invoke() { + await this.testQueue.pause(); + // ... + } +} +``` + + + +### 队列常用操作 + +暂停队列。 + +```typescript +await testQueue.pause(); +``` + +继续队列。 + +```typescript +await testQueue.resume(); +``` + +队列事件。 + +```typescript +// Local events pass the job instance... +testQueue.on('progress', function (job, progress) { + console.log(`Job ${job.id} is ${progress * 100}% ready!`); +}); + +testQueue.on('completed', function (job, result) { + console.log(`Job ${job.id} completed! Result: ${result}`); + job.remove(); +}); +``` + +完整队列 API 请参考 [这里](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md)。 + + + +## 组件日志 + +组件有着自己的日志,默认会将 `ctx.logger` 记录在 `midway-bull.log` 中。 + +我们可以单独配置这个 logger 对象。 + +```typescript +export default { + midwayLogger: { + clients: { + // ... + bullLogger: { + fileLogName: 'midway-bull.log', + }, + }, + }, +} +``` + +这个日志的输出格式,我们也可以单独配置。 + +```typescript +export default { + bull: { + // ... + contextLoggerFormat: info => { + const { jobId, from } = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} [${jobId} ${from.name}] ${info.message}`; + }, + } +} +``` + + + +## 关于 Redis 版本 + +请尽可能选择最新的版本( >=5 ),目前在低版本 redis 上有发现定时任务创建失败的问题。 + + + +## Bull UI + +在分布式场景中,我们可以资利用 Bull UI 来简化管理。 + +和 bull 组件类似,需要独立安装和启用。 + +```bash +$ npm i @midwayjs/bull-board@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/bull-board": "^3.0.0", + // ... + }, +} +``` + +将 bull-board 组件配置到代码中。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as bull from '@midwayjs/bull'; +import * as bullBoard from '@midwayjs/bull-board'; + +@Configuration({ + imports: [ + // ... + bull, + bullBoard, + ] +}) +export class MainConfiguration { + //... +} +``` + +默认的访问路径为:`http://127.1:7001/ui`。 + +效果如下: + +![](https://img.alicdn.com/imgextra/i2/O1CN01j4wEFb1UacPxA06gs_!!6000000002534-2-tps-1932-1136.png) + +可以通过配置进行基础路径的修改。 + +```typescript +// src/config/config.prod.ts +export default { + // ... + bullBoard: { + basePath: '/ui', + }, +} +``` + +此外,组件提供了 `BullBoardManager` ,可以添加动态创建的队列。 + +```typescript +import { Configuration, Inject } from '@midwayjs/core'; +import * as bull from '@midwayjs/bull'; +import * as bullBoard from '@midwayjs/bull-board'; + +@Configuration({ + imports: [ + // ... + bull, + bullBoard + ] +}) +export class MainConfiguration { + + @Inject() + bullFramework: bull.Framework; + + @Inject() + bullBoardManager: bullBoard.BullBoardManager; + + async onReady() { + const testQueue = this.bullFramework.createQueue('test', { + // ... + }); + + this.bullBoardManager.addQueue(testQueue); + } +} +``` + + + +## 常见问题 + +### 1、EVALSHA 错误 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01KfjCKT1yypmNPDkIL_!!6000000006648-2-tps-3540-102.png) + +这个问题基本明确,问题会出现在 redis 的集群版本上。 + +原因是 redis 会对 key 做 hash 来确定存储的 slot,集群下这一步 @midwayjs/bull 的 key 命中了不同的 slot。 + +解决方案: task 里的 prefix 配置用 {} 包括,强制 redis 只计算 {} 里的hash,例如 `prefix: '{midway-task}'`。 + +### 2、EVAL inside MULTI is not allowed 错误 + +表现为 `queue.createBulk()`、`job.moveToFailed()` 等任务队列 API 调用无效,并出现下面的错误。 + +``` +ReplyError: EXECABORT Transaction discarded because of previous errors. + at parseError (/node_modules/redis-parser/lib/parser.js:179:12) + at parseType (/node_modules/redis-parser/lib/parser.js:302:14) { + command: { name: 'exec', args: [] }, + previousErrors: [ + ReplyError: ERR 'EVAL' inside MULTI is not allowed + at parseError (/node_modules/redis-parser/lib/parser.js:179:12) + at parseType (/node_modules/redis-parser/lib/parser.js:302:14) { + command: [Object] + } + ] +} +``` + +:::tip + +常出现于使用阿里云 Redis 服务。 + +::: + +由于这些 API 依赖的 Redis Lua 脚本中使用了 EVAL 或者 EVALSHA,阿里云 Redis 使用代理模式连接时,会对 Lua 脚本调用做额外限制,包括 [不允许在 MULTI 事务中执行 EVAL 命令](https://help.aliyun.com/zh/redis/support/usage-of-lua-scripts?#section-8f7-qgv-dlv),文档中还提到可以通过参数配置 script_check_enable 关闭这一校验,但是验证无效。 + +解决方案: + +* 1、在阿里云控制台操作开启直连地址,将服务切换到直连模式 +* 2、客户端切换成集群模式,参考上述「Redis 集群」章节,切换配置方式 diff --git a/site/versioned_docs/version-3.0.0/extensions/busboy.md b/site/versioned_docs/version-3.0.0/extensions/busboy.md new file mode 100644 index 000000000000..5af2d57d7180 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/busboy.md @@ -0,0 +1,763 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 文件上传 + +适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用上传组件,支持 `file` (服务器临时文件) 和 `stream` (流)两种模式。 + +相关信息: + +| web 支持情况 | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | 💬 | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + +:::caution + +💬 部分函数计算平台不支持流式请求响应,请参考对应平台能力。 + +::: + +:::tip + +本模块自 3.17.0 起替换 upload 组件。 + +和 upload 组件的差异为: + +* 1、配置的 key 从 `upload` 调整为 `busboy` +* 2、中间件不再默认加载,手动可配置到全局或者路由 +* 3、入参定义类型调整为 `UploadStreamFileInfo` +* 4、`fileSize` 的配置有调整 + +::: + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/busboy@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/busboy": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## 启用组件 + +```typescript +// src/configuratin.ts + +import { Configuration } from '@midwayjs/core'; +import * as busboy from '@midwayjs/busboy'; + +@Configuration({ + imports: [ + // ...other components + busboy + ], + // ... +}) +export class MainConfiguration {} +``` + + + +## 配置中间件 + +组件中提供了 `UploadMiddleware` 这个中间件,可以将其配置到全局或者特定路由,推荐配置到特定路由,提升性能。 + + + +**路由中间件** + +```typescript +import { Controller, Post } from '@midwayjs/core'; +import { UploadMiddleware } from '@midwayjs/busboy'; + +@Controller('/') +export class HomeController { + + @Post('/upload', { middleware: [UploadMiddleware] }) + async upload(/*...*/) { + // ... + } +} +``` + +**全局中间件** + + + + +```typescript +// src/configuratin.ts + +import { Configuration } from '@midwayjs/core'; +import * as busboy from '@midwayjs/busboy'; +import { Application } from '@midwayjs/koa'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App('koa') + app: Application; + + async onReady() { + this.app.useMiddleware(busboy.UploadMiddleware); + } +} +``` + + + +```typescript +// src/configuratin.ts + +import { Configuration } from '@midwayjs/core'; +import * as busboy from '@midwayjs/busboy'; +import { Application } from '@midwayjs/web'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App('egg') + app: Application; + + async onReady() { + this.app.useMiddleware(busboy.UploadMiddleware); + } +} +``` + + + +```typescript +// src/configuratin.ts + +import { Configuration } from '@midwayjs/core'; +import * as busboy from '@midwayjs/busboy'; +import { Application } from '@midwayjs/express'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App('express') + app: Application; + + async onReady() { + this.app.useMiddleware(busboy.UploadMiddleware); + } +} +``` + + + +```typescript +// src/configuratin.ts + +import { Configuration } from '@midwayjs/core'; +import * as busboy from '@midwayjs/busboy'; +import { Application } from '@midwayjs/faas'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App('faas') + app: Application; + + async onReady() { + this.app.useMiddleware(busboy.UploadMiddleware); + } +} +``` + + + + + +## 配置 + +组件使用 `busboy` 作为配置的 key。 + + + +### 上传模式 + +上传分为三种模式,文件模式,流式模式以及新增的异步迭代器模式。 + +代码中使用 `@Files()` 装饰器获取上传的文件, `@Fields` 装饰器获取其他上传表单字段。 + + + + +`file` 为默认值,配置 mode 为 `file` 字符串。 + +```typescript +// src/config/config.default.ts +export default { + // ... + busboy: { + mode: 'file', + }, +} +``` + +在代码中获取上传的文件,支持同时上传多个文件。 + +```typescript +import { Controller, Post, Files, Fields } from '@midwayjs/core'; +import { UploadFileInfo } from '@midwayjs/busboy'; + +@Controller('/') +export class HomeController { + + @Post('/upload', /*...*/) + async upload(@Files() files: Array, @Fields() fields: Record) { + /* + files = [ + { + filename: 'test.pdf', // 文件原名 + data: '/var/tmp/xxx.pdf', // 服务器临时文件地址 + mimeType: 'application/pdf', // mime + fieldName: 'file' // field name + }, + ] + */ + } +} +``` + +使用 file 模式时, 获取的 `data` 为上传的文件在服务器的 `临时文件地址`,后续可以再通过 `fs.createReadStream` 等方式来处理此文件内容,支持同时上传多个文件,多个文件会以数组的形式存放。 + +每个数组内的对象包含以下几个字段 + +```typescript +export interface UploadFileInfo { + /** + * 上传的文件名 + */ + filename: string; + /** + * 上传文件 mime 类型 + */ + mimeType: string; + /** + * 上传服务端保存的路径 + */ + data: string; + /** + * 上传的表单字段名 + */ + fieldName: string; +} +``` + + + + + + + +从 `v3.18.0` 提供,替代原有的 `stream` 模式,该模式支持多个文件流式上传。 + +配置 mode 为 `asyncIterator` 字符串。 + +```typescript +// src/config/config.default.ts +export default { + // ... + busboy: { + mode: 'asyncIterator', + }, +} +``` + +在代码中获取上传的文件。 + +```typescript +import { Controller, Post, Files, Fields } from '@midwayjs/core'; +import { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy'; + +@Controller('/') +export class HomeController { + + @Post('/upload', /*...*/) + async upload( + @Files() fileIterator: AsyncGenerator, + @Fields() fieldIterator: AsyncGenerator + ) { + // ... + } +} +``` + +在该模式下,`@Files` 和 `@File` 装饰器会提供同一个 `AsyncGenerator` ,而 `@Fields` 会也同样会提供一个 `AsyncGenerator`。 + +通过循环 `AsyncGenerator` ,可以针对每个上传文件的 `ReadStream` 做处理。 + +```typescript +import { Controller, Post, Files, Fields } from '@midwayjs/core'; +import { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy'; +import { tmpdir } from 'os'; +import { createWriteStream } from 'fs'; + +@Controller('/') +export class HomeController { + + @Post('/upload', /*...*/) + async upload( + @Files() fileIterator: AsyncGenerator, + @Fields() fieldIterator: AsyncGenerator + ) { + for await (const file of fileIterator) { + const { filename, data } = file; + const p = join(tmpdir, filename); + const stream = createWriteStream(p); + data.pipe(stream); + } + + for await (const { name, value } of fieldIterator) { + // ... + } + + // ... + } +} +``` + +注意,如果一次上传中任意一个文件抛出了错误,本次上传流会直接关闭,所有未传输完成的文件都会异常。 + +异步迭代器中的上传对象包含以下几个字段。 + +```typescript +export interface UploadStreamFieldInfo { + /** + * 上传的文件名 + */ + filename: string; + /** + * 上传文件 mime 类型 + */ + mimeType: string; + /** + * 上传文件的文件流 + */ + data: Readable; + /** + * 上传的表单字段名 + */ + fieldName: string; +} +``` + +异步迭代器中的 `@Fields` 的对象略有不同,返回的数据会包含 `name` 和 `value` 字段。 + +```typescript +export interface UploadStreamFieldInfo { + /** + * 表单名 + */ + name: string; + /** + * 表单值 + */ + value: any; +} +``` + + + + + + + +:::caution + +不再推荐使用。 + +::: + +配置 mode 为 `stream` 字符串。 + + +使用 stream 模式时,通过 `@Files` 中获取的 `data` 为 `ReadStream`,后续可以再通过 `pipe` 等方式继续将数据流转至其他 `WriteStream` 或 `TransformStream`。 + + +使用 stream 模式时,仅同时上传一个文件,即 `@Files` 数组中只有一个文件数据对象。 + +另外,stream 模式 `不会` 在服务器上产生临时文件,所以获取到上传的内容后无需手动清理临时文件缓存。 + +:::tip + +faas 场景实现方式视平台而定,如果平台不支持流式请求/响应但是业务开启了 `mode: 'stream'`,将采用先读取到内存,再模拟流式传输来降级处理。 + +::: + +在代码中获取上传的文件,流式模式下仅支持单个文件。 + +```typescript +import { Controller, Post, Files, Fields } from '@midwayjs/core'; +import { UploadStreamFileInfo } from '@midwayjs/busboy'; + +@Controller('/') +export class HomeController { + + @Post('/upload', /*...*/) + async upload(@Files() files: Array, @Fields() fields: Record + + + + + +### 上传文件后缀检查 + +通过 `whitelist` 属性,配置允许上传的文件后缀名,配置 `null` 则不校验后缀名。 + +:::caution + +如果配置为 `null`,则不对上传文件后缀名进行校验,如果采取文件上传模式 (mode=file),则会有可能被攻击者所利用,上传 `.php`、`.asp` 等后缀的 WebShell 实现攻击行为。 + +当然,由于组件会对上传后的临时文件采取 `重新随机生成` 文件名写入,只要开发者 `不将` 上传后的临时文件地址返回给用户,那么即使用户上传了一些不被预期的文件,那也无需过多担心会被利用。 + +::: + + +如果上传的文件后缀不匹配,会响应 `400` error,默认值如下: + +```ts +'.jpg', +'.jpeg', +'.png', +'.gif', +'.bmp', +'.wbmp', +'.webp', +'.tif', +'.psd', +'.svg', +'.js', +'.jsx', +'.json', +'.css', +'.less', +'.html', +'.htm', +'.xml', +'.pdf', +'.zip', +'.gz', +'.tgz', +'.gzip', +'.mp3', +'.mp4', +'.avi', +``` + +可以通过组件中导出的 `uploadWhiteList` 获取到默认的后缀名白名单。 + +另外,midway 上传组件,为了避免部分 `恶意用户`,通过某些技术手段来`伪造`一些可以被截断的扩展名,所以会对获取到的扩展名的二进制数据进行过滤,仅支持 `0x2e`(即英文点 `.`)、`0x30-0x39`(即数字 `0-9`)、`0x61-0x7a`(即小写字母 `a-z`) 范围内的字符作为扩展名,其他字符将会被自动忽略。 + +你可以传递一个函数,可以根据不同的条件动态返回白名单。 + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/busboy'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + busboy: { + whitelist: (ctx) => { + if (ctx.path === '/') { + return [ + '.jpg', + '.jpeg', + ]; + } else { + return [ + '.jpg', + ] + }; + }, + // ... + }, +} +``` + + + + + +### 上传文件 MIME 类型检查 + +部分`恶意用户`,会尝试将 `.php` 等 WebShell 修改扩展名为 `.jpg`,来绕过基于扩展名的白名单过滤规则,在某些服务器环境内,这个 jpg 文件依然会被作为 PHP 脚本来执行,造成安全风险。 + +组件提供了 `mimeTypeWhiteList` 配置参数 **【请注意,此参数无默认值设置,即默认不校验】**,您可以通过此配置设置允许的文件 MIME 格式,规则为由数组 `[扩展名, mime, [...moreMime]]` 组成的 `二级数组`,例如: + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/busboy'; +export default { + // ... + busboy: { + // ... + // 扩展名白名单 + whitelist: uploadWhiteList, + // 仅允许下面这些文件类型可以上传 + mimeTypeWhiteList: { + '.jpg': 'image/jpeg', + // 也可以设置多个 MIME type,比如下面的允许 .jpeg 后缀的文件是 jpg 或者是 png 两种类型 + '.jpeg': ['image/jpeg', 'image/png'], + // 其他类型 + '.gif': 'image/gif', + '.bmp': 'image/bmp', + '.wbmp': 'image/vnd.wap.wbmp', + '.webp': 'image/webp', + } + }, +} +``` + +您也可以使用组件提供的 `DefaultUploadFileMimeType` 变量,作为默认的 MIME 校验规则,它提供了常用的 `.jpg`、`.png`、`.psd` 等文件扩展名的 MIME 数据: + +```typescript +// src/config/config.default.ts +import { uploadWhiteList, DefaultUploadFileMimeType } from '@midwayjs/busboy'; +export default { + // ... + busboy: { + // ... + // 扩展名白名单 + whitelist: uploadWhiteList, + // 仅允许下面这些文件类型可以上传 + mimeTypeWhiteList: DefaultUploadFileMimeType, + }, +} +``` + +文件格式与对应的 MIME 映射,您可以通过 `https://mimetype.io/` 这个网站来查询,对于文件的 MIME 识别,我们使用的是 [file-type@16](https://www.npmjs.com/package/file-type) 这个 npm 包,请注意它支持的文件类型。 + +:::info + +MIME 类型校验规则仅适用于使用 文件上传模式 `mode=file`,同时设置此校验规则之后,由于需要读取文件内容进行匹配,所以会稍微影响上传性能。 + +但是,我们依然建议您在条件允许的情况下,设置 `mimeTypeWhiteList` 参数,这将提升您的应用程序安全性。 + +::: + +你可以传递一个函数,可以根据不同的条件动态返回 MIME 规则。 + +```typescript +// src/config/config.default.ts +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + busboy: { + mimeTypeWhiteList: (ctx) => { + if (ctx.path === '/') { + return { + '.jpg': 'image/jpeg', + }; + } else { + return { + '.jpeg': ['image/jpeg', 'image/png'], + } + }; + } + }, +} + +``` + + + +### Busboy 上传限制 + +默认情况下没有限制,可以通过配置修改,数字类型,单位为 byte。 + +```typescript +// src/config/config.default.ts +export default { + // ... + busboy: { + // ... + limits: { + fileSize: 1024 + } + }, +} +``` + +除此之外,还可以设置一些其他的 [限制](https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports)。 + + + + +### 临时文件与清理 + + +如果你使用了 `file` 模式来获取上传的文件,那么上传的文件会存放在您于 `config` 文件中设置的 `upload` 组件配置中的 `tmpdir` 选项指向的文件夹内。 + +你可以通过在配置中使用 `cleanTimeout` 来控制自动的临时文件清理时间,默认值为 `5 * 60 * 1000`,即上传的文件于 `5 分钟` 后自动清理,设置为 `0` 则视为不开启自动清理功能。 + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/busboy'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + busboy: { + mode: 'file', + tmpdir: join(tmpdir(), 'midway-busboy-files'), + cleanTimeout: 5 * 60 * 1000, + }, +} + +``` + +你也可以在代码中通过调用 `await ctx.cleanupRequestFiles()` 来主动清理当前请求上传的临时文件。 + + + +### 设置不同路由的配置 + +通过中间件的不同实例,可以对不同的路由做不同的配置,这种场景下会和全局配置合并,仅能覆盖一小部分配置。 + +```typescript +import { Controller, Post, Files, Fields } from '@midwayjs/core'; +import { UploadFileInfo, UploadMiddleware } from '@midwayjs/busboy'; + +@Controller('/') +export class HomeController { + @Post('/upload1', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'file'}) ]}) + async upload1(@Files() files Array) { + // ... + } + + @Post('/upload2', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'stream'}) ]}) + async upload2(@Files() files Array) { + // ... + } +} +``` + +当前可以传递的配置包括 `mode` 以及 `busboy` 自带的 [配置](https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports)。 + + + +## 内置错误 + +以下的错误在不同上传模式下均会自动触发。 + +* `MultipartInvalidFilenameError` 无效文件名 +* `MultipartInvalidFileTypeError` 无效文件类型 +* `MultipartFileSizeLimitError` 文件大小超出限制 +* `MultipartFileLimitError` 文件数量超出限制 +* `MultipartPartsLimitError` 上传 parts 数量超出限制 +* `MultipartFieldsLimitError` fields 数量超出限制 +* `MultipartError` 其余的 busbuy 错误 + + + +## 安全提示 + +1. 请注意是否开启 `扩展名白名单` (whiteList),如果扩展名白名单被设置为 `null`,则会有可能被攻击者所利用上传 `.php`、`.asp` 等WebShell。 +2. 请注意是否设置 `match` 或 `ignore` 规则,否则普通的 `POST/PUT` 等接口会有可能被攻击者利用,造成服务器负荷加重和空间大量占用问题。 +3. 请注意是否设置 `文件类型规则` (fileTypeWhiteList),否则可能会被攻击者伪造文件类型进行上传。 + + + +## 前端文件上传示例 + +### 1. html form 的形式 + +```html +
+ Name:
+ File:
+ +
+``` + +### 2. fetch FormData 方式 + +```js +const fileInput = document.querySelector('#your-file-input') ; +const formData = new FormData(); +formData.append('file', fileInput.files[0]); + +fetch('/api/upload', { + method: 'POST', + body: formData, +}); +``` + + + +## Postman 测试示例 + +![](https://img.alicdn.com/imgextra/i4/O1CN01iv9ESW1uIShNiRjBF_!!6000000006014-2-tps-2086-1746.png) + diff --git a/site/versioned_docs/version-3.0.0/extensions/cache.md b/site/versioned_docs/version-3.0.0/extensions/cache.md new file mode 100644 index 000000000000..87e7afed7d87 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/cache.md @@ -0,0 +1,249 @@ +# 缓存 + +Midway Cache 是为了方便开发者进行缓存操作的组件,它有利于改善项目的性能。它为我们提供了一个数据中心以便进行高效的数据访问。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + +## 安装 + +首先安装相关的组件模块。 + +```bash +$ npm i @midwayjs/cache@3 cache-manager --save +$ npm i @types/cache-manager --save-dev +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/cache": "^3.0.0", + "cache-manager": "^3.4.1", + // ... + }, + "devDependencies": { + "@types/cache-manager": "^3.4.0", + // ... + } +} +``` + + + +## 使用 Cache + +Midway 为不同的 cache 存储提供了统一的 API。默认内置了一个基于内存数据存储的数据中心。如果想要使用别的数据中心,开发者也可以切换到例如 mongodb、fs 等模式。 + + +首先,引入 Cache 组件,在 `configuration.ts` 中导入: + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as cache from '@midwayjs/cache'; +import { join } from 'path' + +@Configuration({ + imports: [ + // ... + cache // 导入 cache 组件 + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + +然后在业务代码中即可注入使用。 + +```typescript +import { Inject, Provide } from '@midwayjs/core'; +import { IUserOptions } from '../interface'; +import { CacheManager } from '@midwayjs/cache'; + +@Provide() +export class UserService { + + @Inject() + cacheManager: CacheManager; // 依赖注入 CacheManager +} +``` + +通过提供的 API 来设置,获取缓存数据。 + + +```typescript +import { Inject, Provide } from '@midwayjs/core'; +import { IUserOptions } from '../interface'; +import { CacheManager } from '@midwayjs/cache'; + +@Provide() +export class UserService { + + @Inject() + cacheManager: CacheManager; + + async getUser(options: IUserOptions) { + // 设置缓存内容 + await this.cacheManager.set(`name`, 'stone-jin'); + + // 获取缓存内容 + let result = await this.cacheManager.get(`name`); + + return result; + } + + async getUser2(){ + //获取缓存内容 + let result = await this.cacheManager.get(`name`); + return result; + } + + async reset(){ + await this.cacheManager.reset(); // 清空对应 store 的内容 + } +} +``` + + + +### 设置缓存 + + +我们通过 `await this.cache.set(key, value)` 方法进行设置,此处默认过期时间是10s。 + + +你也可以手动设置 TTL(过期时间),如下: +```typescript +await this.cacheManager.set(key, value, {ttl: 1000}); // ttl的单位为秒 +``` +如果你想要 Cache 不过期,则将 TTL 设置为 null 即可。 +```typescript +await this.cacheManager.set(key, value, {ttl: null}); +``` +同时你也可以通过全局的 `config.default.ts` 中进行设置。 +```typescript +export default { + // ... + cache: { + store: 'memory', + options: { + max: 100, + ttl: 10, // 修改默认的ttl配置 + }, + } +} +``` + + +### 获取缓存 + +```typescript +const value = await this.cacheManager.get(key); +``` +如果获取不到,则为 undefined。 + + + +### 移除缓存 + + +移除缓存,可以通过 del 方法。 +```typescript +await this.cacheManager.del(key); +``` + + + +### 清空整体store数据(此处是整体清除,需要重点⚠️) + + +比如用户设置了某个 redis 为 store,调用的话,包括非 cache 模块设置的也会清除。 +```typescript +await this.cacheManager.reset(); // 这块需要注意 +``` + + + +## 全局配置 + + +当我们引用了这个 cache 组件后,我们能对其进行全局的配置。配置方法跟别的组件类似。 + + +默认的配置: +```typescript +export default { + // ... + cache: { + store: 'memory', + options: { + max: 100, + ttl: 10, + }, + } +} +``` +例如用户可以修改默认的 TTL,也就是过期时间。 + + + +## 其他Cache + + +用户也可以修改 store 方式,在 `config.default.ts` 中进行组件的配置: +```typescript +import * as redisStore from 'cache-manager-ioredis'; + +export default { + // ... + cache: { + store: redisStore, + options: { + host: 'localhost', // default value + port: 6379, // default value + password: '', + db: 0, + keyPrefix: 'cache:', + ttl: 100 + }, + } +} +``` +或者修改为 mongodb 的 cache。 + + +:::danger +**再次注意⚠️:使用 redis 作为 cache 的时候,代码里面慎用 reset 方法,因为会把整个 redis 给 flushdb,简称数据清空。** +::: + + + +## 相关文档 + + +由于 Midway Cache 是基于 cache-manager 封装,所以相关资料用户也可以查询:[cache-manger](https://www.npmjs.com/package/cache-manager)。 + + + +## 常见问题 + + + +### 1、set 和 get 无法得到相同值? + +用户使用了 cache 模块,默认是内存式的,例如在本地用 dev 模式,由于是单进程的,那 set 和 ge t最终能达到相同的值。但是用户部署到服务器上面后,由于会有多 worker,相当于第一次请求,落在进程1上,然后第二次落在进程2上,这样获得到空了。 + + +解决办法:参考 其他 Cache 的章节,配置 store 为分布式,例如 redis 的 store 等方式。 diff --git a/site/versioned_docs/version-3.0.0/extensions/caching.md b/site/versioned_docs/version-3.0.0/extensions/caching.md new file mode 100644 index 000000000000..8eecc313474a --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/caching.md @@ -0,0 +1,543 @@ +# 缓存 + +缓存是一个伟大而简单的技术,有助于提高你的应用程序的性能。本组件提供了缓存相关的能力,你可以将数据缓存到不同的数据源,也可以针对不同场景建立多级缓存,提高数据访问速度。 + +:::tip + +Midway 提供基于 [cache-manager v5](https://github.com/node-cache-manager/node-cache-manager) 模块重新封装了缓存组件,原有的缓存模块基于 v3 开发不再迭代,如需查看老文档,请访问 [这里](/docs/extensions/cache)。 + +::: + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 安装 + +首先安装相关的组件模块。 + +```bash +$ npm i @midwayjs/cache-manager@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/cache-manager": "^3.0.0", + // ... + }, +} +``` + + + +## 启用组件 + + +首先,引入组件,在 `configuration.ts` 中导入: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as cacheManager from '@midwayjs/cache-manager'; +import { join } from 'path' + +@Configuration({ + imports: [ + // ... + cacheManager, + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + + +## 使用缓存 + + + +### 1、配置缓存 + +在使用前,你需要配置缓存所在的位置,比如内置的内存缓存,或者是引入 Redis 缓存,每个缓存对应了一个缓存的 Store。 + +下面的示例代码,配置了一个名为 `default` 的内存缓存。 + +```typescript +// src/config/config.default.ts +export default { + cacheManager: { + clients: { + default: { + store: 'memory', + }, + }, + } +} +``` + +最常用的场景下,缓存会包含两个参数,配置 `max` 修改缓存的数量,配置 `ttl` 修改缓存的过期时间,单位毫秒。 + +```typescript + // src/config/config.default.ts +export default { + cacheManager: { + clients: { + default: { + store: 'memory', + options: { + max: 100, + ttl: 10, + }, + }, + }, + } +} +``` + +:::tip + +* `ttl` 的单位是毫秒 +* `max` 代表缓存 key 的最大个数 +* 不同的 Store 淘汰 key 的算法不同,内存缓存使用的淘汰算法是 LRU + +::: + +### 2、使用缓存 + +可以通过服务工厂的装饰器获取到实例,可以通过简单的 `get` 和 `set` 方法获取和保存缓存。 + +```typescript +import { InjectClient, Provide } from '@midwayjs/core'; +import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + + @InjectClient(CachingFactory, 'default') + cache: MidwayCache; + + async invoke(name: string, value: string) { + // 设置缓存 + await this.cache.set(name, value); + // 获取缓存 + const data = await this.cache.get(name); + // ... + } +} + +``` + +动态设置 `ttl` 过期时间。 + +```typescript +await this.cache.set('key', 'value', 1000); +``` + +若要禁用缓存过期,可以将 `ttl` 配置属性设置为 0。 + +```typescript +await this.cache.set('key', 'value', 0); +``` + +删除单个缓存。 + +```typescript +await this.cache.del('key'); +``` + +清理整个缓存,可以使用 `reset` 方法。 + +```typescript +await this.cacheManager.reset(); +``` + +:::danger + +注意,清理整个缓存非常危险,如果使用了 Redis 作为缓存 store,将清空整个 Redis 数据。 + +::: + +除了装饰器之外,也可以通过 API 获取缓存实例。 + +```typescript +import { InjectClient, Provide } from '@midwayjs/core'; +import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + + @Inject() + cachingFactory: CachingFactory; + + async invoke() { + const caching = await this.cachingFactory.get('default'); + // ... + } +} +``` + + + +### 3、配置多个缓存 + +和其他组件一样,组件支持配置多个缓存实例。 + +```typescript +// src/config/config.default.ts +export default { + cacheManager: { + clients: { + default: { + store: 'memory', + }, + otherCaching: { + store: 'memory', + } + }, + } +} +``` + +可以注入不同的缓存实例。 + +```typescript +import { InjectClient, Provide } from '@midwayjs/core'; +import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + + @InjectClient(CachingFactory, 'default') + cache: MidwayCache; + + @InjectClient(CachingFactory, 'otherCaching') + customCaching: MidwayCache; + +} + +``` + + + +### 4、配置不同 Store + +组件基于 [cache-manager](https://github.com/node-cache-manager/node-cache-manager) 可以配置不同的缓存 Store,比如最常见的可以配置 Redis Store。 + +假如项目已经配置了一个 `Redis`,通过组件内置的 `createRedisStore` 方法,可以快速创建一个 Redis Store。 + + +```typescript +import { createRedisStore } from '@midwayjs/cache-manager'; + +// src/config/config.default.ts +export default { + cacheManager: { + clients: { + default: { + store: createRedisStore('default'), + options: { + ttl: 10, + } + }, + }, + }, + redis: { + clients: { + default: { + port: 6379, + host: '127.0.0.1', + } + } + } +} +``` + +`createRedisStore` 方法可以传递一个已经配置的 redis 实例名,可以和 redis 组件复用实例。 + + + +### 5、配置三方 Store + +除了 Redis 之外,用户也可以自行选择 Cache-Manager 的 Store,列表可以参考 [这里](https://github.com/node-cache-manager/node-cache-manager?tab=readme-ov-file#store-engines)。 + +下面是一个配置 [node-cache-manager-ioredis-yet](https://github.com/node-cache-manager/node-cache-manager-ioredis-yet) 的例子。 + +```typescript +// src/config/config.default.ts +import { redisStore } from 'cache-manager-ioredis-yet'; + +export default { + cacheManager: { + clients: { + default: { + store: redisStore, + options: { + port: 6379, + host: 'localhost', + ttl: 10, + }, + }, + }, + } +} +``` + + + +### 6、多级缓存 + +[cache-manager](https://github.com/node-cache-manager/node-cache-manager) 支持将多个缓存 Store 聚合到一起,实现多级缓存。 + +比如我可以创建一个多级缓存将多个缓存 Store 合并到一起。 + +```typescript +// src/config/config.default.ts +import { createRedisStore } from '@midwayjs/cache-manager'; +export default { + cacheManager: { + clients: { + memoryCaching: { + store: 'memory', + }, + redisCaching: { + store: createRedisStore('default'), + options: { + ttl: 10, + }, + }, + multiCaching: { + store: ['memoryCaching', 'redisCaching'], + options: { + ttl: 100, + }, + }, + }, + }, + redis: { + clients: { + default: { + port: 6379, + host: '127.0.0.1', + }, + }, + }, +}; + +``` + +这样 `multiCaching` 这个缓存实例就包含了两级缓存,缓存的优先级从上到下,在查找时,会先查找 `memoryCaching` ,如果内存缓存不存在 key,则继续查找 `redisCaching`。 + + + +### 7、使用多级缓存 + +和普通缓存类似,多级缓存除了 `set`、`get`、`del`方法外,还增加了 `mset` 、`mget`、`mdel` 方法。 + +```typescript +import { InjectClient, Provide } from '@midwayjs/core'; +import { CachingFactory, MidwayMultiCache } from '@midwayjs/cache-manager'; + +const userId2 = 456; +const key2 = 'user_' + userId; +const ttl = 5; + +@Provide() +export class UserService { + + @InjectClient(CachingFactory, 'multiCaching') + multiCache: MidwayMultiCache; + + async invoke() { + // 设置到所有级别的缓存 + await this.multiCache.set('foo2', 'bar2', ttl); + + // 从最高优先级的缓存 Store 中获取 key + console.log(await this.multiCache.get('foo2')); + // >> "bar2" + + // 调用每一个 Store 的 del 方法进行删除 + await this.multiCache.del('foo2'); + + // 在所有缓存中设置多个 key,可以多个键值对 + await this.multiCache.mset( + [ + ['foo', 'bar'], + ['foo2', 'bar2'], + ], + ttl + ); + + // mget() 从最高优先级的缓存中获取值 + // 如果第一个缓存 Store 中不包含所有的 key, + // 继续在下一个缓存 Store 中查找没有找到的 key。 + // 这是递归地完成的,直到: + // - 所有的 key 都已经查找到值 + // - 所有的缓存 Store 都被查找过 + console.log(await this.multiCache.mget('key', 'key2')); + // >> ['bar', 'bar2'] + + // 调用每一个 Store 的 mdel 方法进行删除 + await this.multiCache.mdel('foo', 'foo2'); + } +} + +``` + +### 8、自动刷新 + +不管是普通缓存还是多级缓存,都支持后台刷新功能,只需要配置 `refreshThreshold` 的时间,单位为毫秒。 + +```typescript +// src/config/config.default.ts +export default { + cacheManager: { + clients: { + default: { + store: 'memory', + options: { + refreshThreshold: 3 * 1000, + }, + }, + }, + } +} +``` + +如果设置了 `refreshthreshold`,每次从缓存获取值之后,会检查 `ttl` 的值,如果剩余的 `ttl` 小于 `refreshthreshold` ,则系统将异步更新缓存,同时系统会返回旧值,直到 `ttl` 过期。 + +:::tip + +* 在多级缓存的情况下,根据优先级顺序找到第一个包含 key 的 Store。 + +* 如果阈值较低且执行的函数比较慢,key 可能会过期,有可能会遇到并发更新的情况。 + +* 后台刷新机制目前只支持单个 key。 + +* 如果没有为 key 设置 `ttl`,则不会触发刷新机制。对于 redis,`ttl` 默认设置为-1。 + +::: + + + +## 自动缓存 + +### 使用装饰器缓存方法 + +可以通过 `@Caching` 装饰器缓存方法的结果,比如缓存 http 响应或者服务调用的结果。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { Caching } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + @Caching('default') + async getUser(name: string) { + return name; + } +} + +``` + +当第一次调用 `getUser` 方法时,会正常执行逻辑,返回结果,装饰器会将结果缓存起来,第二次执行时,如果缓存未失效,则会从缓存中直接返回。 + +### 指定缓存的 ttl + +也可以单独设置 `ttl` 。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { Caching } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + @Caching('default', 100) + async getUser(name: string) { + return name; + } +} +``` + +### 手动指定缓存 key + +如果对自动生成的 key 不满意,可以手动指定缓存的 key。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { Caching } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + @Caching('default', 'customKey', 100) + async getUser(name: string) { + return name; + } +} +``` + +### 带逻辑的缓存 + +如果你希望根据一些特定逻辑进行缓存,比如特定参数,或者特定的 Header,可以传递一个工具函数进行逻辑判断。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { Caching } from '@midwayjs/cache-manager'; + +function cacheBy({methodArgs, ctx, target}) { + if (methodArgs[0] === 'harry' || methodArgs[0] === 'mike') { + return 'cache1'; + } +} + +@Provide() +export class UserService { + @Caching('default', cacheBy, 100) + async getUser(name: string) { + return 'hello ' + name; + } +} +``` + +上面的示例中,`cacheBy` 方法自定义了缓存的逻辑,当方法入参值为 `harry` 或者 `mike` 时,将返回缓存的 `key` ,而其他参数时则跳过缓存。 + +这个时候执行的结果为: + +```typescript +await userService.getUser('harry')); // hello harry +await userService.getUser('mike')); // hello harry +await userService.getUser('lucy')); // hello lucy +``` + +`@Caching` 装饰器可以在第二个参数中传递一个方法,这个方法的入参 options 为: + +* `methodArgs` 当前调用方法的实际参数 +* `ctx` 如果是请求作用域,则是当前调用的上下文对象,如果是单例,则该对象为空对象 +* `target` 当前调用的实例 + +方法的返回值为字符串或者布尔值,当返回字符串时,表示以该 key 将方法的结果缓存,当返回 `undefined` 或者 `null` 时,表示跳过缓存。 + +通过这些参数判断,我们可以实现非常灵活的自定义缓存逻辑。 + + + +## 常见问题 + + + +### 1、多进程下内存缓存 set 和 get 无法得到相同值 + +这是正常现象,每个进程的数据是独立的,仅保存在当前进程中。如需跨进程缓存,请使用 Redis 这类分布式缓存系统。 diff --git a/site/versioned_docs/version-3.0.0/extensions/captcha.md b/site/versioned_docs/version-3.0.0/extensions/captcha.md new file mode 100644 index 000000000000..a8c26874be18 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/captcha.md @@ -0,0 +1,287 @@ +# 验证码 + +适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用验证码组件,支持 `图片验证码`、`计算表达式` 等类型验证码。 + +您也可以通过此组件,来实现 `短信验证码`、`邮件验证码` 等验证能力,但是注意,本组件本身不含发送短信、邮件功能。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + +## 安装依赖 + +```bash +$ npm i @midwayjs/captcha@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/captcha": "^3.0.0", + // ... + }, +} +``` + +## 启用组件 + +在 `src/configuration.ts` 中引入组件。 + +```typescript +import * as captcha from '@midwayjs/captcha'; + +@Configuration({ + imports: [ + // ...other components + captcha + ], +}) +export class MainConfiguration {} +``` + +## 调用服务 + +```typescript +import { Controller, Inject } from '@midwayjs/core'; +import { CaptchaService } from '@midwayjs/captcha'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx; + + @Inject() + captchaService: CaptchaService; + + // 示例:获取图像验证码 + @Get('/get-image-captcha') + async getImageCaptcha() { + const { id, imageBase64 } = await this.captchaService.image({ width: 120, height: 40 }); + return { + id, // 验证码 id + imageBase64, // 验证码 SVG 图片的 base64 数据,可以直接放入前端的 img 标签内 + } + } + + // 示例:获取计算表达式验证码 + @Get('/get-formula-captcha') + async getFormulaCaptcha() { + const { id, imageBase64 } = await this.captchaService.formula({ noise: 1 }); + return { + id, // 验证码 id + imageBase64, // 验证码 SVG 图片的 base64 数据,可以直接放入前端的 img 标签内 + } + } + + // 验证验证码是否正确 + @Post('/check-captcha') + async getCaptcha() { + const { id, answer } = this.ctx.request.body; + const passed: boolean = await this.captchaService.check(id, answer); + if (passed) { + return 'passed'; + } + return 'error'; + } + + // 示例:短信验证码 + @Post('/sms-code') + async sendSMSCode() { + // 验证验证码是否正确 + const { id, text: code } = await this.captchaService.text({ size: 4 }); + await sendSMS(18888888888, code); + return { id } + } + + // 示例:邮件验证码 + @Post('/email-code') + async sendEmailCode() { + // 验证验证码是否正确 + const { id, text: code } = await this.captchaService.text({ type: 'number'}); + await sendEmail('admin@example.com', code); + return { id } + } + + // 示例:将任意文本内容塞入验证码中 + @Get('/test-text') + async testText() { + // 存入内容,获取验证码id + const id: string = await this.captchaService.set('123abc'); + // 根据验证码id,校验内容是否正确 + const passed: boolean = await this.captchaService.check(id, '123abc'); + return { + passed: passed === true, + } + } +} +``` + +## 可用配置 + +```typescript +interface CaptchaOptions { + default?: { // 默认配置 + // 验证码字符长度,默认 4 个字符 + size?: number; + // 干扰线条的数量,默认 1 条 + noise?: number; + // 宽度,默认为 120 像素 + width?: number; + // 宽度,默认为 40 像素 + height?: number; + // 图形验证码配置,图形中包含一些字符 + }, + image?: { + // 验证码字符长度,默认 4 个字符 + size?: number; + // 图像验证码中的字符类型,默认为 'mixed' + // - 'mixed' 表示 0-9、A-Z 和 a-z + // - 'letter' 表示 A-Z 和 a-z + // - 'number' 表示 0-9 + type?: 'mixed', + // 干扰线条的数量,默认 1 条 + noise?: number; + // 宽度,默认为 120 像素 + width?: number; + // 宽度,默认为 40 像素 + height?: number; + }, + // 计算公式验证码配置,例如返回的图像内容为 1+2,需要用户填入 3 + formula?: { + // 干扰线条的数量,默认 1 条 + noise?: number; + // 宽度,默认为 120 像素 + width?: number; + // 宽度,默认为 40 像素 + height?: number; + }, + // 纯文本验证码配置,基于纯文本验证码可以实现短信验证码、邮件验证码 + text?: { + // 验证码字符长度,默认 4 个字符 + size?: number; + // 文本验证码中的字符类型,默认为 'mixed' + // - 'mixed' 表示 0-9、A-Z 和 a-z + // - 'letter' 表示 A-Z 和 a-z + // - 'number' 表示 0-9 + type?: 'mixed', + }, + // 验证码过期时间,默认为 1h + expirationTime?: 3600, + // 验证码存储的 key 前缀 + idPrefix: 'midway:vc', +} + +export const captcha: CaptchaOptions = { + default: { // 默认配置 + size: 4, + noise: 1, + width: 120, + height: 40, + }, + image: { // 最终会合并 default 配置 + type: 'mixed', + }, + formula: {}, // 最终会合并 default 配置 + text: {}, // 最终会合并 default 配置 + expirationTime: 3600, + idPrefix: 'midway:vc', +} +``` +### 配置示例一 + +获取一个 包含 `5个纯英文字母` 的图像验证码,图像宽度 `200` 像素,高度 `50` 像素,并且包含 `3` 条干扰线。 + +因为图像验证码的配置 `image`, 会与 `default` 配置进行合并,所以可以只修改 `default` 配置: + +```typescript +export const captcha: CaptchaOptions = { + default: { + size: 5, + noise: 3, + width: 200, + height: 50 + }, + image: { + type: 'letter' + } +} +``` +当然,也可以 `不` 修改 `default` 配置,将宽度、高度等在 `image` 配置项中进行配置,取得 `同样`的效果: +```typescript +export const captcha: CaptchaOptions = { + image: { + size: 5, + noise: 3, + width: 200, + height: 50 + type: 'letter' + } +} +``` + +### 配置示例二 + +获取一个图像宽度 `100` 像素,高度 `60` 像素,并且包含 `2` 条干扰线的计算表达式验证码。 + +因为计算表达式验证码的配置 `formula` ,会与 `default` 配置合并,所以可以只修改 `default` 配置: + +```typescript +export const captcha: CaptchaOptions = { + default: { + noise: 2, + width: 100, + height: 60 + }, +} +``` +当然,也可以 `不` 修改 `default` 配置,将宽度、高度等在 `formula` 配置项中进行配置,取得 `同样`的效果: +```typescript +export const captcha: CaptchaOptions = { + formula: { + noise: 2, + width: 100, + height: 60 + } +} +``` + + +## 组件依赖 + +验证码的内容存储基于 `@midwayjs/cache-manager` 组件,默认创建了一个名为 `captcha` 的缓存实例,将数据存储在 `memory` 中。 + +```typescript +export default { + cacheManager: { + clients: { + captcha: { + store: 'memory', + }, + }, + }, +}; +``` + +如果要替换为 `redis` 或其他服务,请参照 `@midwayjs/cache-manager` 的 [文档](/docs/extensions/caching),对 cache 进行配置。 + + + + +## 效果 + +**图片验证码** + +![图片验证码](https://gw.alicdn.com/imgextra/i4/O1CN014cEzLH23vEniOgoyp_!!6000000007317-2-tps-120-40.png) + +**计算表达式** + + ![计算表达式](https://gw.alicdn.com/imgextra/i4/O1CN01u3Mj0q24lRx1md9pX_!!6000000007431-2-tps-120-40.png) diff --git a/site/versioned_docs/version-3.0.0/extensions/casbin.md b/site/versioned_docs/version-3.0.0/extensions/casbin.md new file mode 100644 index 000000000000..a5b61fdb8921 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/casbin.md @@ -0,0 +1,545 @@ +# 角色鉴权 + +Casbin 是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型。 + +官网文档:https://casbin.org/ + + + +## Casbin 是什么 + +Casbin 可以: + +1. 支持自定义请求的格式,默认的请求格式为`{subject, object, action}`。 +2. 具有访问控制模型model和策略policy两个核心概念。 +3. 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。 +4. 支持内置的超级用户 例如:`root` 或 `administrator`。超级用户可以执行任何操作而无需显式的权限声明。 +5. 支持多种内置的操作符,如 `keyMatch`,方便对路径式的资源进行管理,如 `/foo/bar` 可以映射到 `/foo*` + +Casbin 不能: + +1. 身份认证 authentication(即验证用户的用户名和密码),Casbin 只负责访问控制。应该有其他专门的组件负责身份认证,然后由 Casbin 进行访问控制,二者是相互配合的关系。 +2. 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户通常有他们的密码,但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。 + +:::tip + +注意: + +- 1、在 Midway v3.6.0 之后可用 +- 2、Midway 只是封装了 Casbin 的 API 并提供简单的支持,策略规则编写请查看 [官方文档](https://casbin.org/) +- 3、Casbin 不提供登录,只提供现有用户的鉴权,需要搭配 passport 等获取用户信息的组件来使用 + +::: + + + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/casbin@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/casbin": "^3.0.0", + // ... + }, +} +``` + + + +## 启用组件 + + +首先,引入组件,在 `configuration.ts` 中导入: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as casbin from '@midwayjs/casbin'; +import { join } from 'path' + +@Configuration({ + imports: [ + // ... + casbin, + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + + +## 准备模型和策略 + +使用 Casbin 前需要定义模型和策略,这两个文件的内容贯穿本文,建议先去官网了解相关内容。 + +我们以一个基础的模型为例,比如: + +``` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ +g2 = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act || r.sub == "root" +``` + +将其保存在项目根目录的 `basic_model.conf` 文件中。 + +以及包含下面内容的策略文件。 + +``` +p, superuser, user, read:any +p, manager, user_roles, read:any +p, guest, user, read:own + +g, alice, superuser +g, bob, guest +g, tom, manager + +g2, users_list, user +g2, user_roles, user +g2, user_permissions, user +g2, roles_list, role +g2, role_permissions, role +``` + +将其保存在项目根目录的 `basic_policy.csv` 文件中。 + + + +## 配置模型和策略 + +这里我们的策略将以文件形式进行演示。 + +配置如下: + +```typescript +import { MidwayAppInfo } from '@midwayjs/core'; +import { join } from 'path'; + +export default (appInfo: MidwayAppInfo) => { + return { + // ... + casbin: { + modelPath: join(appInfo.appDir, 'basic_model.conf'), + policyAdapter: join(appInfo.appDir, 'basic_policy.csv'), + } + }; +} + +``` + + + +## 装饰器鉴权 + +有多种形式来使用 Casbin,这里以装饰器作为示例。 + +### 定义资源 + +首先定义资源,比如放在 `src/resource.ts` 文件中,对应策略文件中对应的资源。 + +```typescript +export enum Resource { + USERS_LIST = 'users_list', + USER_ROLES = 'user_roles', + USER_PERMISSIONS = 'user_permissions', + ROLES_LIST = 'roles_list', + ROLE_PERMISSIONS = 'role_permission', +} +``` + + + +### 配置获取用户的方式 + +在使用装饰器鉴权时,我们需要配置一个获取用户的方式,比如在 passport 组件之后,我们会从 `ctx.user` 上获取用户名。 + +```typescript +import { MidwayAppInfo } from '@midwayjs/core'; +import { join } from 'path'; + +export default (appInfo: MidwayAppInfo) => { + return { + // ... + casbin: { + modelPath: join(appInfo.appDir, 'basic_model.conf'), + policyAdapter: join(appInfo.appDir, 'basic_policy.csv'), + usernameFromContext: (ctx) => { + return ctx.user; + } + } + }; +} + +``` + + + +### 增加守卫 + +装饰器鉴权依赖守卫,我们可以在全局或者某些路由上开启,全局守卫使用请参考守卫章节。 + +比如,我们只在下面的 `findAllUsers` 方法上开启鉴权,`AuthGuard` 是 `@midwayjs/casbin` 提供的守卫,可以直接使用。 + +```typescript +import { Controller, Get, UseGuard } from '@midwayjs/core'; +import { AuthGuard } from '@midwayjs/casbin'; +import { Resource } from './resouce'; + +@Controller('/') +export class HomeController { + + @UseGuard(AuthGuard) + @Get('/users') + async findAllUsers() { + // ... + } +} +``` + + + +### 定义权限 + +使用 `UsePermission` 装饰器定义路由需要的权限。 + +```typescript +import { Controller, Get, UseGuard } from '@midwayjs/core'; +import { AuthActionVerb, AuthGuard, AuthPossession, UsePermission } from '@midwayjs/casbin'; +import { Resource } from './resouce'; + +@Controller('/') +export class HomeController { + + @UseGuard(AuthGuard) + @UsePermission({ + action: AuthActionVerb.READ, + resource: Resource.USER_ROLES, + possession: AuthPossession.ANY + }) + @Get('/users') + async findAllUsers() { + // ... + } +} +``` + +没有权限读取 `USER_ROLES` 的用户不能调用 findAllUsers 方法,在请求时会返回 403 状态码。 + +比如,上面的 `bob` 用户访问则会返回 403, 而 `tom` 用户访问则正常返回。 + + + +`UsePermission` 需要提供一个对象参数,包括 `action`、`resource`、`possession` 和一个可选的 `isOwn` 的对象。 + +- `action` 是一个 `AuthActionVerb` 枚举,包含读,写等操作 +- `resource` 资源字符串 +- `possession` 是一个 `AuthPossession` 枚举 +- `isOwn` 是一个接受`Context`(守卫 `canActivate`的参数)作为唯一参数并返回布尔值的函数。 `AuthZGuard` 使用它来确定用户是否是资源的所有者。 如果未定义,将使用返回 `false` 的默认函数。 + +可以同时定义多个权限,但只有当所有权限都满足时,才能访问该路由。 + +比如: + +```typescript +@UsePermissions({ + action: AuthActionVerb.READ, + resource: 'USER_ADDRESS', + possession: AuthPossession.ANY +}, { + action; AuthActionVerb.READ, + resource: 'USER_ROLES, + possession: AuthPossession.ANY +}) +``` + +只有当用户被授予读取 `USER_ADDRESS` 和 `USER_ROLES` 这两个权限时,才能访问该路由。 + + + +## API 鉴权 + +Casbin 本身提供了一些通用的 API 和权限相关的功能。 + +我们可以通过直接注入 `CasbinEnforcerService` 服务来使用。 + +比如,我们可以在守卫或者中间件中编码。 + +```typescript +import { CasbinEnforcerService } from '@midwayjs/casbin'; +import { Guard, IGuard } from '@midwayjs/core'; + +@Guard() +export class UserGuard extends IGuard { + + @Inject() + casbinEnforcerService: CasbinEnforcerService; + + async canActivate(ctx, clz, methodName) { + // 用户登录了,并且是特定的方法,则检查权限 + if (ctx.user && methodName === 'findAllUsers') { + return await this.casbinEnforcerService.enforce(ctx.user, 'USER_ROLES', 'read'); + } + // 未登录用户不允许访问 + return false; + } +} +``` + +在启用守卫后,效果和上面的装饰器相同。 + +此外,`CasbinEnforcerService` 还有更多的 API,比如重新加载策略。 + +```typescript +await this.casbinEnforcerService.loadPolicy(); +``` + + + +## 分布式策略存储 + +在多台机器部署的场景下,需要将策略存储到外部。 + +当前已经实现的适配器有: + +- Redis +- Typeorm + + + +### Redis Adapter + +需要依赖 `@midwayjs/casbin-redis-adapter` 包和 redis 组件。 + +```bash +$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save +``` + +启用 redis 组件。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as redis from '@midwayjs/redis'; +import * as casbin from '@midwayjs/casbin'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + redis, + casbin, + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + +配置 redis 连接和 casbin 适配器。 + +```typescript +import { MidwayAppInfo } from '@midwayjs/core'; +import { join } from 'path'; +import { createAdapter } from '@midwayjs/casbin-redis-adapter'; + +export default (appInfo: MidwayAppInfo) => { + return { + // ... + redis: { + clients: { + // 为 casbin 定义了一个连接 + 'node-casbin-official': { + host: '127.0.0.1', + port: 6379, + password: '', + db: '0', + } + } + }, + casbin: { + policyAdapter: createAdapter({ + // 配置了上面的连接名 + clientName: 'node-casbin-official' + }), + // ... + }, + }; +} + +``` + + + +### TypeORM Adapter + +需要依赖 `@midwayjs/casbin-typeorm-adapter` 包和 typeorm 组件。 + +``` +$ npm i @midwayjs/casbin-typeorm-adapter @midwayjs/typeorm --save +``` + +启用 typeorm 组件。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as typeorm from '@midwayjs/typeorm'; +import * as casbin from '@midwayjs/casbin'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + typeorm, + casbin, + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + +配置适配器,下面以 sqlite 存储为例,mysql 的配置可以查看 typeorm 组件。 + +```typescript +import { MidwayAppInfo } from '@midwayjs/core'; +import { join } from 'path'; +import { CasbinRule, createAdapter } from '@midwayjs/casbin-typeorm-adapter'; + +export default (appInfo: MidwayAppInfo) => { + return { + // ... + typeorm: { + dataSource: { + // 为 casbin 定义了一个连接 + 'node-casbin-official': { + type: 'sqlite', + synchronize: true, + database: join(appInfo.appDir, 'casbin.sqlite'), + // 注意这里显式引入了 Entity + entities: [CasbinRule], + } + } + }, + casbin: { + policyAdapter: createAdapter({ + // 配置了上面的连接名 + dataSourceName: 'node-casbin-official' + }), + // ... + } + }; +} +``` + + + +## 监视器 + +使用分布式消息系统,例如 [etcd](https://github.com/coreos/etcd) 来保持多个Casbin执行器实例之间的一致性。 因此,我们的用户可以同时使用多个Casbin 执行器来处理大量的权限检查请求。 + +Midway 当前只提供一种 Redis 更新策略,如有其他需求,可以给我们提交 issue。 + +### Redis Watcher + +需要依赖 `@midwayjs/casbin-redis-adapter` 包和 redis 组件。 + +```bash +$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save +``` + +启用 redis 组件。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as redis from '@midwayjs/redis'; +import * as casbin from '@midwayjs/casbin'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + redis, + casbin, + ], + // ... +}) +export class MainConfiguration { +} +``` + +使用示例: + +```typescript +import { MidwayAppInfo } from '@midwayjs/core'; +import { join } from 'path'; +import { createAdapter, createWatcher } from '@midwayjs/casbin-redis-adapter'; + +export default (appInfo: MidwayAppInfo) => { + return { + // ... + redis: { + clients: { + 'node-casbin-official': { + host: '127.0.0.1', + port: 6379, + db: '0', + }, + 'node-casbin-sub': { + host: '127.0.0.1', + port: 6379, + db: '0', + } + } + }, + casbin: { + // ... + policyAdapter: createAdapter({ + clientName: 'node-casbin-official' + }), + policyWatcher: createWatcher({ + pubClientName: 'node-casbin-official', + subClientName: 'node-casbin-sub', + }) + }, + }; +} +``` + +注意,pub/sub 连接需要不同的客户端,上面代码定义了两个客户端。 + +pub 客户端可以和普通 Redis 客户端连接复用,而 sub 需要一个独立的客户端。 diff --git a/site/versioned_docs/version-3.0.0/extensions/cfork.md b/site/versioned_docs/version-3.0.0/extensions/cfork.md new file mode 100644 index 000000000000..5a7f11fbc0e5 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/cfork.md @@ -0,0 +1,62 @@ +# cfork + +很多同学没有听过 cfork,cfork 库是 egg-scripts 中用于启动主进程的库,是 egg 使用的基础库之一,他的功能是启动进程,并维持多个进程的保活。 + + +文档在此:[https://github.com/node-modules/cfork](https://github.com/node-modules/cfork) + + +由于 bootstrap.js 的特性,有时候不是很适合 pm2 来部署(比如集团内部,全局不安装,需要 API 启动)。 + + +我们可以新增一个 `server.js` 用来做主进程的入口,将 `bootstrap.js` 作为每个子进程的启动入口。 + + +```javascript +// server.js + +'use strict'; + +const cfork = require('cfork'); +const util = require('util'); +const path = require('path'); +const os = require('os'); + +// 获取 cpu 核数 +const cpuNumbers = os.cpus().length; + +cfork({ + exec: path.join(__dirname, './bootstrap.js'), + count: cpuNumbers, +}) + .on('fork', (worker) => { + console.warn('[%s] [worker:%d] new worker start', Date(), worker.process.pid); + }) + .on('disconnect', (worker) => { + console.warn( + '[%s] [master:%s] wroker:%s disconnect, exitedAfterDisconnect: %s, state: %s.', + Date(), + process.pid, + worker.process.pid, + worker.exitedAfterDisconnect, + worker.state + ); + }) + .on('exit', (worker, code, signal) => { + const exitCode = worker.process.exitCode; + const err = new Error( + util.format( + 'worker %s died (code: %s, signal: %s, exitedAfterDisconnect: %s, state: %s)', + worker.process.pid, + exitCode, + signal, + worker.exitedAfterDisconnect, + worker.state + ) + ); + err.name = 'WorkerDiedError'; + console.error('[%s] [master:%s] wroker exit: %s', Date(), process.pid, err.stack); + }); +``` + +最后启动 `node server.js` 即可。 diff --git a/site/versioned_docs/version-3.0.0/extensions/code_dye.md b/site/versioned_docs/version-3.0.0/extensions/code_dye.md new file mode 100644 index 000000000000..1082e841f6ac --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/code_dye.md @@ -0,0 +1,132 @@ +# 代码染色 + +适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的代码染色组件。 + +用于在 HTTP 场景展示调用链路耗时与各个方法的出入参,帮你更快地定位代码问题。 + +比如: + ++ 代码执行缓慢 + - 不知道是哪一个方法执行的慢:通过代码染色后,可以查看每一个 `方法的执行时长`。 ++ 代码执行错误 + - 可能是方法没有调到:通过代码染色后,可以查看每一个 `方法的调用链`。 + - 可能是方法调用参数出错:通过代码染色后,查看每一个`方法的入参和返回值` + +使用效果: + +![](https://gw.alicdn.com/imgextra/i1/O1CN017Zd6y628M2PvqJO7I_!!6000000007917-2-tps-2392-844.png) + + + + +相关信息: + +| web 支持情况 | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/code-dye@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/code-dye": "^3.0.0" + // ... + }, +} +``` + + + +## 启用组件 + +将 code-dye 组件配置到代码中。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as codeDye from '@midwayjs/code-dye'; + +@Configuration({ + imports: [ + // ... + { + component: codeDye, + enabledEnvironment: ['local'], // 只在本地启用 + } + ], +}) +export class MainConfiguration {} +``` + + + + + +:::tip + +- 可以在 `本地` 或 `研发` 环境开启本组件,便于开发时定位问题,但是 `不建议` 在线上启用,会对线上访问性能产生影响。 + +::: + + + +## 配置染色 + +可以通过`matchQueryKey` 配置,控制当 `query` 参数包含`matchQueryKey` 配置对应的值的时候,进入染色链路,例如,配置为: + +```typescript +// src/config/config.local.ts +export default { + codeDye: { + matchQueryKey: 'codeDyeABC', + } +} +``` +当请求接口 `http://127.0.0.1:7001/test?codeDyeABC=html` 时,就会判断 `query` 中是否存在 `codeDyeABC` 参数来决定是否染色,并根据参数对应的值,来响应不同的染色结果。 + +也可以通过`matchHeaderKey` 配置,控制当 `headers` 参数包含 `matchHeaderKey` 配置对应的值的时候,进入染色链路,例如,配置为: + +```typescript +// src/config/config.local.ts +export default { + codeDye: { + matchHeaderKey: 'codeDyeHeader', + } +} +``` +当请求接口 `http://127.0.0.1:7001/test` 时,就会判断请求的 `headers` 中是否存在 `codeDyeHeader` 参数来决定是否染色,并根据参数对应的值,来响应不同的染色结果。 + + + +## 染色报告 + +开启了代码染色后,链路染色的结果,可以通过开启染色的不同参数值来配置,目前支持以下三种: + ++ `html`:`对` 当前请求的结果进行处理,将染色信息添加到结果中,响应为 `html`,可以在浏览器上查看,效果可以查看此文档上面的图片效果展示。 ++ `json`:`对` 当前请求的结果进行处理,将染色信息添加到结果中,响应为 `json` 结构化信息。 ++ `log`:`不对` 当前请求的结果进行处理,染色的信息将会输出到日志中,不影响请求。 + +例如,配置为: + +```typescript +// src/config/config.local.ts +export default { + codeDye: { + matchQueryKey: 'codeDyeXXX', + } +} +``` + +当请求接口 `http://127.0.0.1:7001/test?codeDyeXXX=html` 时,就会判断 `query` 中 `codeDyeXXX` 参数的值为 `html`,就将染色结果输出在当前请求的响应中,并且内容为 `html` 格式。 diff --git a/site/versioned_docs/version-3.0.0/extensions/consul.md b/site/versioned_docs/version-3.0.0/extensions/consul.md new file mode 100644 index 000000000000..8f3a6c18fa66 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/consul.md @@ -0,0 +1,367 @@ +# Consul + +consul 用于微服务下的服务治理,主要特点有:服务发现、服务配置、健康检查、键值存储、安全服务通信、多数据中心等。 + +本文介绍了如何用 consul 作为 midway 的服务注册发现中心,以及如何使用 consul来做软负载的功能。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ❌ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +感谢 [boostbob](https://github.com/boostbob) 提供的组件。 + + +效果如下图: +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01e5cFZx1I0draeZynr_!!6000000000831-2-tps-1500-471.png) + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01iLYF8r1HQ0B3b47Fh_!!6000000000751-2-tps-1500-895.png) + + +## 安装组件 + +首先安装 consul 组件和类型: + +```bash +$ npm i @midwayjs/consul@3 --save +$ npm i @types/consul --save-dev +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/consul": "^3.0.0", + // ... + }, + "devDependencies": { + "@types/consul": "^0.40.0", + // ... + } +} +``` + + + +## 目前支持的能力 + +- 注册能力(可选) +- 在停止服务的时候反注册(可选) +- 服务选择(随机) +- 暴露原始的 consul 对象 + + + +## 启用组件 + +```typescript +import * as consul from '@midwayjs/consul' + +@Configuration({ + imports: [ + // .. + consul + ], + importConfigs: [join(__dirname, 'config')] +}) +export class MainConfiguration {} +``` + + + +## 配置 + +配置 `config.default.ts` 文件: + +```typescript +// src/config/config.default +export default { + // ... + consul: { + provider: { + // 注册本服务 + register: true, + // 应用正常下线反注册 + deregister: true, + // consul server 服务地址 + host: '192.168.0.10', + // consul server 服务端口 + port: '8500', + // 调用服务的策略(默认选取 random 具有随机性) + strategy: 'random', + }, + service: { + // 此处是当前这个 midway 应用的地址 + address: '127.0.0.1', + // 当前 midway 应用的端口 + port: 7001, + // 做泳道隔离等使用 + tags: ['tag1', 'tag2'], + name: 'my-midway-project' + // others consul service definition + } + }, +} +``` + +打开我们 consul server 的 ui 地址,效果如下: + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01QI7A1d1dU3ECG8QxQ_!!6000000003738-2-tps-1500-471.png) + +可以观察到 my-midway-project 项目已经注册完毕。 + +假如停止我们的 midway 项目。 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01EDocUO1TIvRvpxXbw_!!6000000002360-2-tps-1500-401.png) + +我们可以看到我们项目的状态就变为红色。 + +我们演示多台的情况,如下表现:(1台在线+1台不在线) + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01kfmul91eSxu5EiJE3_!!6000000003871-2-tps-1500-405.png) + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01PZrdpp21Sir5n3y9I_!!6000000006984-2-tps-1500-360.png) + + + +## 作为客户端 + +例如我们作为客户端 A,需要调用服务 B 的接口,然后我们首先是查出 B 健康的服务,然后进行 http 请求。 + + +此处为了方便理解,我们模拟查询刚刚注册的成功的服务: +```typescript +import { Controller, Get, Inject, Provide } from '@midwayjs/core'; +import { BalancerService } from '@midwayjs/consul' + +@Provide() +@Controller('/') +export class HomeController { + + @Inject() + balancerService: BalancerService; + + @Get('/') + async home() { + const service = await this.balancerService.getServiceBalancer().select('my-midway-project'); + + // output + console.log(service) + + // ... + } +} + +``` +输出的 service 的内容为: +```typescript +{ + ID: 'c434e36b-1b62-c4e1-c4ec-76c5d3742ff8', + Node: '1b2d5b8771cb', + Address: '127.0.0.1', + Datacenter: 'dc1', + TaggedAddresses: { + lan: '127.0.0.1', + lan_ipv4: '127.0.0.1', + wan: '127.0.0.1', + wan_ipv4: '127.0.0.1' + }, + NodeMeta: { 'consul-network-segment': '' }, + ServiceKind: '', + ServiceID: 'my-midway-project:xxx:7001', + ServiceName: 'my-midway-project', + ServiceTags: [ 'tag1', 'tag2' ], + ServiceAddress: 'xxxxx', + ServiceTaggedAddresses: { + lan_ipv4: { Address: 'xxxxx', Port: 7001 }, + wan_ipv4: { Address: 'xxxxxx', Port: 7001 } + }, + ServiceWeights: { Passing: 1, Warning: 1 }, + ServiceMeta: {}, + ServicePort: 7001, + ServiceEnableTagOverride: false, + ServiceProxy: { MeshGateway: {}, Expose: {} }, + ServiceConnect: {}, + CreateIndex: 14, + ModifyIndex: 14 +} +``` +此时,我们只要通过 Address 和 ServicePort 去连接服务 B,比如做 http 请求。 + + +如果需要查询不健康的,则 `select` 方法的第二个参数传入 false 值: +```typescript +import { Controller, Get, Inject, Provide } from '@midwayjs/core'; +import { BalancerService } from '@midwayjs/consul' + +@Provide() +@Controller('/') +export class HomeController { + + @Inject() + balancerService: BalancerService; + + @Get('/') + async home() { + + const service = await this.balancerService + .getServiceBalancer() + .select('my-midway-project', false); + + console.log(service); + + // ... + } +} + +``` + + + +## 配置中心 + + +同时 consul 也能作为一个服务配置的地方,如下代码: +```typescript +import { Controller, Get, Inject } from '@midwayjs/core'; +import * as Consul from 'consul'; + +@Controller('/') +export class HomeController { + + @Inject('consul:consul') + consul: Consul.Consul; + + @Get('/') + async home() { + await this.consul.kv.set(`name`, `juhai`) + // let res = await this.consul.kv.get(`name`); + // console.log(res); + return 'Hello Midwayjs!'; + } +} + +``` +我们调用 `kv.set` 方法,我们可以设置对应的配置,通过 `kv.get` 方法可以拿到对应的配置。 + + +注意:在代码中,有同学出现,在每次请求中去 get 对应的配置,这时你的 QPS 多少对 Consul server 的压力。 + + +所以在QPS比较大的情况,可以如下处理: +```typescript +import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; +import * as Consul from 'consul'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class ConfigService { + + @Inject('consul:consul') + consul: Consul.Consul; + + config: any; + + @Init() + async init() { + setInterval(()=>{ + this.consul.kv.get(`name`).then(res=>{ + this.config = res; + }) + }, 5000); + this.config = await this.consul.kv.get(`name`); + } + + async getConfig(){ + return this.config; + } +} + +``` +上面的代码,相当于定时去获取对应的配置,当每个请求进来的时候,获取 Scope 为 ScopeEnum.Singleton 服务的 `getConfig` 方法,这样每 5s 一次获取请求,就减少了对服务的压力。 + +Consul 界面上如下图: + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01V3P6uK1rIVs19JiWn_!!6000000005608-2-tps-1500-374.png) + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN014O2GyH1sMvIhmlbs4_!!6000000005753-2-tps-1500-667.png) + + +一共提供如下几种方法: + +- [get](https://www.npmjs.com/package/consul#kv-get),获取对应key的value +- [keys](https://www.npmjs.com/package/consul#kv-keys),查询某个prefix的key的列表 +- [set](https://www.npmjs.com/package/consul#kv-set),设置对应的key的值 +- [del](https://www.npmjs.com/package/consul#kv-del),删除对应的key + + + +## 其他说明 + + +这样的好处,就是 A->B,B 也可以进行扩展,并且可以通过 tags 做泳道隔离。例如做单元隔离等。并且可以通过 ServiceWeights 做对应的权重控制。 + + +Consul 还能做 Key/Value 的配置中心的作用,这个后续我们考虑支持。 + + +## 搭建 Consul 测试服务 + + +下面描述了单机版本的 consul 搭建流程。 +```bash +docker run -itd -P consul +``` +然后执行 `docker ps` +```bash +➜ my_consul_app docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +1b2d5b8771cb consul "docker-entrypoint.s…" 4 seconds ago Up 2 seconds 0.0.0.0:32782->8300/tcp, 0.0.0.0:32776->8301/udp, 0.0.0.0:32781->8301/tcp, 0.0.0.0:32775->8302/udp, 0.0.0.0:32780->8302/tcp, 0.0.0.0:32779->8500/tcp, 0.0.0.0:32774->8600/udp, 0.0.0.0:32778->8600/tcp cocky_wing +``` +然后我们打开 8500 所对应的端口:(上图比如我的对应端口是 32779) + +[http://127.0.0.1:32779/ui/](http://127.0.0.1:32779/ui/dc1/kv) + +打开后效果如下: + +![](https://img.alicdn.com/imgextra/i2/O1CN014O2GyH1sMvIhmlbs4_!!6000000005753-2-tps-1500-667.png) + +然后我们的 `config.default.ts` 中的port就是 32779 端口。 + + + +## 下线服务 +如果想要手动将consul界面上不需要的服务给下线掉,可以通过下面的方法: +```typescript +import { Controller, Get, Inject, Provide } from '@midwayjs/core'; +import * as Consul from 'consul' + +@Provide() +@Controller('/') +export class HomeController { + + @Inject('consul:consul') + consul: Consul.Consul; + + @Get("/222") + async home2(){ + let res = await this.consul.agent.service.deregister(`my-midway-project:30.10.72.195:7002`); + console.log(res); + + // ... + } + +} + +``` +`deregister` 方法,对应 consul 界面上的名字。 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01d5QMUJ1DULTKPSJsr_!!6000000000219-2-tps-1500-465.png) diff --git a/site/versioned_docs/version-3.0.0/extensions/cos.md b/site/versioned_docs/version-3.0.0/extensions/cos.md new file mode 100644 index 000000000000..cf616b799470 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/cos.md @@ -0,0 +1,149 @@ +# 腾讯云对象存储(COS) + +本文介绍了如何使用 midway 接入腾讯云 COS。 + +相关信息: + +| 描述 | | +| ----------------- | --- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/cos@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/cos": "^3.0.0", + // ... + }, +} +``` + + + +## 引入组件 + + +首先,引入 组件,在 `configuration.ts` 中导入: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as cos from '@midwayjs/cos'; +import { join } from 'path' + +@Configuration({ + imports: [ + // ... + cos // 导入 cos 组件 + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + +## 配置 + +比如: + + +**单客户端配置** +```typescript +// src/config/config.default +export default { + // ... + cos: { + client: { + SecretId: '***********', + SecretKey: '***********', + }, + }, +} +``` + + +**多个客户端配置,需要配置多个** + +```typescript +// src/config/config.default +export default { + // ... + cos: { + clients: { + instance1: { + SecretId: '***********', + SecretKey: '***********', + }, + instance2: { + SecretId: '***********', + SecretKey: '***********', + }, + }, + }, +} +``` +更多参数可以查看 [cos-nodejs-sdk-v5](https://github.com/tencentyun/cos-nodejs-sdk-v5) 文档。 + + +## 使用 COS 服务 + + +我们可以在任意的代码中注入使用。 +```typescript +import { Provide, Controller, Inject, Get } from '@midwayjs/core'; +import { COSService } from '@midwayjs/cos'; + +@Provide() +export class UserService { + + @Inject() + cosService: COSService; + + async invoke() { + await this.cosService.sliceUploadFile({ + Bucket: 'test-1250000000', + Region: 'ap-guangzhou', + Key: '1.zip', + FilePath: './1.zip' + }, + } +} +``` + + +可以使用 `COSServiceFactory` 获取不同的实例。 +```typescript +import { COSServiceFactory } from '@midwayjs/cos'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + cosServiceFactory: COSServiceFactory; + + async save() { + const cos1 = await this.cosServiceFactory.get('instance1'); + const cos2 = await this.cosServiceFactory.get('instance3'); + + //... + + } +} +``` + diff --git a/site/versioned_docs/version-3.0.0/extensions/cron.md b/site/versioned_docs/version-3.0.0/extensions/cron.md new file mode 100644 index 000000000000..f2b1578cd8d1 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/cron.md @@ -0,0 +1,288 @@ +# 本地任务 + +和 bull 组件不同,cron 组件提供的是本地任务能力,即在每台机器的每个进程都会执行。如需不同机器或者不同进程之间只执行一次任务,请使用 [bull 组件](./bull) 。 + + + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ❌ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ✅ | +| 包含独立日志 | ✅ | + + + +## 安装组件 + +```bash +$ npm i @midwayjs/cron@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/cron": "^3.0.0", + // ... + }, +} +``` + + + +## 使用组件 + +将组件配置到代码中。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as cron from '@midwayjs/cron'; + +@Configuration({ + imports: [ + // ... + cron + ] +}) +export class MainConfiguration { + //... +} +``` + + + +## 编写任务处理类 + +使用 `@Job` 装饰器装饰一个类,用于快速定义一个任务处理器。 + +比如,在 `src/job` 目录中创建一个 `sync.job.ts`,用于某些数据同步任务,代码如下: + +```typescript +// src/job/sync.job.ts +import { Job, IJob } from '@midwayjs/cron'; +import { FORMAT } from '@midwayjs/core'; + +@Job({ + cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE, + start: true, +}) +export class DataSyncCheckerJob implements IJob { + async onTick() { + // ... + } +} +``` + +`@Job` 装饰器用于修饰一个任务类,在初始化时,框架会自动将其转变为一个任务。 + +任务类需要实现 `IJob` 接口,实现 `onTick` 方法,每当任务触发时,会自动调用 `onTick` 方法。 + +此外,还有一个可选的 `onComplete` 方法,用于在 `onTick` 完成后执行。 + +```typescript +@Job({ + cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE, + start: true, +}) +export class DataSyncCheckerJob implements IJob { + async onTick() { + // ... + } + + async onComplete() { + // 记录一些数据等等,用处不是很大 + } +} +``` + + + +`@Job` 装饰器的常用参数如下: + +| 参数 | 类型 | 描述 | +| --------- | ------- | ---------------------- | +| cronTime | string | crontab 表达式 | +| start | boolean | 是否自动启动任务 | +| runOnInit | boolean | 是否在初始化就执行一次 | + +更多参数,请参考 [Cron](https://github.com/kelektiv/node-cron)。 + + + +## 任务管理 + +除了定时执行任务,我们还通过框架提供的 API,对任务进行手动管理。 + +比如,下面的代码仅仅定义了一个任务,但是不会启动执行。 + +```typescript +@Job('syncJob', { + cronTime: '*/2 * * * * *', // 每隔 2s 执行 +}) +export class DataSyncCheckerJob implements IJob { + async onTick() { + // ... + } +} +``` + +我们定义了一个名为 `syncJob` 的任务,并且给它了一个默认的调度时间。 + + + +### 获取任务对象 + +我们可以通过两种方式获取任务对象。 + +通过`@InjectJob` 用来注入某个任务,参数为类本身或者任务名。 + +```typescript +// src/configuration.ts +import { Configuration, Inject } from '@midwayjs/core'; +import * as cron from '@midwayjs/cron'; +import { InjectJob, CronJob } from '@midwayjs/cron'; +import { DataSyncCheckerJob } from './job/sync.job'; + +@Configuration({ + imports: [ + cron + ], +}) +export class ContainerConfiguration { + @InjectJob(DataSyncCheckerJob) + syncJob: CronJob; + + @InjectJob('syncJob') + syncJob2: CronJob; + + async onServerReady() { + // this.syncJob === this.syncJob2 + } +} + +``` + +通过 Framework API 获取。 + +```typescript +// src/configuration.ts +import { Configuration, Inject } from '@midwayjs/core'; +import * as cron from '@midwayjs/cron'; +import { InjectJob, CronJob } from '@midwayjs/cron'; +import { DataSyncCheckerJob } from './job/sync.job'; + +@Configuration({ + imports: [ + cron + ], +}) +export class ContainerConfiguration { + @Inject() + cronFramework: cron.Framework; + + async onServerReady() { + const syncJob = this.cronFramework.getJob(DataSyncCheckerJob); + const syncJob2 = this.cronFramework.getJob('syncJob'); + + // syncJob === syncJob2 + } +} + +``` + +:::caution + +注意,任务对象都必须在 `onServerReady` 生命周期或者启动之后才能获取。 + +::: + + + +### 启停任务 + +我们可以在初始化或者某些程序执行完成之后,将这个任务启动。 + +```typescript +// src/configuration.ts +import { Configuration, Inject } from '@midwayjs/core'; +import * as cron from '@midwayjs/cron'; +import { InjectJob, CronJob } from '@midwayjs/cron'; +import { DataSyncCheckerJob } from './job/sync.job'; + +@Configuration({ + imports: [ + cron + ], +}) +export class ContainerConfiguration { + @InjectJob(DataSyncCheckerJob) + syncJob: CronJob; + + async onServerReady() { + this.syncJob.start(); + + // ... + this.syncJob.stop(); + } +} + +``` + + + +## 上下文 + +任务执行是在请求作用域中,其有着特殊的 Context 对象结构。 + +```typescript +export interface Context extends IMidwayContext { + job: CronJob; +} +``` + +这里的 `CronJob` 类型来自于 [Cron](https://github.com/kelektiv/node-cron) 包。 + + + +## 组件日志 + +组件有着自己的日志,默认会将 `ctx.logger` 记录在 `midway-cron.log` 中。 + +我们可以单独配置这个 logger 对象。 + +```typescript +export default { + midwayLogger: { + // ... + clients: { + // ... + cronLogger: { + fileLogName: 'midway-cron.log', + }, + } + } +} +``` + + + +## 全局配置 + +可以针对 Job 进行一些全局配置,会和每个 Job 的配置进行合并。 + +```typescript +export default { + cron: { + defaultCronJobOptions: { + // ... + } + } +} +``` + +这里的 `defaultCronJobOptions` 配置项请参考 [CronJobParameters](https://github.com/kelektiv/node-cron/blob/main/lib/job.js#L51) diff --git a/site/versioned_docs/version-3.0.0/extensions/cross_domain.md b/site/versioned_docs/version-3.0.0/extensions/cross_domain.md new file mode 100644 index 000000000000..8f7a3cf98a3e --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/cross_domain.md @@ -0,0 +1,196 @@ +# 跨域 + +适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用跨域组件,支持 `cors` 、`jsonp` 多种模式。 + +相关信息: + +| web 支持情况 | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/cross-domain --save +``` + +## 引入组件 + +在 `src/configuration.ts` 中引入组件。 + +```typescript +import * as crossDomain from '@midwayjs/cross-domain'; +@Configuration({ + imports: [ + // ...other components + crossDomain + ], +}) +export class MainConfiguration {} +``` + + + +## 什么是跨域 + +假设有两个网站: + +- **A.com**:这是你的网站,你想要从这里访问一些资源。 +- **B.com**:这是另一个网站,它拥有你想要访问的资源。 + +### 场景设定 + +1. **A.com** 是你的主网站,你在这里运行一些 JavaScript 代码。 +2. **B.com** 拥有一些 API 接口,你想要通过 A.com 的 JavaScript 代码来调用这些 API 接口。 + +由于同源策略,浏览器默认不允许 A.com 的 JavaScript 代码直接访问 B.com 的资源。这是因为浏览器出于安全考虑,防止恶意网站读取其他网站的敏感数据。你想要在 A.com 上运行的 JavaScript 代码中发起一个请求到 B.com 的 API 接口,这个请求会被视为跨域请求。 + +简单来说,从 A.com 访问 B.com 的接口,就是跨域。 + +此外,跨域还有一些条件: + +* 1、同源策略是浏览器安全机制的一部分,所以一般只有浏览器访问才有跨域问题 +* 2、当浏览器发起跨域请求时,它会根据当前源自动添加一个 `Origin` 头部到请求中,服务端也会根据 `origin` 头判断来源,进而做处理 + +所以,当你发现跨域设置不生效时,请检查: + +* 1、是否真的是跨域请求 +* 2、是否真的从浏览器发起 +* 3、是否请求带有 `origin` 头 + + + +## 什么是 CORS + +在前端代码调用后端服务中经常会碰到下面的错误,这就最为常见的跨域 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 错误。 + +``` +Access to fetch at 'http://127.0.0.1:7002/' from origin 'http://127.0.0.1:7001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. +``` + +出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求。例如,`XMLHttpRequest` 和 [Fetch API](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API) 遵循[同源策略](https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy)。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。 + +CORS 机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 [`XMLHttpRequest`](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) 或 [Fetch](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API))使用 CORS,以降低跨源 HTTP 请求所带来的风险。 + + + +## 常见的 CORS 配置 + +下面列举几种跨域的解决方案,我们以 `fetch` 方法为例。 + +### 未使用 `credentials` + +客户端。 + +```javascript +fetch(url); +``` + +服务端配置。 + +```typescript +// src/config/config.default.ts +export default { + // ... + cors: { + origin: '*', + }, +} +``` + +### 使用 `credentials` + +客户端。 + +```javascript +fetch(url, { + credentials: "include", +}); +``` + +服务端配置 + +```typescript +// src/config/config.default.ts +export default { + // ... + cors: { + credentials: true, + }, +} +``` + +### 限制 `origin` 来源 + +假如我们的网页地址为 `http://127.0.0.1:7001` 而接口为 `http://127.0.0.1:7002`。 + +客户端。 + +```javascript +fetch('http://127.0.0.1:7002/', { + credentials: 'include' +}) +``` + +服务端配置,注意,由于启用了 `credentials`,这个时候 `origin` 字段不能为 `*`。 + +```typescript +// src/config/config.default.ts +export default { + // ... + cors: { + origin: 'http://127.0.0.1:7001', + credentials: true, + }, +} +``` + + + +## 更多 CORS 配置 + +完整可用配置如下: + +```typescript +export const cors = { + // 允许跨域的方法,【默认值】为 GET,HEAD,PUT,POST,DELETE,PATCH + allowMethods: string |string[]; + // 设置 Access-Control-Allow-Origin 的值,【默认值】会获取请求头上的 origin + // 也可以配置为一个回调方法,传入的参数为 request,需要返回 origin 值 + // 例如:http://test.midwayjs.org + // 如果设置了 credentials,则 origin 不能设置为 * + origin: string|Function; + // 设置 Access-Control-Allow-Headers 的值,【默认值】会获取请求头上的 Access-Control-Request-Headers + allowHeaders: string |string[]; + // 设置 Access-Control-Expose-Headers 的值 + exposeHeaders: string |string[]; + // 设置 Access-Control-Allow-Credentials,【默认值】false + // 也可以配置为一个回调方法,传入的参数为 request,返回值为 true 或 false + credentials: boolean|Function; + // 是否在执行报错的时候,把跨域的 header 信息写入到 error 对的 headers 属性中,【默认值】false + keepHeadersOnError: boolean; + // 设置 Access-Control-Max-Age + maxAge: number; +} +``` + + +## JSONP 配置 + +可以在 `src/config/config.default` 中进行 JSONP 配置。 + +```typescript +// src/config/config.default.ts +export default { + // ... + jsonp: { + callback: 'jsonp', + limit: 512, + }, +} +``` diff --git a/site/versioned_docs/version-3.0.0/extensions/egg.md b/site/versioned_docs/version-3.0.0/extensions/egg.md new file mode 100644 index 000000000000..62c256e24b7f --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/egg.md @@ -0,0 +1,857 @@ +# EggJS + +Midway 可以使用 EggJS 作为上层 Web 框架,EggJS 提供了非常多常用的插件和 API,帮助用户快速构建企业级 Web 应用。本章节内容,主要介绍 EggJS 在 Midway 中如何使用自身的能力。 + +| 描述 | | +| ----------------- | ---- | +| 包含独立主框架 | ✅ | +| 包含独立日志 | ✅ | + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/web@3 egg --save +$ npm i @midwayjs/egg-ts-helper --save-dev +``` + +针对 EggJS 场景,这些包列举如下。 + +```json + "dependencies": { + "@midwayjs/web": "^3.0.0", + "@midwayjs/core": "^3.0.0", + "egg": "^2.0.0", + "egg-scripts": "^2.10.0" + }, + "devDependencies": { + "@midwayjs/egg-ts-helper": "^1.0.1", + }, +``` + +| @midwayjs/web | **必须**,Midway EggJS 适配层 | +|-------------------------|----------------------------| +| @midwayjs/core | **必须**,Midway 核心包 | +| egg | **必须**,EggJS 依赖包,提供定义等其他能力 | +| egg-scripts | **可选**,EggJS 启动脚本 | +| @midwayjs/egg-ts-helper | **可选**,EggJS 定义生成工具 | + +也可以直接使用脚手架创建示例。 + +```bash +# npm v6 +$ npm init midway --type=egg-v3 my_project + +# npm v7 +$ npm init midway -- --type=egg-v3 my_project +``` + + + +## 开启组件 + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as web from '@midwayjs/web'; +import { join } from 'path'; + +@Configuration({ + imports: [web], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration { + @App() + app: web.Application; + + async onReady() { + // ... + } +} + +``` + + + +## 和默认 EggJS 的不同之处 + + +- 1、从 v3 开始,midway 提供了更多的组件,大部分 egg 内置插件默认禁用 +- 2、baseDir 默认调整为 `src` 目录,服务器上为 `dist` 目录 +- 3、禁用 egg-logger,全部替换为 @midwayjs/logger,不可切换 + + + +整个架构如下: +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1614842824740-fc0c1432-3ace-4f77-b51f-15212984b168.png) + + +## 目录结构 + + +除了 Midway 提供的目录结构外,EggJS 还有一些特殊的目录结构(不可变),整个结构如下。 +``` +➜ my_midway_app tree +. +├── src +| ├── app.ts ## EggJS 扩展 Worker 生命周期文件(可选) +| ├── agent.ts ## EggJS 扩展 Agent 生命周期文件(可选) +| ├── app ## EggJS 固定的根目录(可选) +| │ ├── public ## EggJS 静态托管插件的默认目录(可配) +| │ | └── reset.css +| │ ├── view (可选) ## EggJS 模板渲染的默认目录(可配) +| │ | └── home.tpl +| │ └── extend (可选) ## EggJS 扩展目录(可配) +| │ ├── helper.ts (可选) +| │ ├── request.ts (可选) +| │ ├── response.ts (可选) +| │ ├── context.ts (可选) +| │ ├── application.ts (可选) +| │ └── agent.ts (可选) +| │ +| ├── config +| | ├── plugin.ts +| | ├── config.default.ts +| │ ├── config.prod.ts +| | ├── config.test.ts (可选) +| | ├── config.local.ts (可选) +| | └── config.unittest.ts (可选) +│ ├── controller ## Midway 控制器目录(推荐) +│ ├── service ## Midway 服务目录(推荐) +│ └── schedule ## Midway 定时器目录(推荐) +│ +├── typings ## EggJS 定义生成目录 +├── test +├── package.json +└── tsconfig.json +``` +以上是 EggJS 的目录结构全貌,其中包含了很多 EggJS 特有的目录,有一些在 Midway 体系中已经有相应的能力替代,可以直接替换。整个结构,基本上等价于将 EggJS 的目录结构移动到了 `src` 目录下。 + + +由于 EggJS 是基于约定的框架,整个工程的目录结构是固定的,这里列举一些常用的约定目录。 + +| `src/app/public/**` | 用于放置静态资源,可选,具体参见内置插件 [egg-static](https://github.com/eggjs/egg-static)。 | +| --- | --- | +| `src/config/config.{env}.ts` | 用于编写配置文件,具体参见[配置](https://eggjs.org/zh-cn/basics/config.html)。 | +| `src/config/plugin.js` | 用于配置需要加载的插件,具体参见[插件](https://eggjs.org/zh-cn/basics/plugin.html)。 | +| `test/**` | 具体参见[单元测试](https://eggjs.org/zh-cn/core/unittest.html)。 | +| `src/app.js` 和 `src/agent.js` | 用于自定义启动时的初始化工作,可选,具体参见[启动自定义](https://eggjs.org/zh-cn/basics/app-start.html)。关于`agent.js`的作用参见[Agent机制](https://eggjs.org/zh-cn/core/cluster-and-ipc.html#agent-%E6%9C%BA%E5%88%B6)。 | + + + +## 配置定义 + + +Midway 在脚手架中提供了标准的 EggJS 的 TS 配置写法,MidwayConfig 中包括了 egg 中配置的定义和属性提示,结构如下。 +```typescript +// src/config/config.default.ts +import { MidwayConfig, MidwayAppInfo } from '@midwayjs/core'; + +export default (appInfo: MidwayAppInfo) => { + return { + // use for cookie sign key, should change to your own and keep security + keys: appInfo.name + '_xxxx', + egg: { + port: 7001, + }, + // security: { + // csrf: false, + // }, + } as MidwayConfig; +}; + +``` +通过这样返回方法的形式,在运行期会被自动执行,合并进完整的配置对象。 + + +这个函数的参数为 `MidwayAppConfig` 类型,值为以下内容。 + +| **appInfo** | **说明** | +| --- | --- | +| pkg | package.json | +| name | 应用名,同 pkg.name | +| baseDir | 应用代码的 src (本地开发)或者 dist (上线后)目录 | +| appDir | 应用代码的目录 | +| HOME | 用户目录,如 admin 账户为 /home/admin | +| root | 应用根目录,只有在 local 和 unittest 环境下为 baseDir,其他都为 HOME。 | + + + +:::info +注意,这里的 `baseDir` 和 `appDir` 和 EggJS 应用有所区别。 +::: + + + + +## 使用 Egg 插件 + +插件是 EggJS 的特色之一,`@midwayjs/web` 也支持 EggJS 的插件体系,但是在有 Midway 组件的情况下,尽可能优先使用 Midway 组件。 + + +插件一般通过 npm 模块的方式进行复用。 +```bash +$ npm i egg-mysql --save +``` +然后需要在应用或框架的 `src/config/plugin.js` 中声明开启。 + + +如果有 `export default` ,请写在其中。 +```typescript +import { EggPlugin } from 'egg'; +export default { + static: false, // default is true + mysql: { + enable: true, + package: 'egg-mysql' + } +} as EggPlugin; + +``` +如果没有 `export default` ,可以直接导出。 +```typescript +// src/config/plugin.ts +// 使用 mysql 插件 +export const mysql = { + enable: true, + package: 'egg-mysql', +}; +``` + + +在开启插件之后,我们就可以在业务代码中使用插件提供的功能了。一般来说,插件会将对象挂载到 EggJS 的 `app` 和 `ctx` 之上,然后直接使用。 + + +```typescript +app.mysql.query(sql, values); // egg 提供的方法 +``` +在 Midway 中可以通过 `@App` 获取 `app` 对象,以及在请求作用域中通过 `@Inject() ctx` 获取 `ctx` 对象,所以我们可以通过注入来获取插件对象。 + + +```typescript +import { Provide, Inject, Get } from '@midwayjs/core'; +import { Application, Context } from '@midwayjs/web'; + +@Provide() +export class HomeController { + + @App() + app: Application; + + @Inject() + ctx: Context; + + @Get('/') + async home() { + this.app.mysql.query(sql, values); // 调用 app 上的方法(如果有的话) + this.ctx.mysql.query(sql, values); // 调用挂载在 ctx 上的方法(如果有的话) + } +} +``` +此外,还可以通过 `@Plugin` 装饰器来直接注入 `app` 挂载的插件,默认情况下,如果不传参数,将以属性名作为 key。 + + +```typescript +import { Provide, Get, Plugin } from '@midwayjs/core'; + +@Provide() +export class HomeController { + + @Plugin() + mysql: any; + + @Get('/') + async home() { + this.mysql.query(sql, values); + } +} +``` +:::info +`@Plugin() mysql` 等价于 `app.mysql` 。 `@Plugin` 的作用就是从 app 对象上拿对应属性名的插件,所以 `@Plugin() xxx` 就等于 `app['xxx']` 。 +::: + + +## Web 中间件 + + +中间件样例如下: + + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { Context, NextFunction } from '@midwayjs/web'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + const startTime = Date.now(); + await next(); + console.log(Date.now() - startTime); + }; + } + +} +``` + +:::caution +注意 + +1、如果要继续使用 EggJS 传统的函数式写法,必须将文件放在 `src/app/middleware` 下 + +2、egg 自带的内置中间件已经集成 + +::: + +应用中间件。 + +```typescript +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as egg from '@midwayjs/web'; +import { ReportMiddleware } from './middleware/user.middleware'; + +@Configuration({ + imports: [egg] + // ... +}) +export class MainConfiguration { + + @App() + app: egg.Application; + + async onReady() { + this.app.useMiddleware(ReportMiddleware); + } +} + +``` + +更多用法请参考 [Web 中间件](../middleware) + + + +## 中间件顺序 + +由于 egg 也有自己的中间件逻辑,在新版本中,我们将中间件加载顺序做了一定的处理,执行顺序如下: + +- 1、egg 框架中的中间件 +- 2、egg 插件通过 config.coreMiddleware 添加的顺序 +- 3、业务代码配置在 config.middleware 中配置的顺序 +- 4、app.useMiddleware 添加的顺序 + +因为 midway 的中间件会后置加载,所以我们可以在 onReady 中进行自定义排序。 + + + +## BodyParser + +egg 自带 `bodyParser` 功能,默认会解析 `Post` 请求,自动识别 `json` 和 `form` 类型。 + +如需 text 或者 xml,可以自行配置。 + +默认的大小限制为 `1mb`,可以单独对每项配置大小。 + +```typescript +// src/config/config.default +export default { + // ... + bodyParser: { + formLimit: '1mb', + jsonLimit: '1mb', + textLimit: '1mb', + xmlLimit: '1mb', + }, +} +``` + +注意,使用 Postman 做 Post 请求时的类型选择: + +![postman](https://img.alicdn.com/imgextra/i4/O1CN01QCdTsN1S347SuzZU5_!!6000000002190-2-tps-1017-690.png) + + + + +## 定时任务 +v3 开始请参考 [bull 组件](./bull) 。 + +如需兼容之前的 [egg 定时任务](https://eggjs.org/zh-cn/basics/schedule.html) ,请照下列方法。 + +首先安装 `midway-schedule` 依赖。 + +```bash +$ npm i midway-schedule --save +``` + +添加到插件中即可。 + +```typescript +// src/config/plugin.ts +export default { + schedule: true, + schedulePlus: { + enable: true, + package: 'midway-schedule', + }, +}; +``` + +使用请参考上一版本文档。 + + + +## 日志 + +v3 开始无法使用 egg-logger,请参考 [日志](../logger) 章节。 + + + +## 异常处理 + +EggJS 框架通过 [onerror](https://github.com/eggjs/egg-onerror) 插件提供了统一的错误处理机制,会作为 Midway 的兜底错误逻辑,和 [错误过滤器](../error_filter) 不冲突。 + +对一个请求的所有处理方法(Middleware、Controller、Service)中抛出的任何异常都会被它捕获,并自动根据请求想要获取的类型返回不同类型的错误(基于 [Content Negotiation](https://tools.ietf.org/html/rfc7231#section-5.3.2))。 + + + +| 请求需求的格式 | 环境 | errorPageUrl 是否配置 | 返回内容 | +| --- | --- | --- | --- | +| HTML & TEXT | local & unittest | - | onerror 自带的错误页面,展示详细的错误信息 | +| HTML & TEXT | 其他 | 是 | 重定向到 errorPageUrl | +| HTML & TEXT | 其他 | 否 | onerror 自带的没有错误信息的简单错误页(不推荐) | +| JSON & JSONP | local & unittest | - | JSON 对象或对应的 JSONP 格式响应,带详细的错误信息 | +| JSON & JSONP | 其他 | - | JSON 对象或对应的 JSONP 格式响应,不带详细的错误信息 | + + + + +onerror 插件的配置中支持 errorPageUrl 属性,当配置了 errorPageUrl 时,一旦用户请求线上应用的 HTML 页面异常,就会重定向到这个地址。 + + +在 `src/config/config.default.ts` 中 +```typescript +// src/config/config.default.ts +module.exports = { + onerror: { + // 线上页面发生异常时,重定向到这个页面上 + errorPageUrl: '/50x.html', + }, +}; +``` + + + +## 扩展 Application/Context/Request/Response + + +### 增加扩展逻辑 + + +虽然 MidwayJS 并不希望直接将属性挂载到 koa 的 Context,App 上(会造成管理和定义的不确定性),但是 EggJS 的这项功能依旧可用。 + + +文件位置如下。 +``` +➜ my_midway_app tree +. +├── src +│ ├── app +│ │ └── extend +│ │ ├── application.ts +│ │ ├── context.ts +│ │ ├── request.ts +│ │ └── response.ts +│ ├── config +│ └── interface.ts +├── test +├── package.json +└── tsconfig.json +``` +内容和原来的 EggJS 相同。 +```typescript +// src/app/extend/context.ts +export default { + get hello() { + return 'hello world'; + }, +}; +``` +### 增加扩展定义 + +Context 请使用 Midway 的方式来扩展,请查看 [扩展上下文定义](/docs/context_definition)。 + + +其余的部分,沿用 egg 的方式,请在 `src/interface.ts` 中扩展。 +```typescript +// src/interface.ts +declare module 'egg' { + interface Request { + // ... + } + interface Response { + // ... + } + interface Application { + // ... + } +} +``` +:::info +业务自定义扩展的定义请 **不要放在根目录** `typings` 下,避免被 ts-helper 工具覆盖掉。 +::: + + + +## 使用 egg-scripts 部署 + +由于 EggJS 提供了默认的多进程部署工具 `egg-scripts` ,Midway 也继续支持这种方式,如果上层是 EggJS,推荐这种部署方式。 + +首先在依赖中,确保安装 `egg-scripts` 包。 + +```bash +$ npm i egg-scripts --save +``` + + + +添加 `npm scripts` 到 `package.json`: + +在上面的代码构建之后,使用我们的 `start` 和 `stop` 命令即可完成启动和停止。 + +```json +"scripts": { + "start": "egg-scripts start --daemon --title=********* --framework=@midwayjs/web", + "stop": "egg-scripts stop --title=*********", +} +``` + + + +:::info + +`*********` 的地方是你的项目名。 +::: + +> 注意:`egg-scripts` 对 Windows 系统的支持有限,参见 [#22](https://github.com/eggjs/egg-scripts/pull/22)。 + +#### + +**启动参数** + +```bash +$ egg-scripts start --port=7001 --daemon --title=egg-server-showcase +``` + +Copy + +如上示例,支持以下参数: + +- `--port=7001` 端口号,默认会读取环境变量 process.env.PORT,如未传递将使用框架内置端口 7001。 +- `--daemon` 是否允许在后台模式,无需 nohup。若使用 Docker 建议直接前台运行。 +- `--env=prod` 框架运行环境,默认会读取环境变量 process.env.EGG_SERVER_ENV, 如未传递将使用框架内置环境 prod。 +- `--workers=2` 框架 worker 线程数,默认会创建和 CPU 核数相当的 app worker 数,可以充分的利用 CPU 资源。 +- `--title=egg-server-showcase` 用于方便 ps 进程时 grep 用,默认为 `egg-server-${appname}`。 +- `--framework=yadan` 如果应用使用了[自定义框架](https://eggjs.org/zh-cn/advanced/framework.html),可以配置 package.json 的 egg.framework 或指定该参数。 +- `--ignore-stderr` 忽略启动期的报错。 +- `--https.key` 指定 HTTPS 所需密钥文件的完整路径。 +- `--https.cert` 指定 HTTPS 所需证书文件的完整路径。 +- 所有 [egg-cluster](https://github.com/eggjs/egg-cluster) 的 Options 都支持透传,如 --port 等。 + +更多参数可查看 [egg-scripts](https://github.com/eggjs/egg-scripts) 和 [egg-cluster](https://github.com/eggjs/egg-cluster) 文档。 + +:::info + +使用 egg-scripts 部署的日志会存放在 **用户目录** 下**,**比如 `/home/xxxx/logs` 。 + +::: + + + +## 启动环境 + +原有 egg 使用 `EGG_SERVER_ENV` 中作为环境标志,在 Midway 中请使用 `MIDWAY_SERVER_ENV`。 + + + +## State 类型定义 + +在 egg 底层的 koa 的 Context 中有一个特殊的 State 属性,通过和 Context 类似的方式可以扩展 State 定义。 + +```typescript +// src/interface.ts + +declare module '@midwayjs/web/dist/interface' { + interface Context { + abc: string; + } + + interface State{ + bbb: string; + ccc: number; + } +} +``` + + + +## 配置 + +### 默认配置 + +```typescript +// src/config/config.default +export default { + // ... + egg: { + port: 7001, + }, +} +``` + +`@midwayjs/web` 所有参数如下: + +| 配置项 | 类型 | 描述 | +| -------------- | ---------------- | ---------------------------- | +| port | number | 必填,启动的端口 | +| key | string | Buffer | +| cert | string | Buffer | +| ca | string | Buffer | +| hostname | string | 监听的 hostname,默认 127.1 | +| http2 | boolean | 可选,http2 支持,默认 false | +| queryParseMode | simple\|extended | 默认为 extended | +| queryParseOptions | `qs.IParseOptions` | 解析选项,当使用'simple'模式解析时可用 | + +以上的属性,对本地和使用 `bootstrap.js` 部署的应用生效。 + + + +### 修改端口 + +:::tip + +注意,这个方式只会对本地研发,以及使用 bootstrap.js 文件部署的项目生效。 + +::: + +默认情况下,我们在 `config.default` 提供了 `7001` 的默认端口参数,修改它就可以修改 egg http 服务的默认端口。 + +比如我们修改为 `6001`: + +```typescript +// src/config/config.default +export default { + // ... + egg: { + port: 6001, + }, +} +``` + +默认情况下,单测环境由于需要 supertest 来启动端口,我们的 port 配置为 `null`。 + +```typescript +// src/config/config.unittest +export default { + // ... + egg: { + port: null, + }, +} +``` + +此外,也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口,此方法会覆盖配置中的端口。 + + + +### 全局前缀 + +此功能请参考 [全局前缀](../controller#全局路由前缀)。 + + + +### Https 配置 + +在大多数的情况,请尽可能使用外部代理的方式来完成 Https 的实现,比如 Nginx。 + +在一些特殊场景下,你可以通过配置 SSL 证书(TLS 证书)的方式,来直接开启 Https。 + +首先,你需要提前准备好证书文件,比如 `ssl.key` 和 `ssl.pem`,key 为服务端私钥,pem 为对应的证书。 + +然后配置即可。 + +```typescript +// src/config/config.default +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export default { + // ... + egg: { + key: join(__dirname, '../ssl/ssl.key'), + cert: join(__dirname, '../ssl/ssl.pem'), + }, +} +``` + + + +### favicon 设置 + +默认情况下,浏览器会发起一个 `favicon.ico` 的请求。 + +```typescript +// src/config/config.default +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export default { + // ... + siteFile: { + '/favicon.ico': readFileSync(join(__dirname, 'favicon.png')), + }, +} +``` + + + +如果开启了 `@midwayjs/static-file` 组件,那么会优先使用组件的静态文件托管。 + +### 修改上下文日志 + +可以单独修改 egg 框架的上下文日志。 + +```typescript +export default { + egg: { + contextLoggerFormat: info => { + const ctx = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} [${ctx.userId} - ${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`; + } + // ... + }, +}; +``` + + + +### Query 数组解析 + +默认情况下,`ctx.query` 会解析为忽略数组的情况,而 `ctx.queries` 会严格的将所有的字段都变成数组。 + +如果调整 `queryParseMode` ,则可以使 `ctx.query` 变为两者之间的结构(querystring 的结果)。 + +```typescript +// src/config/config.default +export default { + // ... + egg: { + // ... + queryParseMode: 'simple', + queryParseOptions: { + arrayLimit: 100, + }, + }, +} +``` + + + + +## 常见问题 +### 1、生成 ts 定义 + + +Midway 提供了 `@midwayjs/egg-ts-hepler` 工具包,用于快速生成 EggJS 开发时所依赖的定义。 +```bash +$ npm install @midwayjs/egg-ts-helper --save-dev +``` +在 `package.json` 中加入对应的 `ets` 命令即可,一般来说,我们会在 dev 命令前加入,以保证代码的正确性。 +```json + "scripts": { + "dev": "cross-env ets && cross-env NODE_ENV=local midway-bin dev --ts", + }, +``` +:::info +在第一次编写代码前,需要执行一次此命令才能有 ts 定义生成。 +::: + + +EggJS 生成的定义在 `typings` 目录中。 +``` +➜ my_midway_app tree +. +├── src ## midway 项目源码 +├── typings ## EggJS 定义生成目录 +├── test +├── package.json +└── tsconfig.json +``` + + +### 2、EggJS 中 Configuration 的特殊情况 + + +在 EggJS 下, `configuration.ts` 中的生命周期**只会在 worker 下加载执行**。如果在 Agent 有类似的需求,请直接使用 EggJS 自身的 `agent.ts` 处理。 + + + +### 3、异步初始化配置无法覆盖插件配置 + +`onConfigLoad` 生命周期会在 egg 插件(若有)初始化之后执行,所以不能用于覆盖 egg 插件所使用的配置。 + + + +### 4、默认的 csrf 错误 + + +在 post 请求,特别是第一次时用户会发现一个 csrf 报错。原因是 egg 在框架中默认内置了安全插件 [egg-security](https://github.com/eggjs/egg-security), 默认开启了 csrf 校验。 + + +我们可以在配置中关闭它,但是更好的是去[**了解它**](https://eggjs.org/zh-cn/core/security.html#%E5%AE%89%E5%85%A8%E5%A8%81%E8%83%81-csrf-%E7%9A%84%E9%98%B2%E8%8C%83)之后再做选择。 + +```typescript +export const security = { + csrf: false, +}; +``` + + + + +### 5、不存在定义的问题 + +一些 egg 插件未提供 ts 定义,导致使用会出现未声明方法的情况,比如 egg-mysql。 +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01mv68zG1zN6nALff8n_!!6000000006701-2-tps-1478-876.png) +可以使用 any 绕过。 + +```typescript +await (this.app as any).mysql.query(sql); +``` + +或者可以自行增加扩展定义。 + +### 6、获取 Http Server + +Eggjs 内部封装了原始的 HttpServer,需要通过事件获取。 + +```typescript +// src/configuration.ts +import { Configuration, App } from '@midwayjs/core'; +import { Application } from '@midwayjs/web'; + +@Configuration(/***/) +export class MainConfiguration { + + @App('egg') + app: Application; + + // ... + async onServerReady() { + this.app.once('server', (server) => { + // ... + }) + } +} +``` + diff --git a/site/versioned_docs/version-3.0.0/extensions/etcd.md b/site/versioned_docs/version-3.0.0/extensions/etcd.md new file mode 100644 index 000000000000..d20adf80ab4c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/etcd.md @@ -0,0 +1,194 @@ +# ETCD + +etcd 是云原生架构中重要的基础组件,由 CNCF 孵化托管。etcd 在微服务和 Kubernates 集群中可以作为服务注册于发现,也可以作为 key-value 存储的中间件。 + +Midway 提供基于 [etcd3](https://github.com/microsoft/etcd3) 模块封装的组件,提供 etcd 的客户端调用能力。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/etcd@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/etcd": "^3.0.0", + // ... + }, +} +``` + + + + +## 引入组件 + + +首先,引入 组件,在 `configuration.ts` 中导入: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as etcd from '@midwayjs/etcd'; +import { join } from 'path' + +@Configuration({ + imports: [ + // ... + etcd, + ], + // ... +}) +export class MainConfiguration { +} +``` + + + +## 配置默认客户端 + +大部分情况下,我们可以只使用默认客户端来完成功能。 + +```typescript +// src/config/config.default.ts +export default { + // ... + etcd: { + client: { + host: [ + '127.0.0.1:2379' + ] + }, + }, +} +``` + + + +## 使用默认客户端 + +配置完成后,我们就可以在代码中使用了。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { ETCDService } from '@midwayjs/etcd'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + etcdService: ETCDService; + + async invoke() { + + await this.etcdService.put('foo').value('bar'); + + const fooValue = await this.etcdService.get('foo').string(); + console.log('foo was:', fooValue); + + const allFValues = await this.etcdService.getAll().prefix('f').keys(); + console.log('all our keys starting with "f":', allFValues); + + await this.etcdService.delete().all(); + } +} +``` + +更多 API 请参考 ts 定义或者 [官网文档](https://microsoft.github.io/etcd3/classes/etcd3.html)。 + + + +## 多实例配置 + +```typescript +// src/config/config.default.ts +export default { + // ... + etcd: { + clients: { + instance1: { + { + host: [ + '127.0.0.1:2379' + ] + }, + }, + instance2: { + { + host: [ + '127.0.0.1:2379' + ] + }, + } + } + }, +} +``` + + + +## 多实例获取 + +```typescript +import { Provide } from '@midwayjs/core'; +import { ETCDServiceFactory } from '@midwayjs/etcd'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + etcdServiceFactory: ETCDServiceFactory; + + async invoke() { + const instance1 = this.etcdServiceFactory.get('instance1'); + // ... + + const instance2 = this.etcdServiceFactory.get('instance2'); + // ... + } +} +``` + + + +## 动态创建实例 + +```typescript +import { Provide } from '@midwayjs/core'; +import { ETCDServiceFactory } from '@midwayjs/etcd'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + etcdServiceFactory: ETCDServiceFactory; + + async invoke() { + const instance3 = await this.etcdServiceFactory.createInstance({ + host: [ + '127.0.0.1:2379' + ] + }, 'instance3'); + // ... + } +} +``` + diff --git a/site/versioned_docs/version-3.0.0/extensions/express.md b/site/versioned_docs/version-3.0.0/extensions/express.md new file mode 100644 index 000000000000..ec99f0754db2 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/express.md @@ -0,0 +1,604 @@ +# Express + +本章节内容,主要介绍在 Midway 中如何使用 Express 作为上层框架,并使用自身的能力。 + +| 描述 | | +| -------------- | ---- | +| 包含独立主框架 | ✅ | +| 包含独立日志 | ✅ | + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/express@3 --save +$ npm i @types/body-parser @types/express @types/express-session --save-dev +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/express": "^3.0.0", + // ... + }, + "devDependencies": { + "@types/body-parser": "^1.19.2", + "@types/express": "^4.17.13", + "@types/express-session": "^1.17.4", + // ... + } +} +``` + +也可以直接使用脚手架创建示例。 + +```bash +# npm v6 +$ npm init midway --type=express-v3 my_project + +# npm v7 +$ npm init midway -- --type=express-v3 my_project +``` + + +针对 Express,Midway 提供了 `@midwayjs/express` 包进行了适配,在其中提供了 Midway 特有的依赖注入、切面等能力。 + +:::info +我们使用的 Express 版本为 `v4` 。 +::: + + +## 目录结构 +``` +. +├── src +│ ├── controller # controller接口的地方 +│ ├── service # service逻辑处理的地方 +| └── configuration.ts # 入口及生命周期配置、组件管理 +├── test +├── package.json +└── tsconfig.json +``` + + + +## 开启组件 + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as express from '@midwayjs/express'; +import { join } from 'path'; + +@Configuration({ + imports: [express], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration { + @App() + app: express.Application; + + async onReady() {} +} +``` + + + + +## 控制器(Controller) + + +整个请求控制器的写法和 Midway 适配其他框架的类似。为了和其他场景的框架写法一致,在请求的时候,Midway 将 Express 的 `req` 映射为 `ctx` 对象。 +```typescript +import { Inject, Controller, Get, Provide, Query } from '@midwayjs/core'; +import { Context, NextFunction } from '@midwayjs/express'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async home(@Query() id) { + console.log(id); // req.query.id === id + return 'hello world'; // 简单返回,等价于 res.send('hello world'); + } +} +``` +你也可以额外注入 `req` 和 `res` 。 +```typescript +import { Inject, Controller, Get, Provide, Query } from '@midwayjs/core'; +import { Context, Response, NextFunction } from '@midwayjs/express'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; // 即为 req + + @Inject() + req: Context; + + @Inject() + res: Response; + + @Get('/') + async home(@Query() id) { + // this.req.query.id === id + } +} +``` + + + +## Web 中间件 + + +Express 的中间件写法比较特殊,它的参数不同。 + + +```typescript +import { Middleware } from '@midwayjs/core'; +import { Context, Response, NextFunction } from '@midwayjs/express'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async ( + req: Context, + res: Response, + next: NextFunction + ) => { + console.log('Request...'); + next(); + }; + } + +} +``` + +注意,这里我们导出了一个 `ReportMiddleware` 类,为了方便对接异步流程,`resolve` 返回可以是 async 函数。 + +Express 中的 next 方法,用于调用到下一个中间件,指的注意的是,Express 中间件并非洋葱模型,是单向调用。 + + + + +### 路由中间件 + + +我们可以把上面编写的中间件应用到单个 Controller 上,也可以将中间件应用到单个路由上。 + + +```typescript +import { Controller, Get, Provide } from '@midwayjs/core'; + +@Controller('/', { middleware: [ ReportMiddleware ]}) // controller 级别的中间件 +export class HomeController { + + @Get('/', { middleware: [ ReportMiddleware ]}) // 路由级别的中间件 + async home() { + return 'hello world' + } +} +``` + + +### 全局中间件 + + +直接使用 Midway 提供的 `app.generateMiddleware` 方法,在入口处加载全局中间件。 +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle } from '@midwayjs/core'; +import * as express from '@midwayjs/express'; +import { ReportMiddleware } from './middleware/report.middleware.ts' + +@Configuration({ + imports: [express], +}) +export class MainConfiguration implements ILifeCycle { + + @App() + app: express.Application; + + async onReady() { + this.app.useMiddleware(ReportMiddleware); + } +} +``` + + +除了加载 Class 形式的中间件外,也支持加载传统的 Express 中间件。 +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, App } from '@midwayjs/core'; +import * as express from '@midwayjs/express'; +import { join } from 'path'; + +@Configuration({ + imports: [express], +}) +export class MainConfiguration implements ILifeCycle { + + @App() + app: express.Application; + + async onReady() { + this.app.useMiddleware((req, res, next) => { + // xxx + }); + } +} +``` +你可以通过注入 `app` 对象,来调用到所有 Express 上的方法。 + + + +## 返回统一处理 + +由于 Express 中间件是单向调用,无法在返回时执行,为此我们额外设计了一个 `@Match` 装饰的过滤器,用于处理返回值的行为。 + +比如,我们可以定义针对全局返回的过滤器。 + +```typescript +// src/filter/globalMatch.filter.ts +import { Match } from '@midwayjs/core'; +import { Context, Response } from '@midwayjs/express'; + +@Match() +export class GlobalMatchFilter { + match(value, req, res) { + // ... + return { + status: 200, + data: { + value + }, + }; + } +} +``` + +也可以匹配特定的路由做返回。 + +```typescript +// src/filter/api.filter.ts +import { Match } from '@midwayjs/core'; +import { Context, Response } from '@midwayjs/express'; + +@Match((ctx: Context, res: Response) => { + return ctx.path === '/api'; +}) +export class APIMatchFilter { + match(value, req: Context, res: Response) { + // ... + return { + data: { + message: + data: value, + }, + }; + } +} +``` + +需要应用到 app 中。 + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as express from '@midwayjs/express'; +import { join } from 'path'; +import { APIMatchFilter } from './filter/api.filter'; +import { GlobalMatchFilter } from 'filter/globalMatch.filter'; + +@Configuration({ + imports: [express], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration { + @App() + app: express.Application; + + async onReady() { + // ... + this.app.useFilter([APIMatchFilter, GlobalMatchFilter]); + } +} +``` + +注意,这类过滤器是按照添加的顺序来匹配执行。 + + + +## 错误处理 + +和普通的项目相同,使用错误过滤器,但是参数略有不同。 + +```typescript +import { Catch } from '@midwayjs/core'; +import { Context, Response } from '@midwayjs/express'; + +@Catch() +export class GlobalError { + catch(err: Error, req: Context, res: Response) { + if (err) { + return { + status: err.status ?? 500, + message: err.message, + } + } + } +} +``` + +需要应用到 app 中。 + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as express from '@midwayjs/express'; +import { join } from 'path'; +import { GlobalError } from './filter/global.filter'; + +@Configuration({ + imports: [express], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration { + @App() + app: express.Application; + + async onReady() { + this.app.useMiddleware((req, res, next) => { + next(); + }); + + this.app.useFilter([GlobalError]); + } +} +``` + +注意,`@Match` 和 `@Catch` 都是过滤器,在内部会自动判断做区分执行。。 + + + +## Cookie + +`@midwayjs/express` 自带 `cookie parser` 功能,使用的是 `cookie-parser` 模块。 + +针对 Cookie,统一使用 `keys` 作为秘钥。 + +```typescript +// src/config/config.default +export default { + keys: ['key1', 'key2'], +} +``` + +获取 Cookie。 + +```typescript +const cookieValue = req.cookies['cookie-key']; +``` + +设置 Cookie。 + +```typescript +res.cookie( + 'cookie-key', + 'cookie-value', + cookieOptions +); +``` + + + +## Session + +`@midwayjs/express` 内置了 Session 组件,给我们提供了 `ctx.session` 来访问或者修改当前用户 Session 。 + +默认情况下为 `cookie-session` ,默认配置如下。 + +```typescript +// src/config/config.default +export default { + session: { + name: 'MW_SESS', + resave: true, + saveUninitialized: true, + cookie: { + maxAge: 24 * 3600 * 1000, // ms + httpOnly: true, + // sameSite: null, + }, + } +} +``` + +我们可以通过简单的 API 来设置 session。 + +```typescript +@Controller('/') +export class HomeController { + + @Inject() + req; + + @Get('/') + async get() { + // set all + this.req.session = req.query; + + // set value + this.req.session.key = 'abc'; + + // get + const key = this.req.session.key; + + // remove + this.req.session = null; + + // set max age + this.req.session.maxAge = Number(req.query.maxAge); + + // ... + } +} + +``` + + + +## BodyParser + +`@midwayjs/express` 自带 `bodyParser` 功能,默认会解析 `Post` 请求,自动识别 `json` 、`text`和 `urlencoded` 类型。 + +默认的大小限制为 `1mb`,可以单独对每项配置大小。 + +```typescript +// src/config/config.default +export default { + // ... + bodyParser: { + json: { + enable: true, + limit: '1mb', + strict: true, + }, + raw: { + enable: false, + limit: '1mb', + }, + text: { + enable: true, + limit: '1mb', + }, + urlencoded: { + enable: true, + extended: false, + limit: '1mb', + parameterLimit: 1000, + }, + }, +} +``` + + + +## 配置 + +### 默认配置 + +`@midwayjs/express` 的配置样例如下: + +```typescript +// src/config/config.default +export default { + // ... + express: { + port: 7001, + }, +} +``` + +所有属性描述如下: + +| 属性 | 类型 | 描述 | +| ------------ |--------------------------------------------| ------------------------------------------------------- | +| port | number | 可选,启动的端口 | +| globalPrefix | string | 可选,全局的 http 前缀 | +| keys | string[] | 可选,Cookies 签名,如果上层未写 keys,也可以在这里设置 | +| hostname | string | 可选,监听的 hostname,默认 127.1 | +| key | string \| Buffer \| Array\ | 可选,Https key,服务端私钥 | +| cert | string \| Buffer \| Array\ | 可选,Https cert,服务端证书 | +| ca | string \| Buffer \| Array\ | 可选,Https ca | +| http2 | boolean | 可选,http2 支持,默认 false | + + + +### 修改端口 + +默认情况下,我们在 `config.default` 提供了 `7001` 的默认端口参数,修改它就可以修改 Express http 服务的默认端口。 + +比如我们修改为 `6001`: + +```typescript +// src/config/config.default +export default { + // ... + express: { + port: 6001, + }, +} +``` + +默认情况下,单测环境由于需要 supertest 来启动端口,我们的 port 配置为 `null`。 + +```typescript +// src/config/config.unittest +export default { + // ... + express: { + port: null, + }, +} +``` + +此外,也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口,此方法会覆盖配置中的端口。 + + + +### 全局前缀 + +此功能请参考 [全局前缀](../controller#全局路由前缀)。 + + + +### Https 配置 + +在大多数的情况,请尽可能使用外部代理的方式来完成 Https 的实现,比如 Nginx。 + +在一些特殊场景下,你可以通过配置 SSL 证书(TLS 证书)的方式,来直接开启 Https。 + +首先,你需要提前准备好证书文件,比如 `ssl.key` 和 `ssl.pem`,key 为服务端私钥,pem 为对应的证书。 + +然后配置即可。 + +```typescript +// src/config/config.default +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export default { + // ... + express: { + key: join(__dirname, '../ssl/ssl.key'), + cert: join(__dirname, '../ssl/ssl.pem'), + }, +} +``` + + + +### 修改上下文日志 + +可以单独修改 express 框架的上下文日志。 + +```typescript +export default { + express: { + contextLoggerFormat: info => { + // 等价 req + const req = info.ctx; + const userId = req?.['session']?.['userId'] || '-'; + return `${info.timestamp} ${info.LEVEL} ${info.pid} [${userId} - ${Date.now() - req.startTime}ms ${req.method}] ${info.message}`; + } + // ... + }, +}; +``` + diff --git a/site/versioned_docs/version-3.0.0/extensions/grpc.md b/site/versioned_docs/version-3.0.0/extensions/grpc.md new file mode 100644 index 000000000000..208777a5208e --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/grpc.md @@ -0,0 +1,1114 @@ +# gRPC + +gRPC 是一个高性能、通用的开源 RPC 框架,其由 Google 主要面向移动应用开发并基于 HTTP/2 协议标准而设计,基于 ProtoBuf(Protocol Buffers) 序列化协议开发,且支持众多开发语言。 + + +本篇内容演示了如何在 Midway 体系下,提供 gRPC 服务,以及调用 gRPC 服务的方法。 + + +Midway 当前采用了最新的 gRPC 官方推荐的 [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) 进行开发,并提供了一些工具包,用于快速发布服务和调用服务。 + +我们使用的模块为 `@midwayjs/grpc` ,既可以独立发布服务,又可以接入其它框架调用 gRPC 服务。 + +相关信息: + +**提供服务** + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ❌ | +| 可用于一体化 | ✅ | + +**调用服务** + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | + +**其他** + +| 描述 | | +| -------------------- | ---- | +| 可作为主框架独立使用 | ✅ | +| 可独立添加中间件 | ✅ | + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/grpc@3 --save +$ npm i @midwayjs/grpc-helper --save-dev +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/grpc": "^3.0.0", + // ... + }, + "devDependencies": { + "@midwayjs/grpc-helper": "^1.0.0", + // ... + } +} +``` + + + +## 开启组件 + +:::tip + +不管是提供服务还是调用服务,都需要开启组件。 + +::: + +`@midwayjs/grpc` 可以作为独立主框架使用。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as grpc from '@midwayjs/grpc'; + +@Configuration({ + imports: [grpc], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + +``` + +也可以附加在其他的主框架下,比如 `@midwayjs/koa` 。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as grpc from '@midwayjs/grpc'; + +@Configuration({ + imports: [koa, grpc], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + +``` + + + +## 目录结构 + +大致的目录结构如下,`src/provider` 是提供 gRPC 服务的目录。 + +``` +. +├── package.json +├── proto ## proto 定义文件 +│ └── helloworld.proto +├── src +│ ├── configuration.ts ## 入口配置文件 +│ ├── interface.ts +│ └── provider ## gRPC 提供服务的文件 +│ └── greeter.ts +├── test +├── bootstrap.js ## 服务启动入口 +└── tsconfig.json +``` + + + +## 定义服务接口 + + +在微服务中,定义一个服务需要特定的接口定义语言(IDL)来完成,在 gRPC中 默认使用 Protocol Buffers 作为序列化协议。 + + +序列化协议独立于语言和平台,提供了多种语言的实现,Java,C++,Go 等等,每一种实现都包含了相应语言的编译器和库文件。所以 gRPC 是一个提供和调用都可以跨语言的服务框架。 + +一个gRPC服务的大体架构可以用官网上的一幅图表示。 + +![](https://img.alicdn.com/imgextra/i3/O1CN01kpIyg51k8i5DtcGpZ_!!6000000004639-2-tps-621-445.png) + + +Protocol Buffers 协议的文件,默认的后缀为 `.proto` 。.proto后缀的IDL文件,并通过其编译器生成特定语言的数据结构、服务端接口和客户端Stub代码。 + + +:::info +由于 proto 文件可以跨语言使用,为了方便共享,我们一般将 proto 文件放在 src 目录外侧,方便其他工具复制分发。 +::: + + +下面是一个基础的 `proto/helloworld.proto` 文件。 +```protobuf +syntax = "proto3"; + +package helloworld; + +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} + +``` + + +proto3 表示的是第三版的 protobuf 协议,是 gRPC 目前推荐的版本,“语法简单,功能更全”。 + + +我们可以用 `service` 格式,定义服务体,其中可以包含方法。同时,我们可以更加细致的通过 `message` 描述服务具体的请求参数和响应参数。 + + +我们可以从 [Google 的官网文档](https://developers.google.com/protocol-buffers/docs/overview#simple) 中查看更多细节。 + + +:::info +大家会看到,这和 Java 中的 Class 非常相像,每个结构就相当于 Java 中的一个类。 +::: + + +### 编写 proto 文件 + + +现在我们再来看之前的服务,是不是就很好理解了。 +```protobuf +syntax = "proto3"; + +package helloworld; + +// 服务的定义 +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// 服务的请求参数 +message HelloRequest { + string name = 1; +} + +// 服务的响应参数 +message HelloReply { + string message = 1; +} + +``` + + +我们定义了一个名为 `Greeter` 的服务,包含一个 `HelloRequest` 结构的请求体,以及返回 `HelloReply` 结构的响应体。 + + +接下去,我们将对这个服务给大家做演示。 + + +### 生成代码定义 + + +传统的 gRPC 框架,需要用户手动编写 proto 文件,以及生成 js 服务,最后再根据 js 生成的服务再编写实现,在 Midway 体系下,我们提供了一个 grpc-helper 工具包来加速这个过程。 + + +如果没有安装,可以先安装。 +```bash +$ npm i @midwayjs/grpc-helper --save-dev +``` + + +grpc-helper 工具的作用,是将用户提供的 proto 文件,生成对应可读的 ts interface 文件。 + + +我们可以添加一个脚本,方便这个过程。 +```json +{ + "scripts": { + "generate": "tsproto --path proto --output src/domain" + } +} +``` + + +然后执行 `npm run generate` 。 + + +上述命令执行后,会在代码的 `src/domain` 目录中生成 proto 文件对应的服务接口定义。 + + +:::info +不管是提供 gRPC 服务还是调用 gRPC 服务,都要先生成定义。 +::: + + +生成的代码如下,包含有一个命名空间(namespace),以及命名空间下的两个 TypeScript Interface, `Greeter` 用于编写服务端实现, `GreeterClient` 用于编写客户端实现。 +```typescript +/** +* This file is auto-generated by grpc-helper +*/ + +import * as grpc from '@midwayjs/grpc'; + +// 生成的命名空间 +export namespace helloworld { + + // 服务端使用的定义 + export interface Greeter { + // Sends a greeting + sayHello(data: HelloRequest): Promise; + } + + // 客户端使用的定义 + export interface GreeterClient { + // Sends a greeting + sayHello(options?: grpc.IClientOptions): grpc.IClientUnaryService; + } + + // 请求体结构 + export interface HelloRequest { + name?: string; + } + + // 响应体结构 + export interface HelloReply { + message?: string; + } +} + +``` + + +:::info +每当 proto 文件被修改时,就需要重新生成对应的服务定义,然后将对应的方法实现。 +::: + + + +## 提供 gRPC 服务(Provider) + + +### 编写服务提供方(Provider) + + +在 `src/provider` 目录中,我们创建 `greeter.ts` ,内容如下 +```typescript +import { + MSProviderType, + Provider, + GrpcMethod, +} from '@midwayjs/core'; +import { helloworld } from '../domain/helloworld'; + +/** + * 实现 helloworld.Greeter 接口的服务 + */ +@Provider(MSProviderType.GRPC, { package: 'helloworld' }) +export class Greeter implements helloworld.Greeter { + + @GrpcMethod() + async sayHello(request: helloworld.HelloRequest) { + return { message: 'Hello ' + request.name }; + } +} + +``` +:::info +注意,@Provider 装饰器和 @Provide 装饰器不同,前者用于提供服务,后者用于依赖注入容器扫描标识的类。 +::: + + +我们使用 `@Provider` 暴露出一个 RPC 服务, `@Provider` 的第一个参数为 RPC 服务类型,这个参数是个枚举,这里选择 GRPC 类型。 + + +`@Provider` 的第二个参数为 RPC 服务的元数据,这里指代的是 gRPC 服务的元数据。这里需要写入 gRPC 的 package 字段,即 proto 文件中的 package 字段(这里的字段用于和 proto 文件加载后的字段做对应)。 + + +对于普通的 gRPC 服务接口(UnaryCall),我们只需要使用 `@GrpcMethod()` 装饰器修饰即可。修饰的方法即为服务定义本身,入参为 proto 中定义好的入参,return 值即为定义好的响应体。 + +:::info +注意,生成的 Interface 是为了更好的编写服务代码,规范结构,请务必按照定义编写。 +::: + + +### 配置服务 + + +配置内容如下。 +```typescript +// src/config/config.default +import { MidwayAppInfo, MidwayConfig } from '@midwayjs/core'; + +export default (appInfo: MidwayAppInfo): MidwayConfig => { + return { + // ... + grpcServer: { + services: [ + { + protoPath: join(appInfo.appDir, 'proto/hero.proto'), + package: 'hero', + }, + { + protoPath: join(appInfo.appDir, 'proto/helloworld.proto'), + package: 'helloworld', + } + ], + } + }; +} +``` +services 字段是数组,意味着 Midway 项目可以同时发布多个 gRPC 服务。每个 service 的结构为: + +| 属性 | 类型 | 描述 | +| --- | --- | --- | +| protoPath | string | 必选,proto 文件的绝对路径 | +| package | string | 必选,服务对应的 package | + +除了 Service 配置之外,还有一些其他的配置。 + +| 属性 | 类型 | 描述 | +| ------------- | ----------------- | ------------------------------------------------------------ | +| url | string | 可选,gRPC 服务地址,默认 6565 端口,比如 'localhost:6565' | +| loaderOptions | Object | 可选,proto file loader 的 options | +| credentials | ServerCredentials | 可选,grpc Server binding 时的 credentials 参数选项 | +| serverOptions | ChannelOptions | 可选,grpc Server 的 [自定义 options](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js#supported-channel-options) | + + + +### 提供安全证书 + +可以通过 `credentials` 参数传递安全证书。 + +```typescript +// src/config/config.default +import { MidwayAppInfo, MidwayConfig } from '@midwayjs/core'; +import { ServerCredentials } from '@midwayjs/grpc'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +const cert = readFileSync(join(__dirname, './cert/server.crt')); +const pem = readFileSync(join(__dirname, './cert/server.pem')); +const key = readFileSync(join(__dirname, './cert/server.key')); + +export default (appInfo: MidwayAppInfo): MidwayConfig => { + return { + // ... + grpcServer: { + // ... + credentials: ServerCredentials.createSsl(cert, [{ private_key: key, cert_chain: pem }]); + } + }; +} +``` + + + +### 编写单元测试 + +`@midwayjs/grpc` 库提供了一个 `createGRPCConsumer` 方法,用于实时调用客户端,一般我们用这个方法做测试。 + +:::caution +这个方法每次调用会实时连接,不建议将该方法用在生产环境。 +::: + + +在测试中写法如下。 +```typescript +import { createApp, close } from '@midwayjs/mock'; +import { Framework, createGRPCConsumer } from '@midwayjs/grpc'; +import { join } from 'path'; +import { helloworld } from '../src/domain/helloworld'; + +describe('test/index.test.ts', () => { + + it('should create multiple grpc service in one server', async () => { + const baseDir = join(__dirname, '../'); + + // 创建服务 + const app = await createApp(); + + // 调用服务 + const service = await createGRPCConsumer({ + package: 'helloworld', + protoPath: join(baseDir, 'proto', 'helloworld.proto'), + url: 'localhost:6565' + }); + + const result = await service.sayHello().sendMessage({ + name: 'harry' + }); + + expect(result.message).toEqual('Hello harry'); + await close(app); + }); + +}); + +``` + + + +## 调用 gRPC 服务(Consumer) + + +我们编写一个 gRPC 服务来调用上面的暴露的服务。 + +:::info +事实上,你可以在 Web 的 Controller,或者 Service 等其他地方来调用,这里只是做一个示例。 +::: + + +### 调用配置 + + +你需要在 `src/config/config.default.ts` 中增加你需要调用的目标服务以及它的 proto 文件信息。 + + +比如,这里我们填写了上面暴露的服务本身,以及该服务的 proto,包名等信息(函数形式)。 +```typescript +// src/config/config.default +import { MidwayAppInfo, MidwayConfig } from '@midwayjs/core'; + +export default (appInfo: MidwayAppInfo): MidwayConfig => { + return { + // ... + grpc: { + services: [ + { + url: 'localhost:6565', + protoPath: join(appInfo.appDir, 'proto/helloworld.proto'), + package: 'helloworld', + }, + ], + }, + }; +} +``` + + +### 代码调用 + + +配置完后,我们就可以在代码里调用了。 + + +`@midwayjs/grpc` 提供了 `clients` ,可以方便的获取到已配置的服务。我们只需要在需要注入的地方,注入这个对象即可。 + + +比如: +```typescript +import { + Provide, + Inject, +} from '@midwayjs/core'; +import { helloworld, hero } from '../interface'; +import { Clients } from '@midwayjs/grpc'; + +@Provide() +export class UserService { + @Inject() + grpcClients: Clients; + +} +``` + + +我们通过 `clients` 获取到对方服务的客户端实例,然后调用即可。 + + +```typescript +import { + Provide, + Inject, +} from '@midwayjs/core'; +import { helloworld, hero } from '../interface'; +import { Clients } from '@midwayjs/grpc'; + +@Provide() +export class UserService { + @Inject() + grpcClients: Clients; + + async invoke() { + // 获取服务 + const greeterService = this.grpcClients.getService( + 'helloworld.Greeter' + ); + + // 调用服务 + const result = await greeterService.sayHello() + .sendMessage({ + name: 'harry' + }); + + // 返回结果 + return result; + } + +} +``` + + +我们也可以利用 `@Init` 装饰器,将需要调用的服务缓存到属性上。这样可以在其他方法调用时复用。 + + +示例如下。 +```typescript +import { + GrpcMethod, + MSProviderType, + Provider, + Inject, + Init, +} from '@midwayjs/core'; +import { helloworld, hero } from '../interface'; +import { Clients } from '@midwayjs/grpc'; + +@Provider(MSProviderType.GRPC, { package: 'hero' }) +export class HeroService implements hero.HeroService { + // 注入客户端 + @Inject() + grpcClients: Clients; + + greeterService: helloworld.GreeterClient; + + @Init() + async init() { + // 赋值一个服务实例 + this.greeterService = this.grpcClients.getService( + 'helloworld.Greeter' + ); + } + + @GrpcMethod() + async findOne(data) { + // 调用服务 + const result = await greeterService.sayHello() + .sendMessage({ + name: 'harry' + }); + + // 返回结果 + return result; + } +} + +``` + + +## 流式服务 + + +gRPC 的流式服务用于减少连接,让服务端或者客户端不需要等待即可执行任务,从而提高执行效率。 + + +gRPC 的流式服务分为三种,以服务端角度来说,为 + + +- 服务端接收流(客户端推) +- 服务端响应流(服务端推) +- 双向流 + + + +下面我们将一一介绍。 + + +### 流式 proto 文件 + + +流式的 proto 文件写法不同,需要在希望使用流式的地方将参数标记为 `stream` 。 +```protobuf + +syntax = "proto3"; + +package math; + +message AddArgs { + int32 id = 1; + int32 num = 2; +} + +message Num { + int32 id = 1; + int32 num = 2; +} + +service Math { + rpc Add (AddArgs) returns (Num) { + } + + // 双向流 + rpc AddMore (stream AddArgs) returns (stream Num) { + } + + // 服务端往客户端推 + rpc SumMany (AddArgs) returns (stream Num) { + } + + // 客户端往服务端推 + rpc AddMany (stream AddArgs) returns (Num) { + } +} + +``` +该服务生成的接口定义为: + + +```typescript +import { + IClientDuplexStreamService, + IClientReadableStreamService, + IClientUnaryService, + IClientWritableStreamService, + IClientOptions, +} from '@midwayjs/grpc'; + +export namespace math { + export interface AddArgs { + id?: number; + num?: number; + } + export interface Num { + id?: number; + num?: number; + } + + /** + * server interface + */ + export interface Math { + add(data: AddArgs): Promise; + addMore(data: AddArgs): Promise; + // 服务端推,客户端读 + sumMany(data: AddArgs): Promise + // 客户端端推,服务端读 + addMany(num: AddArgs): Promise; + } + + /** + * client interface + */ + export interface MathClient { + add(options?: IClientOptions): IClientUnaryService; + addMore(options?: IClientOptions): IClientDuplexStreamService; + // 服务端推,客户端读 + sumMany(options?: IClientOptions): IClientReadableStreamService; + // 客户端端推,服务端读 + addMany(options?: IClientOptions): IClientWritableStreamService; + } +} + +``` + + +### 服务端推送 + + +客户端调用一次,服务端可以多次返回。通过 `@GrpcMethod()` 的参数来标识流式类型。 + + +可用的类型为: + + +- `GrpcStreamTypeEnum.WRITEABLE` 服务端输出流(单工) +- `GrpcStreamTypeEnum.READABLE` 客户端输出流(单工),服务端接受多次 +- `GrpcStreamTypeEnum.DUPLEX` 双工流 + + + +服务端示例如下: +```typescript +import { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core'; +import { Context, Metadata } from '@midwayjs/grpc'; +import { math } from '../interface'; + +/** + */ +@Provider(MSProviderType.GRPC, { package: 'math' }) +export class Math implements math.Math { + + @Inject() + ctx: Context; + + @GrpcMethod({type: GrpcStreamTypeEnum.WRITEABLE }) + async sumMany(args: math.AddArgs) { + this.ctx.write({ + num: 1 + args.num + }); + this.ctx.write({ + num: 2 + args.num + }); + this.ctx.write({ + num: 3 + args.num + }); + + this.ctx.end(); + } + + // ... +} + +``` +服务端使用 `ctx.write` 方法来返回数据,由于是服务端流,可以返回多次。 + + +返回结束后,请使用 `ctx.end()` 方法关闭流。 + + +客户端,调用一次,接受多次数据。 + + +比如下面的累加逻辑。 + + +Promise 写法,会等待服务端数据都返回再做处理。 +```typescript +// 服务端推送 +let total = 0; +let result = await service.sumMany().sendMessage({ + num: 1, +}); + +result.forEach(data => { + total += data.num; +}); + +// total = 9; +``` + + +事件写法,实时处理。 +```typescript +// 服务端推送 +let call = service.sumMany().getCall(); + +call.on('data', data => { + // do something +}); + +call.sendMessage({ + num: 1, +}); + +``` + + +### 客户端推送 + + +客户端调用多次,服务端接收多次数据,返回一个结果。通过 `@GrpcMethod({type: GrpcStreamTypeEnum.READABLE})` 的参数来标识流式类型。 + + +服务端示例如下: +```typescript +import { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core'; +import { Context, Metadata } from '@midwayjs/grpc'; +import { math } from '../interface'; + +/** + */ +@Provider(MSProviderType.GRPC, { package: 'math' }) +export class Math implements math.Math { + + sumDataList: number[] = []; + + @Inject() + ctx: Context; + + @GrpcMethod({type: GrpcStreamTypeEnum.READABLE, onEnd: 'sumEnd' }) + async addMany(data: math.Num) { + this.sumDataList.push(data); + } + + async sumEnd(): Promise { + const total = this.sumDataList.reduce((pre, cur) => { + return { + num: pre.num + cur.num, + } + }); + return total; + } + + // ... +} + +``` + + +客户端每次调用,都会触发一次 `addMany` 方法。 + + +在客户端发送 `end` 事件之后,会调用 `@GrpcMethod` 装饰器上的 `onEnd` 参数指定的方法,该方法的返回值即为最后客户端拿到的值。 + + +客户端示例如下: +```typescript +// 客户端推送 +const data = await service.addMany() +.sendMessage({num: 1}) +.sendMessage({num: 2}) +.sendMessage({num: 3}) +.end(); + +// data.num = 6 +``` + + +### 双向流 + + +客户端可以调用多次,服务端也可以接收多次数据,返回多个结果,类似于传统的 TCP 通信。通过 `@GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX})` 的参数来标识双工流式类型。 + + +服务端示例如下: +```typescript +import { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core'; +import { Context, Metadata } from '@midwayjs/grpc'; +import { math } from '../interface'; + +/** + */ +@Provider(MSProviderType.GRPC, { package: 'math' }) +export class Math implements math.Math { + + @Inject() + ctx: Context; + + @GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX, onEnd: 'duplexEnd' }) + async addMore(message: math.AddArgs) { + this.ctx.write({ + id: message.id, + num: message.num + 10, + }); + } + + async duplexEnd() { + console.log('got client end message'); + } + // ... +} + +``` +服务端可以随时使用 `ctx.write` 返回数据,也可以使用 `ctx.end` 来关闭流。 + + +客户端示例: + + +对于双工通信的客户端,由于无法保证调用、返回的顺序,我们需要使用监听的模式来消费结果。 +```typescript +const clientStream = service.addMore().getCall(); + +let total = 0; +let idx = 0; + +duplexCall.on('data', (data: math.Num) => { + total += data.num; + idx++; + if (idx === 2) { + duplexCall.end(); + // total => 29 + } +}); + +duplexCall.write({ + num: 3, +}); + +duplexCall.write({ + num: 6, +}); +``` + + +如果希望保证调用顺序,我们也提供了保证顺序的双向流调用方法,但是需要在 proto 中定义一个固定的 id,来确保顺序。 + + +比如我们的 Math.proto,对每个入参和出参,都增加了一个固定的 id,所以可以固定顺序。 +```typescript + +syntax = "proto3"; + +package math; + +message AddArgs { + int32 id = 1; // 这里的 id 名字是固定的 + int32 num = 2; +} + +message Num { + int32 id = 1; // 这里的 id 名字是固定的 + int32 num = 2; +} + +service Math { + rpc Add (AddArgs) returns (Num) { + } + + rpc AddMore (stream AddArgs) returns (stream Num) { + } + + // 服务端往客户端推 + rpc SumMany (AddArgs) returns (stream Num) { + } + + // 客户端往服务端推 + rpc AddMany (stream AddArgs) returns (Num) { + } +} + +``` +固定顺序的客户端调用方式如下: +```typescript +// 保证顺序的双向流 +const t = service.addMore(); + +const result4 = await new Promise((resolve, reject) => { + + let total = 0; + + // 第一次调用和返回 + t.sendMessage({ + num: 2 + }) + .then(res => { + expect(res.num).toEqual(12); + total += res.num; + }) + .catch(err => console.error(err)); + + // 第二次调用和返回 + t.sendMessage({ + num: 5 + }).then(res => { + expect(res.num).toEqual(15); + total += res.num; + resolve(total); + }) + .catch(err => console.error(err)); + + t.end(); +}); + +// result4 => 27 +``` +默认的 id 为 `id` ,如果服务端定义不同,需要修改,可以在客户端调用时传递。 +```typescript +// 保证顺序的双向流 +const t = service.addMore({ + messageKey: 'uid' +}); +``` + + +## 元数据(Metadata) + + +gRPC 的元数据等价于 HTTP 的上下文。 + + +服务端通过 `ctx.sendMetadata` 方法返回元数据,也可以通过 `ctx.metadata` 获取客户端传递的元数据。 +```typescript +import { + MSProviderType, + Provider, + GrpcMethod, +} from '@midwayjs/core'; +import { helloworld } from '../domain/helloworld'; +import { Context, Metadata } from '@midwayjs/grpc'; + +/** + * 实现 helloworld.Greeter 接口的服务 + */ +@Provider(MSProviderType.GRPC, { package: 'helloworld' }) +export class Greeter implements helloworld.Greeter { + + @Inject() + ctx: Context; + + @GrpcMethod() + async sayHello(request: helloworld.HelloRequest) { + + // 客户端传递的元数据 + console.log(this.ctx.metadata); + + // 创建元数据 + const meta = new Metadata(); + this.ctx.metadata.add('xxx', 'bbb'); + this.ctx.sendMetadata(meta); + + return { message: 'Hello ' + request.name }; + } +} +``` + + +客户端通过方法的 options 参数传递元数据。 +```typescript +import { Metadata } from '@midwayjs/grpc'; + +const meta = new Metadata(); +meta.add('key', 'value'); + +const result = await service.sayHello({ + metadata: meta, +}).sendMessage({ + name: 'harry' +}); +``` + + +获取元数据相对麻烦一些。 + + +普通一元调用(UnaryCall)获取元数据需要使用 `sendMessageWithCallback` 方法。 +```typescript +const call = service.sayHello().sendMessageWithCallback({ + name: 'zhangting' +}, (err) => { + if (err) { + reject(err); + } +}); +call.on('metadata', (meta) => { + // output meta +}); +``` +其他流式服务,可以通过 `getCall()` 方法获取原始客户端流对象,从而直接订阅。 +```typescript +// 获取服务,注意,这里没有 await +const call = service.addMany().getCall(); +call.on('metadata', (meta) => { + // output meta +}); +``` + + +## 超时处理 + + +我们可以在调用服务时传递参数,单位毫秒。 +```typescript +const result = await service.sayHello({ + timeout: 5000 +}).sendMessage({ + name: 'harry' +}); +``` diff --git a/site/versioned_docs/version-3.0.0/extensions/http-proxy.md b/site/versioned_docs/version-3.0.0/extensions/http-proxy.md new file mode 100644 index 000000000000..3f819f2b7e73 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/http-proxy.md @@ -0,0 +1,152 @@ +# HTTP 代理 + +适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的 HTTP 请求代理组件,支持 GET、POST 等多种请求方法。 + +相关信息: + +| web 支持情况 | | +| ----------------- | --- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | 💬 | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + +:::caution + +💬 部分函数计算平台不支持流式请求响应,请参考对应平台能力。 + +::: + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/http-proxy@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/http-proxy": "^3.0.0" + // ... + }, + "devDependencies": { + // ... + } +} +``` + +## 启用组件 + +在 `src/configuration.ts` 中引入组件 + +```typescript +// ... +import * as proxy from '@midwayjs/http-proxy'; + +@Configuration({ + imports: [ + // ...other components + proxy, + ], +}) +export class MainConfiguration {} +``` + +## 配置 + +代理配置定义如下: + +```typescript +// 代理配置类型 +export interface HttpProxyConfig { + // 匹配要代理的 URL 正则表达式 + match: RegExp; + // 替换匹配到的链接的 host,将请求代理到此地址 + host?: string; + // 通过正则的表达式捕获组处理代理地址 + target?: string; + // 转发请求超时时间,默认为0不设置超时时间 + proxyTimeout?: number; + // 忽略代理请求转发的 header 中的字段 + ignoreHeaders?: { + [key: string]: boolean; + }; +} +``` + +代理支持单个代理和多个代理。 + +单个代理配置 + +```typescript +// src/config/config.default.ts + +export default { + httpProxy: { + match: /\/tfs\//, + host: 'https://gw.alicdn.com', + }, +}; +``` + +多个代理配置 + +```typescript +// src/config/config.default.ts + +// 代理配置类型 +export default { + httpProxy: { + default: { + // 一些每个策略复用的值,会和底下的策略进行合并 + }, + strategy: { + gw: { + // https://gw.alicdn.com/tfs/TB1.1EzoBBh1e4jSZFhXXcC9VXa-48-48.png + match: /\/tfs\//, + host: 'https://gw.alicdn.com', + }, + g: { + // https://g.alicdn.com/mtb/lib-mtop/2.6.1/mtop.js + match: /\/bdimg\/(.*)$/, + target: 'https://sm.bdimg.com/$1', + }, + httpBin: { + // https://httpbin.org/ + match: /\/httpbin\/(.*)$/, + target: 'https://httpbin.org/$1', + }, + }, + }, +}; +``` + +## 示例:使用 host 配置代理 + +```typescript +export default { + httpProxy: { + match: /\/tfs\//, + host: 'https://gw.alicdn.com', + }, +}; +``` + +当请求您的站点路径为: `https://yourdomain.com/tfs/test.png` 时,`match` 字段配置的正则表达式成功匹配,那么就将原始请求路径中的 `host` 部分 `https://yourdomain.com` 替换为配置的 `https://gw.alicdn.com`,从而发起代理请求到 `https://gw.alicdn.com/tfs/test.png`,并把响应结果返回给请求您站点的用户。 + +## 示例:使用 target 配置代理 + +```typescript +export default { + httpProxy: { + match: /\/httpbin\/(.*)$/, + target: 'https://httpbin.org/$1', + }, +}; +``` + +当请求您的站点路径为: `https://yourdomain.com/httpbin/get?name=midway` 时,`match` 字段配置的正则表达式成功匹配,同时正则的捕获组中有结果 `['get?name=midway']` ,那么就将原始请求路径中的 `$1` 部分替换为捕获组中的第 1 个数据(index: 0)的 `get?name=midway`,从而发起代理请求到 `https://httpbin.org/get?name=midway`,并把响应结果返回给请求您站点的用户。 diff --git a/site/versioned_docs/version-3.0.0/extensions/i18n.md b/site/versioned_docs/version-3.0.0/extensions/i18n.md new file mode 100644 index 000000000000..9833113540cd --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/i18n.md @@ -0,0 +1,635 @@ +# 多语言 + +Midway 提供了多语言组件,让业务可以快速指定不同的语言,展示不同的文案,也可以在 HTTP 场景配合请求参数,请求头等方式来使用。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + +## 安装组件 + +```bash +$ npm i @midwayjs/i18n@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/i18n": "^3.0.0", + // ... + }, +} +``` + + + +## 使用组件 + +将 i18n 组件配置到代码中。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as i18n from '@midwayjs/i18n'; + +@Configuration({ + imports: [ + // ... + i18n + ] +}) +export class MainConfiguration { + //... +} +``` + + + +## 使用 + +组件提供了 `MidwayI18nService` 服务,用于翻译多语言文本。 + +使用 `translate` 方法,传入不同的文本关键字和参数,返回不同语言的文本内容。 + +```typescript +@Controller('/') +export class UserController { + + @Inject() + i18nService: MidwayI18nService; + + @Get('/') + async index(@Query('username') username: string) { + return this.i18nService.translate('HELLO_MESSAGE', { + args: { + username + }, + }); + } +} +``` + + + +## 配置多语言文案 + +你可以在配置文件中直接配置,但是大多数情况下,文案会很多,有时候甚至可能文案在远端服务上,这个时候直接配置就不太现实。 + +一般来说,我们会将文案单独放到某个文案配置目录中,比如 `src/locales` 。 + +以 `src/locale` 这个目录为例,我们举个例子,结构如下: + +```text +. +├── src +│ ├── locales +| │ ├── en_US.json +| │ └── zh_CN.json +│ └── controller +│ └── home.controller.ts +├── package.json +└── tsconfig.json +``` + +这里我们建了两个多语言的文件,`en_US.json` 和 `zh_CN.json`,分别代表英文和中文。 + +文件内容分别如下: + +```json +// src/locales/en_US.json +{ + "hello": "Hello {username}", + "email": "email id", + "login": "login account", + "createdAt": "register date" +} +``` + +```json +// src/locales/zh_CN.json +{ + "hello": "你好 {username}", + "email": "邮箱", + "login": "帐号", + "createdAt": "注册时间" +} +``` + +每行一个字符串对,是一个标准的 JSON 格式内容,也可以使用 js/ts 文件,花括号中是可替换的参数占位。 + +同时,需要在配置中加入这两个 JSON,其中 `default` 是语言的默认分组。 + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // 把你的翻译文本放到这里 + localeTable: { + en_US: { + default: require('../locale/en_US'), + }, + zh_CN: { + default: require('../locale/zh_CN'), + } + }, + } +} +``` + +这样就可以使用了,使用输出如下。 + +```typescript +this.i18nService.translate('hello', { + args: { + username: 'harry', + }, + locale: 'en_US', +}); + +// output: Hello harry. + +this.i18nService.translate('hello', { + args: { + username: 'harry', + }, + locale: 'zh_CN', +}); + +// output: 你好 harry. + +``` + + + +## 多语言文案分组 + +在如下配置中,用户配置的多语言文案在 `default` 分组中。 + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // 把你的翻译文本放到这里 + localeTable: { + en_US: { + default: require('../locale/en_US'), + }, + zh_CN: { + default: require('../locale/zh_CN'), + } + }, + } +} +``` + +这样做的好处是,在其他组件或者业务代码中,我们也可以使用不同的分组名,来添加其他的多语言文案。 + +比如: + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // 把你的翻译文本放到这里 + localeTable: { + en_US: { + default: require('../locale/en_US'), + user: require('../locale/user_en_US'), + }, + zh_CN: { + default: require('../locale/zh_CN'), + user: require('../locale/user_zh_CN'), + } + }, + } +} +``` + +在代码中,如果调用非默认分组,需要指定分组参数。 + +```typescript +this.i18nService.translate('user.hello', { + args: { + username: 'harry', + }, + group: 'user', // 指定其他分组 + locale: 'en_US', +}); + +``` + + + +## 多语言文案格式 + +多语言文本中可以添加参数,参数可以有 `对象` 和 `数组` 两种形式。 + +对象形式如下,使用花括号作为占位符。 + +```text +Hello {username} +``` + +使用时,通过配置传递,按对象 key 覆盖变量。 + +```typescript +async index(@Query('username') username: string) { + return this.i18nService.translate('hello', { + args: { + username + }, + }); +} +``` + +数组形式如下,使用数字作为占位符。 + +```text +Hello {0} +``` + +使用时,通过配置传递,格式是数组形式,按数组顺序覆盖数字变量。 + +```typescript +async index(@Query('username') username: string) { + return this.i18nService.translate('hello', { + args: [username] + }); +} +``` + + + +## 动态添加多语言文案 + +有时候,多语言文案可能放在远端,比如数据库等,我们可以通过 `addLocale` 方法进行动态添加。 + +比如,在配置加载后,代码使用前。 + +```typescript +// configuration.ts + +// ... +@Configuration({ + imports: [ + koa, + i18n + ] +}) +export class MainConfiguration { + + @Inject() + i18nService: MidwayI18nService; + + async onReady() { + this.i18nService.addLocale('zh_TW', { + hello: '你好,{username} 美麗的世界' + }); + } + + + // ... +} +``` + +在代码中就可以使用。 + +```typescript +async index(@Query('username') username: string) { + return this.i18nService.translate('hello', { + args: [username], + locale: 'zh_TW' + }); +} +``` + + + +## 通过参数指定当前语言 + +一般情况下,默认语言为 `en_US`,用户的浏览器访问一般会自带 `Accept-Language` 头,所以会正确识别语言。比如用中文浏览器访问,就能正常显示中文。 + +除此之外,在 HTTP 场景下可以通过 URL Query,Cookie,Header 来指定语言。 + +优先级从上到下: + +- query: /?locale=en-US +- cookie: locale=zh-TW +- header: Accept-Language: zh-CN,zh;q=0.5 + +当传递了这些参数之后,多语言数据会自动保存到当前用户的 Cookie 中,下次请求会直接用该设定好的语言。 + + + +## 手动设置语言 + +可以通过调用 `saveRequestLocale` 设置当前语言。 + +```typescript +async index() { + // ... + this.i18nService.saveRequestLocale('zh_CN'); +} +``` + +如果开启了 `writeCookie` 配置,设置后会保存到当前用户的 Cookie 中,下次请求会使用该设置。 + + + +## 语言选择优先级 + +这些多种设置语言的方式,有着不同的优先级,如下优先级从高到低: + +- 1、`i18nService.translate` 方法显式指定的语言 +- 2、通过其他装饰器设置的语言,比如 `@Validate` 装饰器的参数(本质是调用了`i18nService.translate` 方法) +- 3、通过 `saveRequestLocale` API 直接设置的当前语言 +- 4、通过浏览器 Query,Cookie,Header 设置的语言(本质是调用了 `saveRequestLocale`) +- 5、i18n 组件配置中的默认语言 + + + +## 关于语言大小写 + +在代码内部,我们会将所有的多语言,fallback 规则,写入的文本串,返回的 locale 结果,使用下面的规则替代 + +- 1、使用中划线代替下划线 +- 2、使用小写代替大写 + +即所有的 `en_US` 都会变成 `en-us`,`zh_CN` 会变成 `zh-cn`。 + +这样做会安全的适配 URL 和 Cookie。 + + + +## View 中使用 + +在 Web 类型的框架中,我们默认添加了 locals 变量支持,可以在模板引擎中使用。 + +假设我们使用的模板引擎是 [Nunjucks](./render),可以直接引用到 `i18n` 方法。 + +多语言文案如下: + +```json +{ + "hello": "Hello {username}", +} +``` + +模板如下: + +```html +{{ i18n('hello', user) }} +``` + +示例如下: + +```typescript +// ... + +@Controller('/') +export class UserController { + + @Inject() + ctx: Context; + + @Get('/') + async index() { + await this.ctx.render('index', { + // 注意这里是整个对象传递给模板 + user: { + username: 'harry', + } + }); + } +} +``` + +i18n 方法定义如下: + +```typescript +function i18n(templateName: string, args: Record) { + // ... +} +``` + +方法名可以通过配置修改。 + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + localsField: 'i18n', + } +} +``` + + + + + +## 配置 + +### 默认配置 + +大部分情况下,你只需要在配置 `localeTable` 添加你自己的多语言翻译即可。 + +下面是完整的配置,你可以在配置定义中找到。 + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // 默认语言 "en_US" + defaultLocale: 'en_US', + + // 把你的翻译文本放到这里 + localeTable: { + en_US: { + // group name + default: { + // hello: 'hello' + } + }, + zh_CN: { + // group name + default: { + // hello: '你好' + } + }, + }, + + // 语言映射,可以用 * 号通配 + fallbacks: { + // 'en_*': 'en_US', + // pt: 'pt-BR', + }, + // 是否将请求参数写入 cookie + writeCookie: true, + resolver: { + // url query 参数,默认是 "locale" + queryField: 'locale', + cookieField: { + // Cookie 里的 key,默认是 "locale" + fieldName: 'locale', + // Cookie 域名,默认为空,代表当前域名有效 + cookieDomain: '', + // Cookie 默认的过期时间,默认一年 + cookieMaxAge: FORMAT.MS.ONE_YEAR, + }, + }, + localsField: 'i18n', + } +} +``` + + + +### 回写 Cookie + +默认情况下,多语言组件会将当前用户的语言回写到 Cookie 中,避免下次请求再进行查找以提高性能,我们可以通过配置关闭这个行为。 + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + writeCookie: false, + } +} +``` + + + +### 请求解析配置 + +HTTP 场景下,我们提供了通过参数指定当前语言的能力。 + +默认情况下,组件通过下面的字段来查找。 + +- query 的 `locale` 字段 +- cookie 的 `locale` 字段 +- header 的 `Accept-Language` 部分 + +我们可以通过配置修改查询的字段。 + +比如,修改 Query 的字段。 + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + resolver: { + queryField: 'abc' + }, + } +} +``` + +我们就可以通过 `/?abc=en-US` 来请求修改语言。 + +如果不希望通过请求来设置语言,可以将整个 `resolver` 解析关闭,对 Cookie 的回写也将同时停止。 + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + resolver: false, + } +} +``` + + + + + +## 常用语言 + +| 语言 | 语言包名 | +| :--------------- | :------- | +| 阿拉伯 | ar_EG | +| 亞美尼亞 | hy_AM | +| 保加利亚语 | bg_BG | +| 加泰罗尼亚语 | ca_ES | +| 捷克语 | cs_CZ | +| 丹麦语 | da_DK | +| 德语 | de_DE | +| 希腊语 | el_GR | +| 英语 | en_GB | +| 英语(美式) | en_US | +| 西班牙语 | es_ES | +| 爱沙尼亚语 | et_EE | +| 波斯语 | fa_IR | +| 芬兰语 | fi_FI | +| 法语(比利时) | fr_BE | +| 法语 | fr_FR | +| 希伯来语 | he_IL | +| 印地语 | hi_IN | +| 克罗地亚语 | hr_HR | +| 匈牙利 | hu_HU | +| 冰岛语 | is_IS | +| 印度尼西亚语 | id_ID | +| 意大利语 | it_IT | +| 日语 | ja_JP | +| 格鲁吉亚语 | ka_GE | +| 卡纳达语 | kn_IN | +| 韩语/朝鲜语 | ko_KR | +| 库尔德语 | ku_IQ | +| 拉脱维亚语 | lv_LV | +| 马来语 | ms_MY | +| 蒙古语 | mn_MN | +| 挪威 | nb_NO | +| 尼泊尔语 | ne_NP | +| 荷兰语(比利时) | nl_BE | +| 荷兰语 | nl_NL | +| 波兰语 | pl_PL | +| 葡萄牙语(巴西) | pt_BR | +| 葡萄牙语 | pt_PT | +| 斯洛伐克语 | sk_SK | +| 塞尔维亚 | sr_RS | +| 斯洛文尼亚 | sl_SI | +| 瑞典语 | sv_SE | +| 泰米尔语 | ta_IN | +| 泰语 | th_TH | +| 土耳其语 | tr_TR | +| 罗马尼亚语 | ro_RO | +| 俄罗斯语 | ru_RU | +| 乌克兰语 | uk_UA | +| 越南语 | vi_VN | +| 简体中文 | zh_CN | +| 繁体中文 | zh_TW | + + + +## 常见问题 + +### 1、测试配置全局语言不生效 + +一般场景下,你 **无需** 配置全局语言,因为浏览器访问会自动带上语言信息,比如中文浏览器自动返回中文,英文浏览器自动返回英文。 + +假如你明确希望测试全局设置的效果,请务必按下面操作: + +* 1、如果使用的是浏览器,请清空页面 cookie 再访问,因为 cookie 中会记录上一次用户的语言信息 +* 2、如果使用的是 Postman 等工具,请不要带上 cookie 以及语言相关的 Header,Query 等字段 + +### 2、测试返回的语言非预期 + +请使用浏览器进行测试,不要使用 Postman。 + +由于 Postman 请求不会带有浏览器语言相关的 Header,所以服务端无法自动判断语言。 + +如果你一定要使用 Postman,请参考浏览器请求,加上 `Accept-Language` Header。 diff --git a/site/versioned_docs/version-3.0.0/extensions/info.md b/site/versioned_docs/version-3.0.0/extensions/info.md new file mode 100644 index 000000000000..8f0f7ef06bcc --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/info.md @@ -0,0 +1,165 @@ +# 信息查看 + +Midway 提供了 info 组件,用于展示应用的基本信息,方便排查问题。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + +## 安装依赖 + +```bash +$ npm i @midwayjs/info@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/info": "^3.0.0", + // ... + }, +} +``` + + + +## 使用组件 + +将 info 组件配置到代码中。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as info from '@midwayjs/info'; + +@Configuration({ + imports: [ + // ... + info + ] +}) +export class MainConfiguration { + //... +} +``` + +在有些情况下,为了不想让应用信息透出,我们指定在特殊环境下生效。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as info from '@midwayjs/info'; + +@Configuration({ + imports: [ + koa, + { + component: info, + enabledEnvironment: ['local'], // 只在本地启用 + } + ] +}) +export class MainConfiguration { + //... +} +``` + + + +## 查看信息 + +在默认情况下,info 组件会为 Http 场景自动添加一个中间件,我们可以通过 `/_info` 来访问。 + +默认情况下,会展示系统,进程,以及配置等关键信息。 + +效果如下: + +![info](https://img.alicdn.com/imgextra/i3/O1CN01TCkSvr28x8T7gtnCl_!!6000000007998-2-tps-797-1106.png) + + + +## 修改访问路由 + +为了安全,我们可以调整访问的路由。 + +```typescript +// src/config/config.default.ts +export default { + // ... + info: { + infoPath: '/_my_info', + } +} +``` + + + +## 隐藏信息 + +默认情况下,info 组件会隐藏秘钥等信息。我们可以配置增减隐藏的关键字,这个配置会对 **环境变量** 以及 **多环境配置** 生效。 + +关键字可以使用通配符,比如增加一些关键字。 + +```typescript +// src/config/config.default.ts +import { DefaultHiddenKey } from '@midwayjs/info'; + +export default { + // ... + info: { + hiddenKey: DefaultHiddenKey.concat(['*abc', '*def', '*bbb*']), + } +} +``` + + + +## 调用 API + +info 组件默认提供了 `InfoService` 用于在非 Http 或是自定义的场景来使用。 + +比如: + +```typescript +import { Provide } from '@midwayjs/core'; +import { InfoService } from '@midwayjs/info'; + +@Provide() +export class userService { + + @Inject() + inforService: InfoService + + async getInfo() { + // 应用信息,应用名等 + this.inforService.projectInfo(); + // 系统信息 + this.inforService.systemInfo(); + // 堆内存,cpu 等 + this.inforService.resourceOccupationInfo(); + // midway 框架的信息 + this.inforService.softwareInfo(); + // 当前使用的环境配置 + this.inforService.midwayConfig(); + // 依赖注入容器中的服务 + this.inforService.midwayService(); + // 系统时间,时区,启动时常 + this.inforService.timeInfo(); + // 环境变量 + this.inforService.envInfo(); + // 依赖信息 + this.inforService.dependenciesInfo(); + // 网络信息 + this.inforService.networkInfo(); + } +} +``` + diff --git a/site/versioned_docs/version-3.0.0/extensions/jwt.md b/site/versioned_docs/version-3.0.0/extensions/jwt.md new file mode 100644 index 000000000000..e815c4562d2c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/jwt.md @@ -0,0 +1,211 @@ +# JWT + +`JSON Web Token` (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为`JSON`对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。 + +Midway 提供了 jwt 组件,简单提供了一些 jwt 相关的 API,可以基于它做独立的鉴权和校验。 + +相关信息: + +| 描述 | | +| ----------------- | --- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/jwt@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/jwt": "^3.0.0" + // ... + }, +} +``` + +## 使用组件 + +将 jwt 组件配置到代码中。 + +```typescript +import { Configuration, IMidwayContainer } from '@midwayjs/core'; +import { IMidwayContainer } from '@midwayjs/core'; +import * as jwt from '@midwayjs/jwt'; + +@Configuration({ + imports: [ + // ... + jwt, + ], +}) +export class MainConfiguration { + // ... +} +``` + +## 基础配置 + +然后在配置中设置,默认未加密。 + +```typescript +// src/config/config.default.ts +export default { + // ... + jwt: { + secret: 'xxxxxxxxxxxxxx', // fs.readFileSync('xxxxx.key') + sign: { + // signOptions + expiresIn: '2d', // https://github.com/vercel/ms + }, + verify: { + // verifyOptions + }, + decode: { + // decodeOptions + } + }, +}; +``` + +更多配置请查看 ts 定义。 + +## 常用 API + +Midway 将 jwt 常用 API 提供为同步和异步两种形式。 + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { JwtService } from '@midwayjs/jwt'; + +@Provide() +export class UserService { + @Inject() + jwtService: JwtService; + + async invoke() { + // 同步 API + this.jwtService.signSync(payload, secretOrPrivateKey, options); + this.jwtService.verifySync(token, secretOrPublicKey, options); + this.jwtService.decodeSync(token, options); + + // 异步 API + await this.jwtService.sign(payload, secretOrPrivateKey, options); + await this.jwtService.verify(token, secretOrPublicKey, options); + await this.jwtService.decode(token, options); + } +} +``` + +这些 API 都来自于 [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 基础库,如果不了解请阅读原版文档。 + +## 中间件示例 + +一般,jwt 还会配合中间件来完成鉴权,下面是一个自定义 jwt 鉴权的中间件示例。 + +```typescript +// src/middleware/jwt.middleware + +import { Inject, Middleware, httpError } from '@midwayjs/core'; +import { Context, NextFunction } from '@midwayjs/koa'; +import { JwtService } from '@midwayjs/jwt'; + +@Middleware() +export class JwtMiddleware { + @Inject() + jwtService: JwtService; + + public static getName(): string { + return 'jwt'; + } + + resolve() { + return async (ctx: Context, next: NextFunction) => { + // 判断下有没有校验信息 + if (!ctx.headers['authorization']) { + throw new httpError.UnauthorizedError(); + } + // 从 header 上获取校验信息 + const parts = ctx.get('authorization').trim().split(' '); + + if (parts.length !== 2) { + throw new httpError.UnauthorizedError(); + } + + const [scheme, token] = parts; + + if (/^Bearer$/i.test(scheme)) { + try { + //jwt.verify方法验证token是否有效 + await this.jwtService.verify(token, { + complete: true, + }); + } catch (error) { + //token过期 生成新的token + const newToken = getToken(user); + //将新token放入Authorization中返回给前端 + ctx.set('Authorization', newToken); + } + await next(); + } + }; + } + + // 配置忽略鉴权的路由地址 + public match(ctx: Context): boolean { + const ignore = ctx.path.indexOf('/api/admin/login') !== -1; + return !ignore; + } +} +``` + +然后在入口启用中间件即可。 + + +```typescript +// src/configuration.ts + +import { Configuration, App, IMidwayContainer, IMidwayApplication} from '@midwayjs/core'; +import * as jwt from '@midwayjs/jwt'; + +@Configuration({ + imports: [ + // ... + jwt, + ], +}) +export class MainConfiguration { + + @App() + app: IMidwayApplication; + + async onReady(applicationContext: IMidwayContainer): Promise { + // 添加中间件 + this.app.useMiddleware([ + // ... + JwtMiddleware, + ]); + } +} +``` + + + +## 原始 JWT 对象 + +可以通过导出的 `Jwt` 对象引用到原始实例上的对象和方法。 + +```typescript +import { Jwt } from '@midwayjs/jwt'; + +// Jwt.TokenExpiredError +``` diff --git a/site/versioned_docs/version-3.0.0/extensions/kafka.md b/site/versioned_docs/version-3.0.0/extensions/kafka.md new file mode 100644 index 000000000000..a16f959239f1 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/kafka.md @@ -0,0 +1,591 @@ +# Kafka + +在复杂系统的架构中,事件流是很重要的一环,包括从事件源中(数据库、传感器、移动设备等)以事件流的方式去实时捕获数据,持久化事件流方便检索,并实时和回顾操作处理响应事件流。 + +应用于支付和金融交易、实施跟踪和监控汽车等行业信息流动、捕获分析物联网数据等等。 + + +在 Midway中,我们提供了订阅 Kafka 的能力,专门来满足用户的这类需求。 + +相关信息: + +**订阅服务** + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ❌ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ✅ | +| 包含独立日志 | ✅ | + + + +## 基础概念 + + +分布式流处理平台 +* 发布订阅(流)信息 +* 容错(故障转移)存储信息(流),存储事件流 +* 在消息流发生的时候进行处理,处理事件流 + +理解 Producer(生产者) + +* 发布消息到一个主题或多个 topic (主题)。 + +理解 Consumer(主题消费者) +* 订阅一个或者多个 topic,并处理产生的信息。 + +理解 Stream API +* 充当一个流处理器,从 1 个或多个 topic 消费输入流,并生产一个输出流到1个或多个输出 topic,有效地将输入流转换到输出流。 + +理解 Broker +* 已发布的消息保存在一组服务器中,称之为 Kafka 集群。集群中的每一个服务器都是一个代理(Broker)。 消费者可以订阅一个或多个主题(topic),并从Broker拉数据,从而消费这些已发布的消息。 + + +![image.png](https://kafka.apache.org/images/streams-and-tables-p1_p4.png) + +:::tip +从 v3.19 开始,Kafka 组件做了一次重构,Kafka 组件的配置、使用方法和之前都有较大差异,原有使用方式兼容,但是文档不再保留。 +::: + + +## 安装依赖 + + +安装 `@midwayjs/kafka` 模块。 + +```bash +$ npm i @midwayjs/kafka --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/kafka": "^3.0.0", + // ... + } +} +``` + +## 开启组件 + +`@midwayjs/kafka` 可以作为独立主框架使用。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as kafka from '@midwayjs/kafka'; + +@Configuration({ + imports: [ + kafka + ], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} +``` + +也可以附加在其他的主框架下,比如 `@midwayjs/koa` 。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as kafka from '@midwayjs/kafka'; + +@Configuration({ + imports: [ + koa, + kafka + ], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} +``` + +由于 Kafka 分为 **消费者(Consumer)** 和 **生产者(Producer)** 两部分,两个可以独立使用,我们将分别介绍。 + +## 消费者(Consumer) + +### 目录结构 + + +我们一般把消费者放在 consumer 目录。比如 `src/consumer/user.consumer.ts` 。 +``` +➜ my_midway_app tree +. +├── src +│ ├── consumer +│ │ └── user.consumer.ts +│ ├── interface.ts +│ └── service +│ └── user.service.ts +├── test +├── package.json +└── tsconfig.json +``` + + +### 基础配置 + +通过 `consumer` 字段和 `@KafkaConsumer` 装饰器,我们可以配置多个消费者。 + +比如,下面的 `sub1` 和 `sub2` 就是两个不同的消费者。 + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + // ... + }, + sub2: { + // ... + }, + } + } +} +``` + +最简单的消费者配置需要几个字段,Kafka 的连接配置、消费者配置以及订阅配置。 + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + connectionOptions: { + // ... + }, + consumerOptions: { + // ... + }, + subscribeOptions: { + // ... + }, + }, + } + } +} +``` + +比如: + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + connectionOptions: { + clientId: 'my-app', + brokers: ['localhost:9092'], + }, + consumerOptions: { + groupId: 'groupId-test-1', + }, + subscribeOptions: { + topics: ['topic-test-1'], + } + }, + } + } +} +``` + +完整可配置参数包括: + +- `connectionOptions`:Kafka 的连接配置,即 `new Kafka(consumerOptions)` 的参数 +- `consumerOptions`:Kafka 的消费者配置,即 `kafka.consumer(consumerOptions)` 的参数 +- `subscribeOptions`:Kafka 的订阅配置,即 `consumer.subscribe(subscribeOptions)` 的参数 +- `consumerRunConfig`:消费者运行配置,即 `consumer.run(consumerRunConfig)` 的参数 + +这些参数的详细说明,可以参考 [KafkaJS Consumer](https://kafka.js.org/docs/consuming) 文档。 + +### 复用 Kafka 实例 + +如果如果需要复用 Kafka 实例,可以通过 `kafkaInstanceRef` 字段来指定。 + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + connectionOptions: { + clientId: 'my-app', + brokers: ['localhost:9092'], + }, + consumerOptions: { + groupId: 'groupId-test-1', + }, + subscribeOptions: { + topics: ['topic-test-1'], + } + }, + sub2: { + kafkaInstanceRef: 'sub1', + consumerOptions: { + groupId: 'groupId-test-2', + }, + subscribeOptions: { + topics: ['topic-test-2'], + } + } + } + } +} +``` + +注意,上述的 `sub1` 和 `sub2` 是两个不同的消费者,但是它们共享同一个 Kafka 实例,且 `sub2` 的 `groupId` 需要和 `sub1` 不同。 + +用 Kafka SDK 写法类似如下: + +```typescript +const kafka = new Kafka({ + clientId: 'my-app', + brokers: ['localhost:9092'], +}); + +const consumer1 = kafka.consumer({ groupId: 'groupId-test-1' }); +const consumer2 = kafka.consumer({ groupId: 'groupId-test-2' }); +``` + +### 消费者实现 + +我们可以在目录中提供一个标准的消费者实现,比如 `src/consumer/sub1.consumer.ts`。 + +```typescript +// src/consumer/sub1.consumer.ts +import { KafkaConsumer, IKafkaConsumer, EachMessagePayload } from '@midwayjs/kafka'; + +@KafkaConsumer('sub1') +class Sub1Consumer implements IKafkaConsumer { + async eachMessage(payload: EachMessagePayload) { + // ... + } +} +``` + +`sub1` 是消费者名称,使用的是配置中的 `sub1` 消费者。 + +也可以实现 `eachBatch` 方法,处理批量消息。 + +```typescript +// src/consumer/sub1.consumer.ts +import { KafkaConsumer, IKafkaConsumer, EachBatchPayload } from '@midwayjs/kafka'; + +@KafkaConsumer('sub1') +class Sub1Consumer implements IKafkaConsumer { + async eachBatch(payload: EachBatchPayload) { + // ... + } +} +``` + + +### 消息上下文 + + +和其他消息订阅机制一样,消息本身通过 `Context` 字段来传递。 + +```typescript +// src/consumer/sub1.consumer.ts +import { KafkaConsumer, IKafkaConsumer, EachMessagePayload, Context } from '@midwayjs/kafka'; +import { Inject } from '@midwayjs/core'; + +@KafkaConsumer('sub1') +class Sub1Consumer implements IKafkaConsumer { + + @Inject() + ctx: Context; + + async eachMessage(payload: EachMessagePayload) { + // ... + } +} +``` + +`Context` 字段包括几个属性: + +| 属性 | 类型 | 描述 | +| ----------- | ------------------------------ | ---------------- | +| ctx.payload | EachMessagePayload, EachBatchPayload | 消息内容 | +| ctx.consumer | Consumer | 消费者实例 | + + +你可以通过 `ctx.consumer` 来调用 Kafka 的 API,比如 `ctx.consumer.commitOffsets` 来手动提交偏移量或者 `ctx.consumer.pause` 来暂停消费。 + + +## 生产者(Producer) + +### 基础配置 + +服务生产者也需要创建实例,配置本身使用了 [服务工厂](/docs/service_factory) 的设计模式。 + +配置如下: + +```typescript +// src/config/config.default +export default { + kafka: { + producer: { + clients: { + pub1: { + // ... + }, + pub2: { + // ... + } + } + } + } +} +``` + +每个 Producer 实例的配置,同样包括 `connectionOptions` 和 `producerOptions`。 + +```typescript +// src/config/config.default +export default { + kafka: { + producer: { + clients: { + pub1: { + connectionOptions: { + clientId: 'my-app', + brokers: ['localhost:9092'], + }, + producerOptions: { + // ... + } + } + } + } + } +} +``` + +具体参数可以参考 [KafkaJS Producer](https://kafka.js.org/docs/producing) 文档。 + +此外,由于 Kafka Consumer 和 Producer 都可以从同一个 Kafka 实例创建,所以它们可以复用同一个 Kafka 实例。 + +Producer 后于 Consumer 创建,也同样可以使用 `kafkaInstanceRef` 字段来复用 Kafka 实例。 + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + connectionOptions: { + clientId: 'my-app', + brokers: ['localhost:9092'], + }, + } + }, + producer: { + clients: { + pub1: { + kafkaInstanceRef: 'sub1', + } + } + } + } +} +``` + +### 使用 Producer + +Producer 不存在默认实例,由于使用了服务工厂的设计模式,所以可以通过 `@InjectClient()` 来注入。 + + +```typescript +// src/service/user.service.ts +import { Provide, InjectClient } from '@midwayjs/core'; +import { KafkaProducerFactory, Producer } from '@midwayjs/kafka'; + +@Provide() +export class UserService { + + @InjectClient(KafkaProducerFactory, 'pub1') + producer: Producer; + + async invoke() { + await this.producer.send({ + topic: 'topic-test-1', + messages: [{ key: 'message-key1', value: 'hello consumer 11 !' }], + }); + } +} +``` + +## Admin + +Kafka 的 Admin 功能,可以用来创建、删除、查看主题,查看配置和 ACL 等。 + +### 基础配置 + +和 Producer 类似,Admin 也使用了服务工厂的设计模式。 + +```typescript +// src/config/config.default +export default { + kafka: { + admin: { + clients: { + admin1: { + // ... + } + } + } + } +} +``` + +同样的,Admin 也可以复用 Kafka 实例。 + +```typescript +// src/config/config.default +export default { + kafka: { + consumer: { + sub1: { + connectionOptions: { + clientId: 'my-app', + brokers: ['localhost:9092'], + }, + } + }, + admin: { + clients: { + admin1: { + kafkaInstanceRef: 'sub1', + } + } + } + } +} +``` + +### 使用 Admin + +Admin 不存在默认实例,由于使用了服务工厂的设计模式,所以可以通过 `@InjectClient()` 来注入。 + +```typescript +// src/service/admin.service.ts +import { Provide, InjectClient } from '@midwayjs/core'; +import { KafkaAdminFactory, Admin } from '@midwayjs/kafka'; + +@Provide() +export class AdminService { + + @InjectClient(KafkaAdminFactory, 'admin1') + admin: Admin; +} +``` + +更多的 Admin 使用方法,可以参考 [KafkaJS Admin](https://kafka.js.org/docs/admin) 文档。 + + +## 组件日志 + +Kafka 组件默认使用 `kafkaLogger` 日志,默认会将 `ctx.logger` 记录在 `midway-kafka.log`。 + +你可以通过配置修改。 + +```typescript +// src/config/config.default +export default { + midwayLogger: { + clients: { + kafkaLogger: { + fileLogName: 'midway-kafka.log', + }, + }, + }, +} +``` + +这个日志的输出格式,我们也可以单独配置。 + +```typescript +export default { + kafka: { + // ... + contextLoggerFormat: info => { + const { jobId, from } = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.message}`; + }, + } +} +``` + + +## 获取 KafkaJS 模块 + +KafkaJS 模块,可以通过 `@midwayjs/kafka` 的 `KafkaJS` 字段来获取。 + +```typescript +import { KafkaJS } from '@midwayjs/kafka'; + +const { ConfigResourceTypes } = KafkaJS; +// ... +``` + +## 关于分区的警告 + +如果你使用的是 KafkaJS 的 v2.0.0 版本,你可能会看到如下的警告: + +``` +2024-11-04 23:47:28.228 WARN 31729 KafkaJS v2.0.0 switched default partitioner. To retain the same partitioning behavior as in previous versions, create the producer with the option "createPartitioner: Partitioners.LegacyPartitioner". See the migration guide at https://kafka.js.org/docs/migration-guide-v2.0.0#producer-new-default-partitioner for details. Silence this warning by setting the environment variable "KAFKAJS_NO_PARTITIONER_WARNING=1" { timestamp: '2024-11-04T15:47:28.228Z', logger: 'kafkajs' } +``` + +这个警告是由于 KafkaJS 的 v2.0.0 版本默认使用了新的分区器,如果接受新的分区器行为,但想要关闭这个警告消息,可以通过设置环境变量 `KAFKAJS_NO_PARTITIONER_WARNING=1` 来消除这个警告。 + +或者显示声明分区器。 + +```typescript +// src/config/config.default +import { KafkaJS } from '@midwayjs/kafka'; +const { Partitioners } = KafkaJS; + +export default { + kafka: { + producer: { + clients: { + pub1: { + // ... + producerOptions: { + createPartitioner: Partitioners.DefaultPartitioner, + // ... + createPartitioner: Partitioners.LegacyPartitioner, + }, + }, + }, + }, + } +} +``` + +建议你查看 KafkaJS v2.0.0 的 [迁移指南](https://kafka.js.org/docs/migration-guide-v2.0.0#producer-new-default-partitioner) 了解更多细节。 + + + +## 参考文档 + +- [KafkaJS](https://kafka.js.org/docs/introduction) +- [apache kafka官网](https://kafka.apache.org/intro) diff --git a/site/versioned_docs/version-3.0.0/extensions/koa.md b/site/versioned_docs/version-3.0.0/extensions/koa.md new file mode 100644 index 000000000000..7ec42a15459c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/koa.md @@ -0,0 +1,499 @@ +# Koa + +Koa 是一个非常轻量易用的 Web 框架。本章节内容,主要介绍在 Midway 中如何使用 Koa 作为上层框架,并使用自身的能力。 + +Midway 默认的示例都是基于该包。 + +`@midwayjs/koa` 包默认使用 `koa@2` 以及集成了 `@koa/router` 作为路由基础能力,并默认内置了 `session` 和 `body-parser` 功能。 + +| 描述 | | +| -------------- | ---- | +| 包含独立主框架 | ✅ | +| 包含独立日志 | ✅ | + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/koa@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/koa": "^3.0.0", + // ... + }, +} +``` + +也可以直接使用脚手架创建示例。 + +```bash +# npm v6 +$ npm init midway --type=koa-v3 my_project + +# npm v7 +$ npm init midway -- --type=koa-v3 my_project +``` + + + +## 开启组件 + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { join } from 'path'; + +@Configuration({ + imports: [koa], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration { + @App() + app: koa.Application; + + async onReady() { + // ... + } +} + +``` + + + +## BodyParser + +`@midwayjs/koa` 自带 `bodyParser` 功能,默认会解析 `Post` 请求,自动识别 `json` 和 `form` 类型。 + +如需 text 或者 xml,可以自行配置。 + +默认的大小限制为 `1mb`,可以单独对每项配置大小。 + +```typescript +// src/config/config.default +export default { + // ... + bodyParser: { + enableTypes: ['json', 'form', 'text', 'xml'], + formLimit: '1mb', + jsonLimit: '1mb', + textLimit: '1mb', + xmlLimit: '1mb', + }, +} +``` + +注意,使用 Postman 做 Post 请求时的类型选择: + +![postman](https://img.alicdn.com/imgextra/i4/O1CN01QCdTsN1S347SuzZU5_!!6000000002190-2-tps-1017-690.png) + + +关闭 bodyParser 中间件。 + +```typescript +// src/config/config.default +export default { + // ... + bodyParser: { + enable: false, + // ... + }, +} +``` + + +## Cookie 和 Session + +`@midwayjs/koa` 默认封装了 `cookies` 解析和 `Session` 的支持,可以查看 [Cookies 和 Session](../cookie_session)。 + + + +## 扩展 Context + +在一些场景下,需要对 Context 做扩展。 + +如果希望挂在一些临时的请求相关的对象数据,可以使用 `ctx.setAttr(key, value)` API 来实现,比如组件里自用的数据。 + +如果实在有扩展 Context 的诉求,可以使用 koa 自带的 API。 + +比如,我们在 `configuration.ts` 中做扩展提供了一个 `render()` 方法。 + +```typescript +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App() + app: koa.Application; + + async onReady(container) { + Object.defineProperties(app.context, { + render: { + value: async function (...args) { + // ... + }, + }, + }); + } +} +``` + +但是这样做无法直接让 Context 包含 Typescript 定义,需要额外增加定义,请参考 [扩展上下文定义](../context_definition)。 + + + +## 获取 Http Server + +在一些特殊情况下,你需要获取到原始的 Http Server,我们可以在服务器启动后获取。 + +```typescript +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + framework: koa.Framework; + + async onServerReady(container) { + const server = this.framework.getServer(); + // ... + } +} +``` + + + +## State 类型定义 + +在 koa 的 Context 中有一个特殊的 State 属性,通过和 Context 类似的方式可以扩展 State 定义。 + +```typescript +// src/interface.ts + +declare module '@midwayjs/koa/dist/interface' { + interface Context { + abc: string; + } + + interface State{ + bbb: string; + ccc: number; + } +} +``` + + + + + +## 配置 + + + +### 默认配置 + +`@midwayjs/koa` 的配置样例如下: + +```typescript +// src/config/config.default +export default { + // ... + koa: { + port: 7001, + }, +} +``` + +所有属性描述如下: + +| 属性 | 类型 | 描述 | +| ------------ |--------------------------------------------| ------------------------------------------------------- | +| port | number | 可选,启动的端口 | +| globalPrefix | string | 可选,全局的 http 前缀 | +| keys | string[] | 可选,Cookies 签名,如果上层未写 keys,也可以在这里设置 | +| hostname | string | 可选,监听的 hostname,默认 127.1 | +| key | string \| Buffer \| Array\Buffer\|Object> | 可选,Https key,服务端私钥 | +| cert | string \| Buffer \| Array\ | 可选,Https cert,服务端证书 | +| ca | string \| Buffer \| Array\ | 可选,Https ca | +| http2 | boolean | 可选,http2 支持,默认 false | +| proxy | boolean | 可选,是否开启代理,如果为 true 则对于 request 请求中的 ip 优先从 Header 字段中 X-Forwarded-For 获取,默认 false | +| subdomainOffset | number | 可选,子域名的偏移量,默认 2 | +| proxyIpHeader | string | 可选,获取代理 ip 的字段名,默认为 X-Forwarded-For | +| maxIpsCount | number | 可选,获取的 ips 最大数量,默认为 0(全部返回)| +| serverTimeout | number | 可选,服务端超时配置,默认为 2 \* 60 \* 1000(2 分钟),单位毫秒 | +| serverOptions | Record\ | 可选,http Server [选项](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener) | + + + +### 修改端口 + +默认情况下,我们在 `config.default` 提供了 `7001` 的默认端口参数,修改它就可以修改 koa http 服务的默认端口。 + +比如我们修改为 `6001`: + +```typescript +// src/config/config.default +export default { + // ... + koa: { + port: 6001, + }, +} +``` + +默认情况下,单测环境由于需要 supertest 来启动端口,我们的 port 配置为 `null`。 + +```typescript +// src/config/config.unittest +export default { + // ... + koa: { + port: null, + }, +} +``` + +此外,也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口,此方法会覆盖配置中的端口。 + + + +### 全局前缀 + +此功能请参考 [全局前缀](../controller#全局路由前缀)。 + + + +### 反向代理配置 + +如果使用了 Nginx 等反向代理,请开启 `proxy` 配置。 + +```typescript +// src/config/config.default +export default { + // ... + koa: { + proxy: true, + }, +} +``` + +默认使用 `X-Forwarded-For` Header,如果代理配置不同,请自行配置不同的 Header。 + +```typescript +// src/config/config.default +export default { + // ... + koa: { + proxy: true, + proxyIpHeader: 'X-Forwarded-Host' + }, +} +``` + + + + + +### Https 配置 + +在大多数的情况,请尽可能使用外部代理的方式来完成 Https 的实现,比如 Nginx。 + +在一些特殊场景下,你可以通过配置 SSL 证书(TLS 证书)的方式,来直接开启 Https。 + +首先,你需要提前准备好证书文件,比如 `ssl.key` 和 `ssl.pem`,key 为服务端私钥,pem 为对应的证书。 + +然后配置即可。 + +```typescript +// src/config/config.default +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export default { + // ... + koa: { + key: join(__dirname, '../ssl/ssl.key'), + cert: join(__dirname, '../ssl/ssl.pem'), + }, +} +``` + + + +### favicon 设置 + +默认情况下,浏览器会发起一个 `favicon.ico` 的请求。 + +框架提供了一个默认中间件,用来处理该请求,你可以指定一个 `favicon` 的 Buffer。 + +```typescript +// src/config/config.default +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export default { + // ... + siteFile: { + favicon: readFileSync(join(__dirname, '../static/fav.ico')), + }, +} +``` + +如果开启了 `@midwayjs/static-file` 组件,那么会优先使用组件的静态文件托管。 + +关闭中间件。 + +```typescript +// src/config/config.default +export default { + // ... + siteFile: { + enable: false, + // ... + }, +} +``` + +### 修改上下文日志 + +可以单独修改 koa 框架的上下文日志。 + +```typescript +export default { + koa: { + contextLoggerFormat: info => { + const ctx = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} [${ctx.userId} - ${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`; + } + // ... + }, +}; +``` + + +### Query 数组解析 + +默认情况下,koa 使用 `querystring` 解析 query 参数,当碰到数组时,会将数组的数据拆开。 + +比如: + +``` +GET /query?a[0]=1&a[1]=2 +``` + +拿到的结果是: + +```json +{ + "a[0]": 1, + "a[1]": 2, +} +``` + +框架提供了一些参数来处理这种情况。 + +```typescript +// src/config/config.default +export default { + // ... + koa: { + queryParseMode: 'extended', + // ... + }, +} +``` + +`queryParseMode` 参数可以选择 `extended`、 `strict`、`first` 三种值。 + + 当 `queryParseMode` 有值时,会使用 `qs` 模块处理 query,效果同 `koa-qs` 模块。 + +当请求参数为 `/query?a=1&b=2&a=3&c[0]=1&c[1]=2'` 时。 + +默认效果(使用 `querystring`) + +```JSON +{ + "a": ["1", "3" ], + "b": "2", + "c[0]": "1", + "c[1]": "2" +} +``` + + `extended` 效果 + +```JSON +{ + "a": ["1", "3" ], + "b": ["2"], + "c": ["1", "2"] +} +``` + + `strict` 效果 + +```JSON +{ + "a": ["1", "3" ], + "b": "2", + "c": ["1", "2"] +} +``` + + `first` 效果 + +```JSON +{ + "a": "1", + "b": "2", + "c": "1" +} +``` + + +### 超时配置 + +RequestTiemout 和 ServerTimeout 是两种不同的超时情况。 + +- `serverTimeout`:用于设置服务器接收到请求后,等待客户端发送数据的超时时间。如果在该时间内客户端没有发送任何数据,则服务器将关闭连接。此超时适用于整个请求-响应周期,包括请求头、请求主体以及响应。 +- `requestTimeout`:用于设置服务器等待客户端发送完整请求的超时时间。这个超时是针对请求头和请求主体的,服务器将在该时间内等待客户端发送完整的请求。如果在超时时间内没有收到完整的请求,则服务器将中止该请求。 + +默认情况下,`serverTimeout` 为 0,不会触发超时。 + +如有需求,可以通过配置修改,单位毫秒。 + +```typescript +// src/config/config.default +export default { + // ... + koa: { + serverTimeout: 100_000 + }, +} +``` + +如果程序出现 `ERR_HTTP_REQUEST_TIMEOUT` 这个错误,说明是触发了 `requestTimeout`,默认为 `300_000` (五分钟),单位毫秒,可以通过以下配置修改。 + +```typescript +// src/config/config.default +export default { + // ... + koa: { + serverOptions: { + requestTimeout: 600_000 + } + }, +} +``` + diff --git a/site/versioned_docs/version-3.0.0/extensions/mikro.md b/site/versioned_docs/version-3.0.0/extensions/mikro.md new file mode 100644 index 000000000000..0d4314453c42 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/mikro.md @@ -0,0 +1,500 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# MikroORM + +本章节介绍用户如何在 midway 中使用 MikroORM。 MikroORM 是基于数据映射器、工作单元和身份映射模式的 Node.js 的 TypeScript ORM。 + +MikroORM 的官网文档在 [这里](https://mikro-orm.io/docs)。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 关于升级 + +* 从 `v3.14.0` 版本的组件开始,支持 mikro v5/v6 版本,由于 mikro v5 到 v6 有较大的变化,如从 mikro 老版本升级请提前阅读 [Upgrading from v5 to v6](https://mikro-orm.io/docs/upgrading-v5-to-v6) +* 组件示例已更新为 v6 版本 + + + +## 安装组件 + + +安装 mikro 组件,提供接入 mikro-orm 的能力。 + + +```bash +$ npm i @midwayjs/mikro@3 @mikro-orm/core --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/mikro": "^3.0.0", + "@mikro-orm/core": "^6.0.2", + // ... + }, + "devDependencies": { + // ... + } +} +``` + +同时,还需要引入对应数据库的适配包。 + +比如: + +```typescript +{ + "dependencies": { + // sqlite + "@mikro-orm/sqlite": "^6.0.2", + + // mysql + "@mikro-orm/mysql": "^6.0.2", + }, + "devDependencies": { + // ... + } +} +``` + +更多驱动程序请查看 [官方文档](https://mikro-orm.io/docs/usage-with-sql/)。 + + + +## 引入组件 + + +在 `src/configuration.ts` 引入 mikro 组件,示例如下。 + +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as mikro from '@midwayjs/mikro'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + mikro // 加载 mikro 组件 + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + + + +## 基础使用 + +和其他 orm 框架类似,都是分为几个步骤: + +- 1、定义 Entity +- 2、配置数据源 +- 3、获取 EntityModel 进行调用 + +下面的更多 Entity 代码请查看 [示例](https://github.com/midwayjs/midway/tree/main/packages/mikro/test/fixtures/base-fn-origin)。 + + + +### 目录结构 + +一个基础的参考目录结构如下。 + +``` +MyProject +├── src +│ ├── config +│ │ └── config.default.ts +│ ├── entity +│ │ ├── book.entity.ts +│ │ ├── index.ts +│ │ └── base.ts +│ ├── configuration.ts +│ └── service +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + + +### 定义 Entity + +定义基础的 Entity。 + +```typescript +// src/entity/BaseEntity.ts +import { PrimaryKey, Property } from '@mikro-orm/core'; + +export abstract class BaseEntity { + + @PrimaryKey() + id!: number; + + @Property() + createdAt: Date = new Date(); + + @Property({ onUpdate: () => new Date() }) + updatedAt: Date = new Date(); + +} +``` + +定义实际的 Entity,包含一对多,多对多等关系。 + +```typescript +// src/entity/book.entity.ts +import { Cascade, Collection, Entity, ManyToMany, ManyToOne, Property } from '@mikro-orm/core'; +import { Author, BookTag, Publisher } from './index'; +import { BaseEntity } from './base'; + +@Entity() +export class Book extends BaseEntity { + + @Property() + title: string; + + @ManyToOne(() => Author) + author: Author; + + @ManyToOne(() => Publisher, { cascade: [Cascade.PERSIST, Cascade.REMOVE], nullable: true }) + publisher?: Publisher; + + @ManyToMany(() => BookTag) + tags = new Collection(this); + + @Property({ nullable: true }) + metaObject?: object; + + @Property({ nullable: true }) + metaArray?: any[]; + + @Property({ nullable: true }) + metaArrayOfStrings?: string[]; + + constructor(title: string, author: Author) { + super(); + this.title = title; + this.author = author; + } + +} +``` + + + +### 配置数据源 + +mikro v5 和 v6 略有不同。 + + + + +```typescript +// src/config/config.default +import { Author, BaseEntity, Book, BookTag, Publisher } from '../entity'; +import { join } from 'path'; +import { SqliteDriver } from '@mikro-orm/sqlite'; + +export default (appInfo) => { + return { + mikro: { + dataSource: { + default: { + dbName: join(__dirname, '../../test.sqlite'), + driver: SqliteDriver, // 这里使用了 sqlite 做示例 + allowGlobalContext: true, + // 实体形式 + entities: [Author, Book, BookTag, Publisher, BaseEntity], + // 支持如下的扫描形式,为了兼容我们可以同时进行.js和.ts匹配️ + entities: [ + 'entity', // 指定目录 + '**/entity/*.entity.{j,t}s', // 通配加后缀匹配 + ], + } + } + } + } +} +``` + + + + + +```typescript +// src/config/config.default +import { Author, BaseEntity, Book, BookTag, Publisher } from '../entity'; +import { join } from 'path'; + +export default (appInfo) => { + return { + mikro: { + dataSource: { + default: { + dbName: join(__dirname, '../../test.sqlite'), + type: 'sqlite', // 这里使用了 sqlite 做示例 + allowGlobalContext: true, + // 实体形式 + entities: [Author, Book, BookTag, Publisher, BaseEntity], + // 支持如下的扫描形式,为了兼容我们可以同时进行.js和.ts匹配️ + entities: [ + 'entity', // 指定目录 + '**/entity/*.entity.{j,t}s', // 通配加后缀匹配 + ], + } + } + } + } +} +``` + + + + +:::tip + +mikro 的 `entities` 字段配置已经经过框架处理,该字段配置请不要参考原始文档。 + +::: + + + +### 增删查改 + +在业务代码中,可以使用 `InjectRepository` 注入 `Repository` 对象执行简单的查询操作。其它的增删改操作可以通过配合`EntityManger ` 的 `persist` 和 `flush` 接口来实现,使用 `InjectEntityManager` 可以直接注入 `EntityManager` 对象,也可以通过`repository.getEntityManager()`获取。 + +:::caution + +* 1、从 5.7 版本开始,MikroORM 将原来 `Repository` 上 `persist` 和 `flush` 等接口标为*弃用*,并计划在 v6 版本中 [彻底移除](https://github.com/mikro-orm/mikro-orm/discussions/3989),建议直接调用`EntityManager`上的相关接口 +* 2、v6 已经彻底 [弃用](https://mikro-orm.io/docs/upgrading-v5-to-v6#removed-methods-from-entityrepository) 上述接口 + +::: + +```typescript +// src/service/book.service.ts +import { Book } from './entity/book.entity'; +import { Provide } from '@midwayjs/core'; +import { InjectEntityManager, InjectRepository } from '@midwayjs/mikro'; +import { QueryOrder } from '@mikro-orm/core'; +import { EntityManager, EntityRepository } from '@mikro-orm/mysql'; // 需要使用数据库驱动对应的类来执行操作 + +@Provide() +export class BookService { + + @InjectRepository(Book) + bookRepository: EntityRepository; + + @InjectEntityManager() + em: EntityManager; + + async queryByRepo() { + // 使用Repository查询 + const books = await this.bookRepository.findAll({ + populate: ['author'], + orderBy: { title: QueryOrder.DESC }, + limit: 20, + }); + return books; + } + + async createBook() { + const book = new Book({ title: 'b1', author: { name: 'a1', email: 'e1' } }); + // 标记保存Book + this.em.persist(book); + // 执行所有变更 + await this.em.flush(); + return book; + } +} +``` + +## 高级功能 + +### 获取数据源 + +数据源即创建出的数据源对象,我们可以通过注入内置的数据源管理器来获取。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import { MikroDataSourceManager } from '@midwayjs/mikro'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + + async onReady(container: IMidwayContainer) { + const dataSourceManager = await container.getAsync(MikroDataSourceManager); + const orm = dataSourceManager.getDataSource('default'); + const connection = orm.em.getConnection(); + // ... + } +} +``` + +从 v3.8.0 开始,也可以通过装饰器注入。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import { InjectDataSource } from '@midwayjs/mikro'; +import { MikroORM, IDatabaseDriver, Connection } from '@mikro-orm/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + + // 注入默认数据源 + @InjectDataSource() + defaultDataSource: MikroORM>; + + // 注入自定义数据源 + @InjectDataSource('default1') + customDataSource: MikroORM>; + + async onReady(container: IMidwayContainer) { + // ... + } +} +``` + + + +### 日志 + +可以通过配置将 midway 的 logger 添加到 mikro 中,用于记录 sql 等信息。 + +```typescript +// src/config/config.default.ts +exporg default { + midwayLogger: { + clients: { + mikroLogger: { + // ... + } + } + }, + mikro: { + dataSource: { + default: { + entities: [Author, Book, BookTag, Publisher, BaseEntity], + // ... + logger: 'mikroLogger', + } + }, + } +} +``` + +默认情况下 mikro 自带颜色,也会将其写入文件,可以通过配置关闭。 + +```typescript +// src/config/config.default.ts +exporg default { + midwayLogger: { + clients: { + mikroLogger: { + transports: { + console: { + autoColors: false, + }, + file: { + fileLogName: 'mikro.log', + }, + }, + } + } + }, + mikro: { + dataSource: { + default: { + entities: [Author, Book, BookTag, Publisher, BaseEntity], + // ... + logger: 'mikroLogger', + colors: false, + } + }, + } +} +``` + + + +## 常见问题 + + + +### 1、Node 版本 + +Mikro-orm 对 Node 版本有一些限制,必须为 `>=14.0.0` ,所以 `@midwayjs/mikro` 组件的使用规则也如此。 + + + +### 2、Identity Map + +Mikro-orm 内部查询有一个 [Identity Map](https://mikro-orm.io/docs/identity-map) 的概念,Midway 已经在所有的内置 Framework 的中间件内置加入了该功能,如果在非请求链路调用场景下使用,比如 `src/configuration` 中,可以开启 `allowGlobalContext` 选项。 + + + +### 3、多库的支持 + +和其他数据库一样,Midway 支持多数据源的配置。 + +```typescript +// src/config/config.default +import { Author, BaseEntity, Book, BookTag, Publisher } from '../entity'; +import { SqlHighlighter } from '@mikro-orm/sql-highlighter'; +import { join } from 'path'; + +export default (appInfo) => { + return { + mikro: { + dataSource: { + custom1: { + // ... + }, + custom2: { + // ... + } + } + } + } +} +``` + +注意在使用时,需要传递来自哪个数据源。 + +```typescript +// ... + +@Provide() +export class BookController { + + @InjectRepository(Book, 'custom1') + bookRepository: EntityRepository; + + async findBookAndQuery() { + // ... + } +} +``` + diff --git a/site/versioned_docs/version-3.0.0/extensions/mongodb.md b/site/versioned_docs/version-3.0.0/extensions/mongodb.md new file mode 100644 index 000000000000..78803cc831e9 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/mongodb.md @@ -0,0 +1,612 @@ +# MongoDB + +在这一章节中,我们选择 [Typegoose](https://github.com/typegoose/typegoose) 作为基础的 MongoDB ORM 库。就如同他描述的那样 " Define Mongoose models using TypeScript classes",和 TypeScript 结合的很不错。 + +简单的来说,Typegoose 使用 TypeScript 编写 Mongoose 模型的 “包装器”,它的大部分能力还是由 [mongoose](https://www.npmjs.com/package/mongoose) 库来提供的。 + +也可以直接选择 [mongoose](https://www.npmjs.com/package/mongoose) 库来使用,我们会分别描述。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + +:::tip + +- 1、当前模块从 v3.4.0 开始已经重构,历史写法兼容,如果查询历史文档,请参考 [这里](../legacy/mongodb)。 +- 2、如果代码中有读取配置,注意 `mongoose.clients` 可能会读不到,请使用 `mongoose.dataSource`。 + +::: + + + +## 和老写法的区别 + +如果想使用新版本的用法,请参考下面的流程,将老代码进行修改,新老代码请勿混用。 + +升级方法: + +- 1、无需再使用 `EntityModel` 装饰器 +- 3、在 `src/config.default` 的 `mongoose` 部分配置调整,参考下面的数据源配置部分 + - 3.1 修改为数据源的形式 `mongoose.dataSource` + - 3.2 将实体模型在数据源的 `entities` 字段中声明 + + + + +## Mongoose 版本依赖 + + +mongoose 和你服务器使用的 MongoDB Server 的版本也有着一定的关系,如下,请务必注意。 + + +- MongoDB Server 2.4.x: mongoose ^3.8 or 4.x +- MongoDB Server 2.6.x: mongoose ^3.8.8 or 4.x +- MongoDB Server 3.0.x: mongoose ^3.8.22, 4.x, or 5.x +- MongoDB Server 3.2.x: mongoose ^4.3.0 or 5.x +- MongoDB Server 3.4.x: mongoose ^4.7.3 or 5.x +- MongoDB Server 3.6.x: mongoose 5.x +- MongoDB Server 4.0.x: mongoose ^5.2.0 +- MongoDB Server 4.2.x: mongoose ^5.7.0 +- MongoDB Server 4.4.x: mongoose ^5.10.0 +- MongoDB Server 5.x: mongoose ^6.0.0 + + +**mongoose 相关的依赖比较复杂,且对应不同的版本,现阶段,我们使用的主要是 mongoose v5 和 v6。** + + +:::info +从 mongoose@v5.11.0 开始,mongoose 官方支持了定义,所以不再需要安装 @types/mongoose 依赖包。 +::: + +安装包依赖版本如下: + +**支持 MongoDB Server 6.x** + +```json + "dependencies": { + "mongoose": "^7.0.0", + "@typegoose/typegoose": "^10.0.0", // 使用 typegoose 需要安装此依赖 + }, +``` + +**支持 MongoDB Server 5.x** + +```json + "dependencies": { + "mongoose": "^6.0.7", + "@typegoose/typegoose": "^9.0.0", // 使用 typegoose 需要安装此依赖 + }, +``` + + +**支持 MongoDB Server 4.4.x** + + +以下版本不需要安装额外定义包。 +```json + "dependencies": { + "mongoose": "^5.13.3", + "@typegoose/typegoose": "^8.0.0", // 使用 typegoose 需要安装此依赖 + }, +``` + + +以下版本需要安装额外定义包(不推荐)。 +```json + "dependencies": { + "mongodb": "3.6.3", // mongoose 内部写死了该版本 + "mongoose": "~5.10.18", + "@typegoose/typegoose": "^7.0.0", // 使用 typegoose 需要安装此依赖 + }, + "devDependencies": { + "@types/mongodb": "3.6.3", // 只能使用此版本 + "@types/mongoose": "~5.10.3", + } +``` + + +其余的 MongoDB 安装模块类似,未测。 + + + +## 使用 Typegoose + + +### 1、安装组件 + + +安装 Typegoose 组件,提供访问 MongoDB 的能力。 + + +**请务必注意,请查看第一小节提前编写/安装 mongoose 等相关依赖包。** +```bash +$ npm i @midwayjs/typegoose@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + // 组件 + "@midwayjs/typegoose": "^3.0.0", + // 上一节中的 mongoose 依赖 + }, + "devDependencies": { + // 上一节中的 mongoose 依赖 + // ... + } +} +``` + + + +安装后需要手动在 `src/configuration.ts` 配置,代码如下。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as typegoose from '@midwayjs/typegoose'; + +@Configuration({ + imports: [ + typegoose // 加载 typegoose 组件 + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + +:::info +在该组件中,midway 只是做了简单的配置规则化,并将其注入到初始化流程中。 +::: + +### 2、简单的目录结构 + + +我们以一个简单的项目举例,其他结构请自行参考。 + + +```text +MyProject +├── src // TS 根目录 +│ ├── config +│ │ └── config.default.ts // 应用配置文件 +│ ├── entity // 实体(数据库 Model) 目录 +│ │ └── user.ts // 实体文件 +│ ├── configuration.ts // Midway 配置文件 +│ └── service // 其他的服务目录 +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + +在这里,我们的数据库实体主要放在 `entity` 目录(非强制),这只是一个简单的约定。 + + + +### 3、创建实体文件 + +比如在 `src/entity/user.ts` 中。 + + +```typescript +import { prop } from '@typegoose/typegoose'; + +export class User { + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} +``` + +等价于使用 mongoose 的下列代码 + +```typescript +const userSchema = new mongoose.Schema({ + name: String, + jobs: [{ type: String }] +}); + +const User = mongoose.model('User', userSchema); +``` + +:::info +所以说,typegoose 只是简化了 model 的创建过程。 +::: + + + + +### 4、配置连接信息 + + +在 `src/config/config.default.ts` 中加入连接的配置。 + +```typescript +import { User } from '../entity/user'; + +export default { + // ... + mongoose: { + dataSource: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '***********' + }, + // 关联实体 + entities: [ User ] + } + } + }, +} +``` + +如需以目录扫描形式关联,请参考 [数据源管理](../data_source)。 + + + +### 5、引用实体,调用数据库 + + +示例代码如下: + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typegoose'; +import { ReturnModelType } from '@typegoose/typegoose'; +import { User } from '../entity/user'; + +@Provide() +export class TestService { + + @InjectEntityModel(User) + userModel: ReturnModelType; + + async getTest(){ + // create data + const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an "as" assertion, to have types for all properties + + // find data + const user = await this.userModel.findById(id).exec(); + console.log(user) + } +} +``` + + +### 6、多库的情况 + +首先定义多个实体。 + +```typescript +class User { + + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} + +class User2 { + + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} +``` + + +将实体配置到多个数据源。 + + +在 `src/config/config.default.ts` 中加入数据源的配置。 +```typescript +import { User, User2 } from '../entity/user'; + +export default { + // ... + mongoose: { + dataSource: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '***********' + }, + entities: [ User ] + }, + db1: { + uri: 'mongodb://localhost:27017/test1', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '***********' + }, + entities: [ User2 ] + } + } + }, +} +``` + + +定义实例时使用固定的连接,在扫描 dataSource 配置 Model 会自动关联 mongoose连接(`getModelForClass(Model, { existingConnection: conn })`)。 + +```typescript +@Provide() +export class TestService{ + + @InjectEntityModel(User) + userModel: ReturnModelType; + + @InjectEntityModel(User2) + user2Model: ReturnModelType; + + async getTest(){ + const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an "as" assertion, to have types for all properties + const user = await this.userModel.findById(id).exec(); + console.log(user) + + const { _id: id2 } = await this.user2Model.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User2); // an "as" assertion, to have types for all properties + const user2 = await this.user2Model.findById(id2).exec(); + console.log(user2) + } +} + +``` + + + +### 7、关于 schemaOptions + +Typegoose 预留了一个 `setGlobalOptions` 方法用来设置 [schemaOptions](https://typegoose.github.io/typegoose/docs/api/decorators/model-options#schemaoptions) 和一些其他全局性的 [配置](https://typegoose.github.io/typegoose/docs/api/decorators/model-options#options-1)。 + +我们可以在项目配置加载时设置它。 + +```typescript +// srcconfiguration.ts +import { Configuration } from '@midwayjs/core'; +import * as typegoose from '@midwayjs/typegoose'; +import * as Typegoose from '@typegoose/typegoose'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + async onConfigLoad() { + + Typegoose.setGlobalOptions({ + schemaOptions: { + // ... + }, + options: { allowMixed: Severity.ERROR } + }); + // ... + } +} +``` + + + + + +## 直接使用 mongoose + +mongoose 组件是 typegoose 的基础组件,有时候我们可以直接使用它。 + + +### 1、安装组件 + + +**请务必注意,请查看第一小节提前编写/安装 mongoose 等相关依赖包。** + +```bash +$ npm i @midwayjs/mongoose@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + // 组件 + "@midwayjs/mongoose": "^3.0.0", + // 上一节中的 mongoose 依赖 + }, + "devDependencies": { + // 上一节中的 mongoose 依赖 + // ... + } +} +``` + + + +### 2、开启组件 + + +安装后需要手动在 `src/configuration.ts` 配置,代码如下。 +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as mongoose from '@midwayjs/mongoose'; + +@Configuration({ + imports: [ + mongoose // 加载 mongoose 组件 + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + + + + +### 2、配置 + +和 typegoose 相同,或者说 typegoose 使用的就是 mongoose 的配置。 + +不管是单库还是多库,数据源配置都是类似的。 + + +单库: +```typescript +export default { + // ... + mongoose: { + dataSource: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '**********' + } + } + } + }, +} +``` +多库: +```typescript +export default { + // ... + mongoose: { + dataSource: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '***********' + } + }, + db1: { + uri: 'mongodb://localhost:27017/test1', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '***********' + } + } + } + }, +} +``` + + + +### 3、使用 + + +当我们希望获取到原始的连接对象时,可以直接使用封装好的 `MongooseConnectionService` 对象。 +```typescript +import { Provide, Inject, Init } from '@midwayjs/core'; +import { MongooseDataSourceManager } from '@midwayjs/mongoose'; +import { Schema, Document } from 'mongoose'; + +interface User extends Document { + name: string; + email: string; + avatar: string; +} + +@Provide() +export class TestService { + + @Inject() + dataSourceManager: MongooseDataSourceManager; + + @Init() + async init() { + // get default connection + this.conn = this.dataSourceManager.getDataSource('default'); + } + + async invoke(){ + const schema = new Schema({ + name: { type: String, required: true }, + email: { type: String, required: true }, + avatar: String + }); + const UserModel = this.conn.model('User', schema); + const doc = new UserModel({ + name: 'Bill', + email: 'bill@initech.com', + avatar: 'https://i.imgur.com/dM7Thhn.png' + }); + await doc.save(); + } +} + +``` + + + + + + +## 常见问题 + + +### 1、E002: You are using a NodeJS Version below 12.22.0 + + +在新版本 @typegoose/typegoose (v8, v9) 中增加了 Node 版本的校验,如果你的 Node.js 版本低于 v12.22.0,就会出现这个提示。 + + +普通情况下,请升级 Node.js 到这个版本以上即可解决。 + + +在特殊场景下,比如 Serverless 无法修改 Node.js 版本且版本低于 v12.22 的情况下,由于 v12 版本子版本其实都可以,可以通过临时修改 process.version 绕过。 + + +```typescript +// src/configuration.ts + +Object.defineProperty(process, 'version', { + value: 'v12.22.0', + writable: true, +}); + +// other code + +export class MainConfiguration {} +``` + + + diff --git a/site/versioned_docs/version-3.0.0/extensions/mqtt.md b/site/versioned_docs/version-3.0.0/extensions/mqtt.md new file mode 100644 index 000000000000..222915294fa7 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/mqtt.md @@ -0,0 +1,296 @@ +# MQTT + +MQTT是用于物联网 (IoT) 的OASIS标准消息传递协议。它被设计为非常轻量级的发布/订阅消息传输,非常适合以较小的代码占用空间和最小的网络带宽连接远程设备。MQTT目前广泛应用于汽车、制造、电信、石油和天然气等行业。 + +相关信息: + +| 描述 | | +| ----------------- | ------------ | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | 可以发布消息 | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ✅ | +| 包含独立日志 | ✅ | + + + +## 版本要求 + +由于 [mqtt](https://github.com/mqttjs/MQTT.js) 库本身的要求,所需要的版本为 **Node.js >= 16** + + + +## 前置依赖 + +由于 MQTT 需要 Broker 作为中转传输,你需要自行部署 MQTT Broker 服务,本文档不提供 MQTT 服务本身的部署指导。 + + + +## 安装组件 + + +安装 mqtt 组件。 + + +```bash +$ npm i @midwayjs/mqtt@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/mqtt": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## 启用组件 + +在 `src/configuration.ts` 中引入组件 + +```typescript +// ... +import * as mqtt from '@midwayjs/mqtt'; + +@Configuration({ + imports: [ + // ...other components + mqtt, + ], +}) +export class MainConfiguration {} +``` + + + +由于 MQTT 分为 **订阅者(subscriber)** 和 **发布者(publisher)**两部分,两个可以独立使用,我们将分别介绍。 + + + +## 订阅服务 + +### 基础配置 + +通过 `sub` 字段和 `@MqttSubscriber` 装饰器,我们可以配置多个订阅者。 + +比如,下面的 `sub1` 和 `sub2` 就是两个不同的订阅者。 + +```typescript +// src/config/config.default + +export default { + mqtt: { + sub: { + sub1: { + // ... + }, + sub2: { + // ... + } + } + } +} +``` + +最简单的订阅者配置需要几个字段,订阅的地址和订阅的 Topic。 + +```typescript +// src/config/config.default + +export default { + mqtt: { + sub: { + sub1: { + connectOptions: { + host: 'test.mosquitto.org', + port: 1883, + }, + subscribeOptions: { + topicObject: 'test', + }, + }, + sub2: { + // ... + } + } + } +} +``` + + `sub1` 订阅者配置了 `connectOptions` 和 `subscribeOptions` ,分别代表连接配置和订阅配置。 + +### 订阅实现 + +我们可以在目录中提供一个标准的订阅器实现,比如 `src/consumer/sub1.subscriber.ts`。 + +```typescript +// src/consumer/sub1.subscriber.ts + +import { ILogger, Inject } from '@midwayjs/core'; +import { Context, IMqttSubscriber, MqttSubscriber } from '@midwayjs/mqtt'; + +@MqttSubscriber('test') +export class Sub1Subscriber implements IMqttSubscriber { + + @Inject() + ctx: Context; + + async subscribe() { + // ... + } +} +``` + +`@MqttSubscriber` 装饰器声明了一个订阅类实现,它的参数为订阅者的名字,比如我们配置文件中的 `sub1`。 + +`IMqttSubscriber` 接口约定了一个 `subscribe` 方法,每当接收到新的消息时,这个方法就会被执行。 + +和其他消息订阅机制一样,消息本身通过 `Context` 字段来传递。 + +```typescript +// ... +export class Sub1Subscriber implements IMqttSubscriber { + @Inject() + ctx: Context; + + async subscribe() { + const payload = this.ctx.message.toString(); + // ... + } +} +``` + +`Context` 字段包括几个 mqtt 属性。 + +| 属性 | 类型 | 描述 | +| ----------- | ------------------------------ | ---------------- | +| ctx.topic | string | 订阅 Topic | +| ctx.message | Buffer | 消息内容 | +| ctx.packet | IPublishPacket(来自 mqtt 库) | publish 的包信息 | + + + +## 消息发布 + +### 基础配置 + +消息发布也需要创建实例,配置本身使用了 [服务工厂](/docs/service_factory) 的设计模式。 + +比如多实例配置如下: + +```typescript +// src/config/config.default + +export default { + mqtt: { + pub: { + clients: { + default: { + host: 'test.mosquitto.org', + port: 1883, + }, + pub2: { + // ... + } + } + } + } +} +``` + +上面的配置创建了名为 `default` 和 `pub2` 的两个实例。 + + + +### 使用发布者 + +如果实例名为 `default` ,则可以使用默认的消息发布类。 + +比如: + +```typescript +// src/service/user.service.ts +import { Provide, Inject } from '@midwayjs/core'; +import { DefaultMqttProducer } from '@midwayjs/mqtt'; + +@Provide() +export class UserService { + + @Inject() + producer: DefaultMqttProducer; + + async invoke() { + // 同步发布消息 + this.producer.publish('test', 'hello world'); + + // 异步发布 + await this.producer.publishAsync('test', 'hello world'); + + // 增加配置 + await this.producer.publishAsync('test', 'hello world', { + qos: 2 + }); + } +} +``` + +也可以使用内置的工厂类 `MqttProducerFactory` 注入不同的实例。 + +```typescript +// src/service/user.service.ts +import { Provide, Inject } from '@midwayjs/core'; +import { MqttProducerFactory, DefaultMqttProducer } from '@midwayjs/mqtt'; + +@Provide() +export class UserService { + + @InjectClient(MqttProducerFactory, 'pub2') + producer: DefaultMqttProducer; + + async invoke() { + // ... + } +} +``` + + + +## 组件日志 + +组件有着自己的日志,默认会将 `ctx.logger` 记录在 `midway-mqtt.log` 中。 + +我们可以单独配置这个 logger 对象。 + +```typescript +export default { + midwayLogger: { + // ... + mqttLogger: { + fileLogName: 'midway-mqtt.log', + }, + } +} +``` + +这个日志的输出格式,我们也可以单独配置。 + +```typescript +export default { + mqtt: { + // ... + contextLoggerFormat: info => { + const { jobId, from } = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.message}`; + }, + } +} +``` + diff --git a/site/versioned_docs/version-3.0.0/extensions/orm.md b/site/versioned_docs/version-3.0.0/extensions/orm.md new file mode 100644 index 000000000000..7fd1326b01fd --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/orm.md @@ -0,0 +1,1686 @@ +# TypeORM + +[TypeORM](https://github.com/typeorm/typeorm) 是 `node.js` 现有社区最成熟的对象关系映射器(`ORM` )。本文介绍如何在 Midway 中使用 TypeORM 。 + +:::tip + +本模块是从 v3.4.0 开始为新版本,模块名有变化,历史写法部分兼容,如果查询历史文档,请参考 [这里](../legacy/orm)。 + +::: + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 和老写法的区别 + +旧模块为 `@midwayjs/orm` ,新模块为 `@midwayjs/typeorm`,区别如下: + +- 1、包名不同 +- 2、在 `src/config.default` 的部分配置调整 + - 2.1 配置文件中的 key 不同 (orm => typeorm) + - 2.2修改为数据源的形式 `typeorm.dataSource` + - 2.3 实体模型类或者实体模型类的路径,需要在数据源的 `entities` 字段中声明 + - 2.4 Subscriber 需要在数据源的 `subscribers` 字段中声明 +- 3、不再使用 `EntityModel` 装饰器,直接使用 typeorm 提供的能力 + + + +## 安装组件 + + +安装 typeorm 组件,提供数据库 ORM 能力。 + + +```bash +$ npm i @midwayjs/typeorm@3 typeorm --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/typeorm": "^3.0.0", + "typeorm": "~0.3.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## 引入组件 + + +在 `src/configuration.ts` 引入 orm 组件,示例如下。 + +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as orm from '@midwayjs/typeorm'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + orm // 加载 typeorm 组件 + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + + +## 安装数据库 Driver + + +常用数据库驱动如下,选择你对应连接的数据库类型安装: +```bash +# for MySQL or MariaDB,也可以使用 mysql2 替代 +npm install mysql --save +npm install mysql2 --save + +# for PostgreSQL or CockroachDB +npm install pg --save + +# for SQLite +npm install sqlite3 --save + +# for Microsoft SQL Server +npm install mssql --save + +# for sql.js +npm install sql.js --save + +# for Oracle +npm install oracledb --save + +# for MongoDB(experimental) +npm install mongodb --save +``` + +:::info + +- Oracle driver 比较特殊,需要查看 [文档](https://github.com/oracle/node-oracledb) +- 不建议使用 typeorm 链接 mongodb,请使用 mongoose 组件 + +::: + + + + +## 简单的目录结构 + + +我们以一个简单的项目举例,其他结构请自行参考。 + + +``` +MyProject +├── src // TS 根目录 +│ ├── config +│ │ └── config.default.ts // 应用配置文件 +│ ├── entity // 实体(数据库 Model) 目录 +│ │ └── photo.entity.ts // 实体文件 +│ │ └── photoMetadata.entity.ts +│ ├── configuration.ts // Midway 配置文件 +│ └── service // 其他的服务目录 +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + + + +在这里,我们的数据库实体主要放在 `entity` 目录(非强制),这只是一个简单的约定。 + + + +## 入门 + +下面,我们将以 mysql 举例。 + + + + +### 1、创建 Model + + +我们通过模型和数据库关联,在应用中的模型就是数据库表,在 TypeORM 中,模型是和实体绑定的,每一个实体(Entity) 文件,即是 Model,也是实体(Entity)。 + + +在示例中,需要一个实体,我们这里拿 `photo` 举例。新建 entity 目录,在其中添加实体文件 `photo.entity.ts` ,一个简单的实体如下。 +```typescript +// entity/photo.entity.ts +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` +要注意,这里的实体文件的每一个属性,其实是和数据库表一一对应的,基于现有的数据库表,我们往上添加内容。 + + +### 2、定义实体模型 + + +我们使用 `Entity` 来定义一个实体模型类。 +```typescript +// entity/photo.entity.ts +import { Entity } from 'typeorm'; + +@Entity('photo') +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + +如果表名和当前的实体名不同,可以在参数中指定。 +```typescript +// entity/photo.entity.ts +import { Entity } from 'typeorm'; + +@Entity('photo_table_name') +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + + +这些实体列也可以使用 [typeorm_generator](/docs/tool/typeorm_generator) 工具生成。 + + +### 3、添加数据库列 + + +通过 typeorm 提供的 `@Column` 装饰器来修饰属性,每一个属性对应一个列。 + + +```typescript +// entity/photo.entity.ts +import { Entity, Column } from 'typeorm'; + +@Entity() +export class Photo { + + @Column() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` + +现在 `id` , `name` , `description` ,`filename` , `views` , `isPublished` 列将添加到 `photo` 表中。数据库中的列类型是根据您使用的属性类型推断出来的,例如 number 将转换为整数,将字符串转换为 varchar,将布尔值转换为 bool,等等。但是您可以通过在 `@Column`装饰器中显式指定列类型来使用数据库支持的任何列类型。 + + +我们生成了带有列的数据库表,但是还剩下一件事。每个数据库表必须具有带主键的列。 + + +数据库列包括更多的列选项(ColumnOptions),比如修改列名,指定列类型,列长度等,更多的选项请参考 [官方文档](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/entities.md#%E5%88%97%E9%80%89%E9%A1%B9)。 + + + + +### 4、创建主键列 + + +每个实体必须至少具有一个主键列。要使列成为主键,您需要使用 `@PrimaryColumn` 装饰器。 + + +```typescript +// entity/photo.entity.ts +import { Entity, Column, PrimaryColumn } from 'typeorm'; + +@Entity() +export class Photo { + + @PrimaryColumn() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` +### 5、创建自增主键列 + + +现在,如果要设置自增的 id 列,需要将 `@PrimaryColumn` 装饰器更改为 `@PrimaryGeneratedColumn` 装饰器: +```typescript +// entity/photo.entity.ts +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class Photo { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` + + +### 6、列数据类型 + + +接下来,让我们调整数据类型。默认情况下,字符串映射到类似 `varchar(255)` 的类型(取决于数据库类型)。 Number 映射为类似整数的类型(取决于数据库类型)。但是我们不希望所有列都限制为 varchars 或整数,这个时候可以做一些修改。 + + +```typescript +// entity/photo.entity.ts +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class Photo { + + @PrimaryGeneratedColumn() + id: number; + + @Column({ + length: 100 + }) + name: string; + + @Column('text') + description: string; + + @Column() + filename: string; + + @Column("double") + views: number; + + @Column() + isPublished: boolean; +} +``` + + +示例,不同列名 +```typescript +@Column({ + length: 100, + name: 'custom_name' +}) +name: string; +``` + + +此外还有有几种特殊的列类型可以使用: + + +- `@CreateDateColumn` 是一个特殊列,自动为实体插入日期。 +- `@UpdateDateColumn` 是一个特殊列,在每次调用实体管理器或存储库的save时,自动更新实体日期。 +- `@VersionColumn` 是一个特殊列,在每次调用实体管理器或存储库的save时自动增长实体版本(增量编号)。 +- `@DeleteDateColumn` 是一个特殊列,会在调用 soft-delete(软删除)时自动设置实体的删除时间。 + +比如: + +```typescript + @CreateDateColumn({ + type: 'timestamp', + }) + createdDate: Date; +``` + +列类型是特定于数据库的。您可以设置数据库支持的任何列类型。有关支持的列类型的更多信息,请参见[此处](https://github.com/typeorm/typeorm/blob/master/docs/entities.md#column-types)。 + +:::tip + +`CreateDateColumn` 和 `UpdateDateColumn` 是依靠第一次同步表结构时,创建列上的默认数据完成的插入日期功能,如果是自己创建的表,需要自行在列上加入默认数据。 + +::: + + + + +### 7、配置连接信息和实体模型 + + +请参考 [配置](/docs/env_config) 章节,增加配置文件。 + + +然后在 `config.default.ts` 中配置数据库连接信息。 +```typescript +// src/config/config.default.ts +import { Photo } from '../entity/photo.entity'; + +export default { + // ... + typeorm: { + dataSource: { + default: { + /** + * 单数据库实例 + */ + type: 'mysql', + host: '*******', + port: 3306, + username: '*******', + password: '*******', + database: undefined, + synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据 + logging: false, + + // 配置实体模型 + entities: [Photo], + + // 支持如下的扫描形式,为了兼容我们可以同时进行.js和.ts匹配 + entities: [ + 'entity', // 特定目录 + '**/*.entity.{j,t}s', // 通配加后缀匹配 + ] + } + } + }, +} +``` +:::tip + +- 1. 如果使用的数据库已经有表结构同步的功能,比如云数据库,最好不要开启。如果一定要使用,synchronize 配置最好仅在开发阶段,或者第一次使用,避免造成一致性问题。 +- 2. `entities` 字段配置已经经过框架处理,该字段配置请不要参考原始文档。 +::: + + + `type` 字段你可以使用其他的数据库类型,包括`mysql`, `mariadb`, `postgres`, `cockroachdb`, `sqlite`, `mssql`, `oracle`, `cordova`, `nativescript`, `react-native`, `expo`, or `mongodb` + + + 比如 sqlite,需要以下信息。 + + +```typescript +// src/config/config.default.ts +export default { + // ... + typeorm: { + dataSource: { + default: { + type: 'sqlite', + database: path.join(__dirname, '../../test.sqlite'), + synchronize: true, + logging: true, + // ... + } + } + }, +} +``` + + +:::info +注意:synchronize 字段用于同步表结构。使用 `synchronize: true` 进行生产模式同步是不安全的,在上线后,请把这个字段设置为 false。 +::: + + +### 8、使用 Model 插入数据库数据 + + +在常见的 Midway 文件中,使用 `@InjectEntityModel` 装饰器注入我们配置好的 Model。我们所需要做的只是: + + +- 1、创建实体对象 +- 2、执行 `save()` + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from '../entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // save + async savePhoto() { + // create a entity object + let photo = new Photo(); + photo.name = 'Me and Bears'; + photo.description = 'I am near polar bears'; + photo.filename = 'photo-with-bears.jpg'; + photo.views = 1; + photo.isPublished = true; + + // save entity + const photoResult = await this.photoModel.save(photo); + + // save success + console.log('photo id = ', photoResult.id); + } +} +``` + + +### 9、查询数据 + +更多的查询参数,请查询 [find文档](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/find-options.md)。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from '../entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhotos() { + + // find All + let allPhotos = await this.photoModel.find({}); + console.log("All photos from the db: ", allPhotos); + + // find first + let firstPhoto = await this.photoModel.findOne({ + where: { + id: 1 + } + }); + console.log("First photo from the db: ", firstPhoto); + + // find one by name + let meAndBearsPhoto = await this.photoModel.findOne({ + where: { name: "Me and Bears" } + }); + console.log("Me and Bears photo from the db: ", meAndBearsPhoto); + + // find by views + let allViewedPhotos = await this.photoModel.find({ + where: { views: 1 } + }); + console.log("All viewed photos: ", allViewedPhotos); + + let allPublishedPhotos = await this.photoModel.find({ + where: { isPublished: true } + }); + console.log("All published photos: ", allPublishedPhotos); + + // find and get count + let [allPhotos, photosCount] = await this.photoModel.findAndCount({}); + console.log("All photos: ", allPhotos); + console.log("Photos count: ", photosCount); + + } +} + +``` + + +### 10、更新数据库 + + +现在,让我们从数据库中加载一个 Photo,对其进行更新并保存。 + + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from '../entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + + let photoToUpdate = await this.photoModel.findOne({ + where: { + id: 1, + }, + }); + photoToUpdate.name = "Me, my friends and polar bears"; + + await this.photoModel.save(photoToUpdate); + } +} +``` + +### 11、删除数据 + +`remove` 用于删除给定的实体或实体数组。`delete` 用于按给定的 ID 或者条件删除。 + + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from '../entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + /*...*/ + const photo = await this.photoModel.findOne({ + where: { + id: 1, + }, + }); + + // 删除单个 + await this.photoModel.remove(photo) + // 删除多个 + await this.photoModel.remove([photo1, photo2, photo3]); + + // 按 id 删除 + await this.photoModel.delete(1); + await this.photoModel.delete([1, 2, 3]); + await this.photoModel.delete({ name: "Timber" }); + } +} +``` +现在,ID = 1的 Photo 将从数据库中删除。 + + +此外还有软删除的方法。 +```typescript +await this.photoModel.softDelete(1); +// 使用 restore 方法恢复; +await this.photoModel.restore(1); +``` + + +### 12、创建一对一关联 + + +让我们与另一个类创建一对一的关系。让我们在 `entity/photoMetadata.entity.ts` 中创建一个新类。这个类包含 photo 的其他元信息。 + + +```typescript +import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm'; +import { Photo } from './photo.entity'; + +@Entity() +export class PhotoMetadata { + + @PrimaryGeneratedColumn() + id: number; + + @Column("int") + height: number; + + @Column("int") + width: number; + + @Column() + orientation: string; + + @Column() + compressed: boolean; + + @Column() + comment: string; + + @OneToOne(type => Photo) + @JoinColumn() + photo: Photo; + +} +``` + + +在这里,我们使用一个名为 `@OneToOne` 的新装饰器。它允许我们在两个实体之间创建一对一的关系。`type => Photo`是一个函数,它返回我们要与其建立关系的实体的类。 + + +由于语言的特殊性,我们被迫使用一个返回类的函数,而不是直接使用该类。我们也可以将其写为 `() => Photo` ,但是我们使用 `type => Photo`作为惯例来提高代码的可读性。类型变量本身不包含任何内容。 + + +我们还添加了一个 `@JoinColumn`装饰器,它指示关系的这一侧将拥有该关系。关系可以是单向或双向的。关系只有一方可以拥有。关系的所有者端需要使用@JoinColumn装饰器。 如果您运行该应用程序,则会看到一个新生成的表,该表将包含一列,其中包含用于 Photo 关系的外键。 + + +``` ++-------------+--------------+----------------------------+ +| photo_metadata | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| height | int(11) | | +| width | int(11) | | +| comment | varchar(255) | | +| compressed | boolean | | +| orientation | varchar(255) | | +| photoId | int(11) | FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +接下去我们要在代码中关联他们。 + + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from './entity/photo.entity'; +import { PhotoMetadata } from './entity/photoMetadata.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + @InjectEntityModel(PhotoMetadata) + photoMetadataModel: Repository; + + async updatePhoto() { + + // create a photo + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.isPublished = true; + + // create a photo metadata + let metadata = new PhotoMetadata(); + metadata.height = 640; + metadata.width = 480; + metadata.compressed = true; + metadata.comment = "cybershoot"; + metadata.orientation = "portrait"; + metadata.photo = photo; // this way we connect them + + + // first we should save a photo + await this.photoModel.save(photo); + + // photo is saved. Now we need to save a photo metadata + await this.photoMetadataModel.save(metadata); + + // done + console.log("Metadata is saved, and relation between metadata and photo is created in the database too"); + } +} +``` + + +### 13、反向关系映射 + + +关系映射可以是单向或双向的。当在 PhotoMetadata 和 Photo之间的关系是单向的。关系的所有者是PhotoMetadata,而 Photo对 PhotoMetadata 是一无所知的。这使得从 Photo 端访问 PhotoMetadata 变得很复杂。若要解决此问题,我们添加一个反向的关系映射,使 PhotoMetadata 和 Photo之间变成双向关联。让我们修改我们的实体。 + + +```typescript +import { Entity } from 'typeorm'; +import { Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm'; +import { Photo } from './photo'; + +@Entity() +export class PhotoMetadata { + + /* ... other columns */ + + @OneToOne(type => Photo, photo => photo.metadata) + @JoinColumn() + photo: Photo; +} +``` +```typescript +import { Entity } from 'typeorm'; +import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from 'typeorm'; +import { PhotoMetadata } from './photoMetadata.entity'; + +@Entity() +export class Photo { + + /* ... other columns */ + + @OneToOne(type => PhotoMetadata, photoMetadata => photoMetadata.photo) + metadata: PhotoMetadata; +} +``` +`photo => photo.metadata` 是一个返回反向映射关系的函数。在这里,我们显式声明 Photo 类的 metadata 属性用于关联 PhotoMetadata。除了传递返回 photo 属性的函数外,您还可以直接将字符串传递给 `@OneToOne` 装饰器,例如 `“metadata”` 。但是我们使用了这种函数回调的方法来让我们的代码写法更简单。 + + +请注意,只会在关系映射的一侧使用 `@JoinColumn` 装饰器。无论您放置此装饰器的哪一侧,都是关系的所有者。关系的拥有方在数据库中包含带有外键的列。 + + +### 14、加载对象及其依赖关系 + + +现在,让我们尝试在单个查询中一起加载出 Photo 和 PhotoMetadata。有两种方法可以执行此操作,使用 `find *` 方法或使用 `QueryBuilder` 功能。让我们首先使用 `find *` 方法。 `find *` 方法允许您使用 `FindOneOptions` / `FindManyOptions` 接口指定对象。 + + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from './entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhoto() { + /*...*/ + let photos = await this.photoModel.find({ relations: [ 'metadata' ] }); // typeorm@0.2.x + } +} + +``` +在这里,photos 的值是一个数组,包含了整个数据库的查询结果,并且每个 photo 对象都包含其关联的 metadata 属性。在[此文档](https://github.com/typeorm/typeorm/blob/master/docs/find-options.md)中了解有关 `Find Options` 的更多信息。 + + +使用 `Find Options` 很简单,但如果需要更复杂的查询,则应改用 `QueryBuilder` 。 `QueryBuilder` 允许以优雅的方式使用更复杂的查询。 + + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from './entity/photo.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhoto() { + /*...*/ + let photos = await this.photoModel + .createQueryBuilder('photo') + .innerJoinAndSelect('photo.metadata', 'metadata') + .getMany(); + } +} +``` +`QueryBuilder`允许创建和执行几乎任何复杂的 SQL 查询。使用 `QueryBuilder` 时,请像创建 SQL 查询一样思考。在此示例中,“photo” 和 “metadata” 是应用于所选 photos 的别名。您可以使用别名来访问所选数据的列和属性。 + + +### 15、使用级联操作自动保存关联对象 + + +在我们希望在每次保存另一个对象时都自动保存关联的对象,这个时候可以在关系中设置级联。让我们稍微更改照片的 `@OneToOne` 装饰器。 + + +```typescript +export class Photo { + /// ... other columns + + @OneToOne(type => PhotoMetadata, metadata => metadata.photo, { + cascade: true, + }) + metadata: PhotoMetadata; +} +``` +使用 `cascade` 允许我们现在不再单独保存 Photo 和 PhotoMetadata,由于级联选项,元数据对象将被自动保存。 + + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from './entity/photo.entity'; +import { PhotoMetadata } from './entity/photoMetadata.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + + // create photo object + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.isPublished = true; + + // create photo metadata object + let metadata = new PhotoMetadata(); + metadata.height = 640; + metadata.width = 480; + metadata.compressed = true; + metadata.comment = "cybershoot"; + metadata.orientation = "portrait"; + + photo.metadata = metadata; // this way we connect them + + // save a photo also save the metadata + await this.photoModel.save(photo); + + // done + console.log("Photo is saved, photo metadata is saved too"); + } +} +``` + + +注意,我们现在设置 Photo 的元数据,而不需要像之前那样设置元数据的 Photo 属性。这仅当您从 Photo 这边将 Photo 连接到 PhotoMetadata 时,级联功能才有效。如果在 PhotoMetadata 侧设置,则不会自动保存。 + + +### 16、创建多对一/一对多关联 + + +让我们创建一个多对一/一对多关系。假设一张照片有一个作者,每个作者可以有很多照片。首先,让我们创建一个 Author 类: +```typescript +import { Entity } from 'typeorm'; +import { Column, PrimaryGeneratedColumn, OneToMany, JoinColumn } from 'typeorm'; +import { Photo } from './entity/photo.entity'; + +@Entity() +export class Author { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @OneToMany(type => Photo, photo => photo.author) // note: we will create author property in the Photo class below + photos: Photo[]; +} +``` +`Author` 包含了一个反向关系。 `OneToMany` 和 `ManyToOne` 需要成对出现。 + + +现在,将关系的所有者添加到 Photo 实体中: +```typescript +import { Entity } from 'typeorm'; +import { Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; +import { PhotoMetadata } from './photoMetadata.entity'; +import { Author } from './author.entity'; + +@Entity() +export class Photo { + + /* ... other columns */ + + @ManyToOne(type => Author, author => author.photos) + author: Author; +} +``` + + +在多对一/一对多关系中,所有者方始终是多对一。这意味着使用 `@ManyToOne` 的类将存储相关对象的 ID。 + + +运行应用程序后,ORM 将创建 `author` 表: + + +``` ++-------------+--------------+----------------------------+ +| author | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| name | varchar(255) | | ++-------------+--------------+----------------------------+ +``` +它还将修改 `photo` 表,添加新的 `author` 列并为其创建外键: +``` ++-------------+--------------+----------------------------+ +| photo | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| name | varchar(255) | | +| description | varchar(255) | | +| filename | varchar(255) | | +| isPublished | boolean | | +| authorId | int(11) | FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +### 17、创建多对多关联 + + +让我们创建一个多对一/多对多关系。假设一张照片可以在许多相册中,并且每个相册可以包含许多照片。让我们创建一个 `Album` 类。 + + +```typescript +import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm'; + +@Entity() +export class Album { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @ManyToMany(type => Photo, photo => photo.albums) + @JoinTable() + photos: Photo[]; +} +``` + + +`@JoinTable` 用来指明这是关系的所有者。 + + +现在,将反向关联添加到 `Photo` 。 + + +```typescript +export class Photo { + /// ... other columns + + @ManyToMany(type => Album, album => album.photos) + albums: Album[]; +} +``` +运行应用程序后,ORM将创建一个 album_photos_photo_albums 联结表: + + +``` ++-------------+--------------+----------------------------+ +| album_photos_photo_albums | ++-------------+--------------+----------------------------+ +| album_id | int(11) | PRIMARY KEY FOREIGN KEY | +| photo_id | int(11) | PRIMARY KEY FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +现在,让我们将相册和照片插入数据库: + + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Photo } from './entity/photo.entity'; +import { Album } from './entity/album.entity'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + @InjectEntityModel(Album) + albumModel: Repository + + async updatePhoto() { + + // create a few albums + let album1 = new Album(); + album1.name = "Bears"; + await this.albumModel.save(album1); + + let album2 = new Album(); + album2.name = "Me"; + await this.albumModel.save(album2); + + // create a few photos + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.albums = [album1, album2]; + await this.photoModel.save(photo); + + + // now our photo is saved and albums are attached to it + // now lets load them: + const loadedPhoto = await this.photoModel.findOne(1, { relations: ["albums"] }); // typeorm@0.2.x + } +} +``` +`loadedPhoto` 的值为: +```json +{ + id: 1, + name: "Me and Bears", + description: "I am near polar bears", + filename: "photo-with-bears.jpg", + albums: [{ + id: 1, + name: "Bears" + }, { + id: 2, + name: "Me" + }] +} +``` + +### 18、使用 QueryBuilder + + +您可以使用QueryBuilder来构建几乎任何复杂的SQL查询。例如,您可以这样做: + + +```typescript +let photos = await this.photoModel + .createQueryBuilder("photo") // first argument is an alias. Alias is what you are selecting - photos. You must specify it. + .innerJoinAndSelect("photo.metadata", "metadata") + .leftJoinAndSelect("photo.albums", "album") + .where("photo.isPublished = true") + .andWhere("(photo.name = :photoName OR photo.name = :bearName)") + .orderBy("photo.id", "DESC") + .skip(5) + .take(10) + .setParameters({ photoName: "My", bearName: "Mishka" }) + .getMany(); +``` +该查询选择所有带有 “My” 或 “Mishka” 名称的已发布照片。它将从位置 5 开始返回结果(分页偏移),并且将仅选择 10 个结果(分页限制)。选择结果将按 ID 降序排列。该照片的相册将 left-Joined,元数据将自动关联。 + + +您将在应用程序中大量使用查询生成器。在 [此处](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/select-query-builder.md) 了解有关QueryBuilder的更多信息。 + + +### 19、Event Subscriber + + +typeorm 提供了一个事件订阅机制,方便在做一些数据库操作时的日志输出,为此 midway 提供了一个 `EventSubscriberModel` 装饰器,用来标注事件订阅类,代码如下。 + + +```typescript +import { EventSubscriberModel } from '@midwayjs/typeorm'; +import { EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm'; + +@EventSubscriberModel() +export class EverythingSubscriber implements EntitySubscriberInterface { + + /** + * Called before entity insertion. + */ + beforeInsert(event: InsertEvent) { + console.log(`BEFORE ENTITY INSERTED: `, event.entity); + } + + /** + * Called before entity insertion. + */ + beforeUpdate(event: UpdateEvent) { + console.log(`BEFORE ENTITY UPDATED: `, event.entity); + } + + /** + * Called before entity insertion. + */ + beforeRemove(event: RemoveEvent) { + console.log(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); + } + + /** + * Called after entity insertion. + */ + afterInsert(event: InsertEvent) { + console.log(`AFTER ENTITY INSERTED: `, event.entity); + } + + /** + * Called after entity insertion. + */ + afterUpdate(event: UpdateEvent) { + console.log(`AFTER ENTITY UPDATED: `, event.entity); + } + + /** + * Called after entity insertion. + */ + afterRemove(event: RemoveEvent) { + console.log(`AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); + } + + /** + * Called after entity is loaded. + */ + afterLoad(entity: any) { + console.log(`AFTER ENTITY LOADED: `, entity); + } + +} +``` + +这个订阅类提供了一些常用的接口,用来在数据库操作时执行一些事情。 + +同时,我们需要把订阅类加到配置中。 + +```typescript +// src/config/config.default.ts +import { EverythingSubscriber } from '../event/subscriber'; + +export default { + // ... + typeorm: { + dataSource: { + default: { + // ... + entities: [Photo], + // 传入订阅类 + subscribers: [EverythingSubscriber] + } + } + }, +} +``` + + + +## Repository API + +更多 API 请查看 [官网文档](https://github.com/typeorm/typeorm/blob/master/docs/repository-api.md)。 + + + + +## 高级功能 +### 多数据库支持 + + +有时候,我们一个应用中会有多个数据库连接(Connection)的情况,这个时候会有多个配置。我们使用 DataSource 标准的 **对象的形式 **来定义配置。 + + +比如下面定义了 `default` 和 `test` 两个数据库连接(Connection)。 + + +```typescript +import { join } from 'path'; + +export default { + typeorm: { + dataSource: { + default: { + type: 'sqlite', + database: join(__dirname, '../../default.sqlite'), + // ... + }, + test: { + type: 'mysql', + host: '127.0.0.1', + port: 3306, + // ... + } + } + } +} +``` + + +在使用时,需要指定模型归属于哪个连接(Connection)。 +```typescript +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { User } from './entity/user.entity'; + +export class XXX { + + @InjectEntityModel(User, 'test') + testUserModel: Repository; + + //... +} +``` + + + +### 列值转换 + +我们可以在实体定义中处理列值转换。 + +利用列装饰器的 `transformer` 参数,可以进行出入参的处理,比如对时间格式化。 + +```typescript +import { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import * as dayjs from 'dayjs'; + +const dateTransformer = { + from: (value: Date | number) => { + return dayjs(typeof value === 'number' ? value: value.getTime()).format('YYYY-MM-DD HH:mm:ss'); + }, + to: () => new Date(), +}; + +@Entity() +export class Photo { + // ... + + @CreateDateColumn({ + type: 'timestamp', + transformer: dateTransformer, + }) + createdAt: Date; +} + +``` + + + +### 指定默认数据源 + +在包含多个数据源时,可以指定默认的数据源。 + +```typescript +export default { + // ... + typeorm: { + dataSource: { + default1: { + // ... + }, + default2: { + // ... + }, + }, + // 多个数据源时可以用这个指定默认的数据源 + defaultDataSourceName: 'default1', + }, +}; +``` + + + +### 获取数据源 + +数据源即创建出的 DataSource 对象,我们可以通过注入内置的数据源管理器来获取。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import { TypeORMDataSourceManager } from '@midwayjs/typeorm'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + + async onReady(container: IMidwayContainer) { + const dataSourceManager = await container.getAsync(TypeORMDataSourceManager); + const conn = dataSourceManager.getDataSource('default'); + console.log(dataSourceManager.isConnected(conn)); + } +} +``` + +从 v3.8.0 开始,也可以通过装饰器注入。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import { InjectDataSource } from '@midwayjs/typeorm'; +import { DataSource } from 'typeorm'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + + // 注入默认数据源 + @InjectDataSource() + defaultDataSource: DataSource; + + // 注入自定义数据源 + @InjectDataSource('default1') + customDataSource: DataSource; + + async onReady(container: IMidwayContainer) { + // ... + } +} +``` + + + +### 日志 + +数据源在未配置日志对象时,组件会自动创建一个 `typeormLogger`,用于保存执行的 SQL 信息,方便排查问题和 SQL 审核。 + +默认配置为: + +```typescript +export default { + midwayLogger: { + clients: { + typeormLogger: { + fileLogName: 'midway-typeorm.log', + enableError: false, + level: 'info', + }, + }, + } +} +``` + +我们可以使用普通日志的配置方式进行调整,如果不希望生成日志,可以配置关闭。 + +```typescript +export default { + // ... + typeorm: { + default: { + // 所有数据源关闭 + logging: false, + }, + dataSource: { + default: { + // 单个数据源关闭 + logging: false, + }, + }, + }, +}; +``` + + + +### 事务 + +typeorm 的事务需要先获取到数据源,然后开启事务。 + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { TypeORMDataSourceManager } from '@midwayjs/typeorm'; +import { UserDTO } from '../entity/user'; + +@Provide() +export class UserService { + + @Inject() + dataSourceManager: TypeORMDataSourceManager; + + async updateUser(user: UserDTO) { + + // get dataSource + const dataSource = this.dataSourceManager.getDataSource('default'); + + // start transaction + await dataSource.transaction(async (transactionalEntityManager) => { + // run code + await transactionalEntityManager.save(UserDTO, user); + }); + } + +} +``` + +更多的细节,可以参考 [文档](https://github.com/typeorm/typeorm/blob/master/docs/transactions.md)。 + + + +### CLI + +TypeORM 默认提供了一个 CLI,用来创建 entity,migration 等,更多文档请查看 [这里](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/using-cli.md)。 + +由于 TypeORM 的默认配置和 Midway 不同,我们提供了一个简单的修改版本,用于适配 Midway 的数据源配置。 + +检查安装情况: + +```bash +$ npx mwtypeorm -h +``` + +常用的命令有 + + **创建空 Entity** + +将会创建一个 `src/entity/User.ts` 文件。 + +```bash +$ npx mwtypeorm entity:create src/entity/User +``` + +**创建 Migration** + +将会根据现有数据源生成一个 `src/migration/******-photo.entity.ts` 文件。 + +比如配置如下: + +```typescript +export default { + typeorm: { + dataSource: { + 'default': { + // ... + entities: [ + '**/entity/*.entity{.ts,.js}' + ], + migrations: [ + '**/migration/*.ts' + ], + }, + }, +} +``` + +可以执行下面的命令,将修改后的 Entity 生成迁移文件。 + +```bash +$ npx mwtypeorm migration:generate -d ./src/config/config.default.ts src/migration/photo +``` + +:::caution + +注意:上面的 entities 配置由于需要再 CLI 和 Midway 间复用,采用了两者都支持的扫描写法。 + +::: + + + +### 关于表结构同步 + + +- 如果你已有表结构,想自动创建 Entity,使用 [生成器](https://www.npmjs.com/package/typeorm-model-generator) +- 如果已经有 Entity 代码,想创建表结构请使用配置中的 `synchronize: true` ,注意可能会丢失数据 +- 如果已经上线,但是又修改了表结构,可以使用 CLI 中的 `migration:generate` + + + +## 常见问题 + + +### Handshake inactivity timeout + + +一般是网络原因,如果本地出现,可以 ping 但是telnet不通,可以尝试执行如下命令: +```bash +$ sudo sysctl -w net.inet.tcp.sack=0 +``` + + + +### 关于 mysql 时间列的时区展示 + +一般情况下,数据库中保存的是 UTC 时间,如果你希望返回当前时区的时间,可以使用下面的方式 + +**1、检查 mysql 数据库所在的环境** + +比如下面默认的时区其实就是系统 UTC 时间,可以调整为 `+08:00`。 + +```text +mysql> show global variables like '%time_zone%'; ++------------------+--------+ +| Variable_name | Value | ++------------------+--------+ +| system_time_zone | UTC | +| time_zone | SYSTEM | ++------------------+--------+ +2 rows in set (0.05 sec) + +``` + +**2、检查服务代码部署的环境** + +尽量和数据库所在的环境一致,如果不一致,请在配置中设置 `timezone` (设置为和 mysql 一致)。 + +```typescript +export default { + typeorm: { + dataSource: { + default: { + type: 'mysql', + // ... + timezone: '+08:00', + }, + }, + }, +} +``` + + + +### 时间列返回字符串 + +配置 dateStrings 可以使 mysql 返回时间按 DATETIME 格式返回,只对 mysql 生效。 + +```typescript +// src/config/config.default.ts +export default { + // ... + typeorm: { + dataSource: { + default: { + //... + dateStrings: true, + } + } + }, +} +``` + +如果使用了 `@CreateDateColumn` 和 `@UpdateDateColumn` ,可以调整实体返回类型。 + +```typescript +@UpdateDateColumn({ + name: "gmt_modified", + type: 'timestamp' +}) +gmtModified: string; + +@CreateDateColumn({ + name: "gmt_create", + type: 'timestamp', +}) +gmtCreate: string; +``` + + + +效果如下: + +**配置前:** + +```typescript +gmtModified: 2021-12-13T03:49:43.000Z, +gmtCreate: 2021-12-13T03:49:43.000Z +``` +**配置后:** + +```typescript +gmtModified: '2021-12-13 11:49:43', +gmtCreate: '2021-12-13 11:49:43' +``` + + + +### 同时安装 mysql 和 mysql2 + +在 node_modules 中同时有 mysql 和 mysql2 时,typeorm 会自动加载 mysql,而不是 mysql2。 + +这个时候如需使用 mysql2,请指定 driver。 + +```typescript +// src/config/config.default.ts +export default { + // ... + typeorm: { + dataSource: { + default: { + //... + type: 'mysql', + driver: require('mysql2'), + } + } + }, +} +``` + + + + +### Cannot read properties of undefined (reading 'getRepository') + +一般是配置不正确,可以考虑两方面的配置: + +- 1、检查 `config.default.ts` 中的 `entities` 配置是否正确 +- 2、检查 `configuration.ts` 文件,确认是否引入 orm diff --git a/site/versioned_docs/version-3.0.0/extensions/oss.md b/site/versioned_docs/version-3.0.0/extensions/oss.md new file mode 100644 index 000000000000..5086aef63d75 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/oss.md @@ -0,0 +1,261 @@ +# 阿里云对象存储(OSS) + +阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。其数据设计持久性不低于 99.999999999%,服务设计可用性不低于 99.99%。具有与平台无关的 RESTful API 接口,您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。 + +`@midwayjs/oss` 组件就是在 midway 体系下用于对接 OSS 服务的 sdk。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 前置条件 + + +使用 OSS 组件,你需要提前申请一个 OSS Bucket。Bucket 是 OSS 的存储库的概念,你的文件都将存储在这个库里。 + + +- OSS 对象存储官网:[https://www.aliyun.com/product/oss](https://www.aliyun.com/product/oss) +- 什么是对象存储:[https://www.alibabacloud.com/help/zh/doc-detail/31817.htm](https://www.alibabacloud.com/help/zh/doc-detail/31817.htm) + + +## 安装依赖 + +`@midwayjs/oss` 是主要的功能包。 + +```bash +$ npm i @midwayjs/oss@3 --save +``` +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/oss": "^3.0.0", + // ... + }, +} +``` + + + + +## 引入组件 + + +首先,引入 组件,在 `configuration.ts` 中导入: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as oss from '@midwayjs/oss'; +import { join } from 'path' + +@Configuration({ + imports: [ + // ... + oss // 导入 oss 组件 + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + +## 配置 OSS + + +OSS 组件需要配置后才能使用。需要填写 OSS 的 bucket、accessKeyId、accessKeySecret 等必要信息。 + + +支持普通 oss 客户端和 oss 集群客户端,基于 [ali-oss](https://github.com/ali-sdk/ali-oss/) 这个包。 + + +比如: + +**普通的 oss bucket 配置** +```typescript +// src/config/config.default +export default { + // ... + oss: { + // normal oss bucket + client: { + accessKeyId: 'your access key', + accessKeySecret: 'your access secret', + bucket: 'your bucket name', + endpoint: 'oss-cn-hongkong.aliyuncs.com', + timeout: '60s', + }, + }, +} +``` + + +**集群(cluster) 模式的 oss bucket 配置,需要配置多个** + +```typescript +// src/config/config.default +export default { + // ... + oss: { + // need to config all bucket information under cluster + client: { + clusters: [{ + endpoint: 'host1', + accessKeyId: 'id1', + accessKeySecret: 'secret1', + }, { + endpoint: 'host2', + accessKeyId: 'id2', + accessKeySecret: 'secret2', + }], + schedule: 'masterSlave', //default is `roundRobin` + timeout: '60s', + }, + }, +} +``` + +**STS 模式** +```typescript +// src/config/config.default +export default { + // ... + oss: { + // if config.sts == true, oss will create STS client + client: { + sts: true, + accessKeyId: 'your access key', + accessKeySecret: 'your access secret', + }, + }, +} +``` + +## 使用组件 + + +可以直接获取 `OSSService`,然后调用接口,比如,保存文件。 +```typescript +import { OSSService } from '@midwayjs/oss'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + ossService: OSSService; + + async saveFile() { + + const localFile = join(__dirname, 'test.log'); + const result = await this.ossService.put('/test/test.log', localFile); + + // => result.url + } +} +``` + + +如果配置的是 STS 模式,客户端可以使用 `OSSSTSService` 。 +```typescript +import { OSSSTSService } from '@midwayjs/oss'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + stsService: OSSSTSService; + + async saveFile() { + + const roleArn = '******'; // 这里是阿里云角色的 arn + const result = await this.stsService.assumeRole(roleArn); + + // result.credentials.AccessKeyId + // result.credentials.AccessKeySecret; + // result.credentials.SecurityToken; + } +} +``` + +更多的 OSS 客户端 API,请查看 [OSS 文档](https://github.com/ali-sdk/ali-oss)。 + + +## 使用多个 OSS Bucket + + +有些应用需要访问多个 oss bucket,那么就需要配置 `oss.clients`。 +```typescript +// src/config/config.default +export default { + // ... + oss: { + clients: { + bucket1: { + bucket: 'bucket1', + // ... + }, + bucket2: { + bucket: 'bucket2', + // ... + }, + }, + // client, clients,createInstance 方法共享的配置 + default: { + endpoint: '', + accessKeyId: '', + accessKeySecret: '', + }, + }, + // other custom config + bucket3: { + bucket: 'bucket3', + // ... + }, +} +``` + +可以使用 `OSSServiceFactory` 获取不同的实例。 + +```typescript +import { OSSServiceFactory } from '@midwayjs/oss'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + ossServiceFactory: OSSServiceFactory; + + @Config('bucket3') + bucket3Config; + + async saveFile() { + + // 默认获取的类型是 OSSService + const bucket1 = this.ossServiceFactory.get('bucket1'); + const bucket2 = this.ossServiceFactory.get('bucket2'); + + // 如果是 STS,需要设置泛型联系 + // const bucket1 = this.ossServiceFactory.get('bucket1'); + + // 会合并 config.bucket3 和 config.oss.default + const bucket3 = await this.ossServiceFactory.createInstance(this.bucket3Config, 'bucket3'); + // 传了名字之后也可以从 factory 中获取 + bucket3 = this.ossServiceFactory.get('bucket3'); + + } +} +``` diff --git a/site/versioned_docs/version-3.0.0/extensions/otel.md b/site/versioned_docs/version-3.0.0/extensions/otel.md new file mode 100644 index 000000000000..1c4139bd5b8c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/otel.md @@ -0,0 +1,338 @@ +# 链路追踪 + +Midway 采用社区最新的 [open-telemetry](https://opentelemetry.io/) 方案,其前身是知名的 OpenTracing 和 OpenCensus 规范,现阶段也是 CNCF 的孵化项目,社区许多知名的大公司如 Amazon,Dynatrace,Microsoft,Google,Datadog,Splunk 等都有使用。 + + [open-telemetry](https://opentelemetry.io/) 提供了通用的 Node.js 接入方案,以供应商无关的方式将数据接收,处理,导出,支持向一个或多个开源或者商业化的采集端发送可观测的数据(比如阿里云 SLS,Jaeger,Prometheus,Fluent Bit 等)。 + +Midway 提供了接入 [open-telemetry](https://opentelemetry.io/) 的 Node.js 方案,并提供了一些简单的使用 API。 + +:::info + +[open-telemetry](https://opentelemetry.io/) 的 Tracing 部分当前 Node.js SDK 已经 Release 1.0.0,可以在生产使用,Metrics 部分未正式发布,我们依旧在跟进(编码)中。 + +::: + + + +## 使用须知 + +[open-telemetry](https://opentelemetry.io/) 基于 Node.js 的 Async_Hooks 的稳定 API 实现,经过我们的测试,在最新的 Node.js v14/v16 性能影响已经很小,可以在生产使用,在 v12 情况下虽然可以使用,但是性能依旧有不小的损失,请尽可能在 Node.js >= v14 的版本下使用。 + + + +## 安装基础依赖 + +```bash +# Node.js 的 api 抽象 +$ npm install --save @opentelemetry/api + +# Node.js 的 api 实现 +$ npm install --save @opentelemetry/sdk-node + +# 常用 Node.js 模块的埋点实现 +$ npm install --save @opentelemetry/auto-instrumentations-node + +# jaeger 输出器 +$ npm install --save @opentelemetry/exporter-jaeger +``` + +以上的包均为 [open-telemetry](https://opentelemetry.io/) 的官方包。 + + + +## 启用 open-telemetry + + [open-telemetry](https://opentelemetry.io/) 的模块请尽可能加在代码的最开始(比框架还要早),所以在不同场景中,我们有不同的添加方式。 + + + +### 使用 bootstrap 部署 + +如果使用 `bootstrap.js` 部署,你可以加在 `bootstrap.js` 的最顶部,示例代码如下。 + +```typescript +const process = require('process'); +const { NodeSDK, node, resources } = require('@opentelemetry/sdk-node'); +const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); +const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions') +const { JaegerExporter } = require('@opentelemetry/exporter-jaeger') + +// Midway 启动文件 +const { Bootstrap } = require('@midwayjs/bootstrap'); + +// https://www.npmjs.com/package/@opentelemetry/exporter-jaeger +const tracerAgentHost = process.env['TRACER_AGENT_HOST'] || '127.0.0.1' +const jaegerExporter = new JaegerExporter({ + host: tracerAgentHost, +}); + +// 初始化一个 open-telemetry 的 SDK +const sdk = new NodeSDK({ + // 设置追踪服务名 + resource: new resources.Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'my-app', + }), + // 配置当前的导出方式,比如这里配置了一个输出到控制台的,也可以配置其他的 Exporter,比如 Jaeger + traceExporter: new node.ConsoleSpanExporter(), + // 配置当前导出为 jaeger + // traceExporter: jaegerExporter, + + // 这里配置了默认自带的一些监控模块,比如 http 模块等 + // 若初始化时间很长,可注销此行,单独配置需要的 instrumentation 条目 + instrumentations: [getNodeAutoInstrumentations()] +}); + +// 初始化 SDK,成功启动之后,再启动 Midway 框架 +sdk.start() + +// 在进程关闭时,同时关闭数据采集 +process.on('SIGTERM', () => { + sdk.shutdown() + .then(() => console.log('Tracing terminated')) + .catch((error) => console.log('Error terminating tracing', error)) + .finally(() => process.exit(0)); +}); + +Bootstrap + .configure(/**/) + .run(); +``` + + + +### 使用 egg-scripts 部署 + +egg-scripts 由于未提供入口部署,必须采用 `--require` 的形式加载额外的文件。 + +我们在根目录添加一个 `otel.js` (注意是 js 文件),内容如下。 + +```javascript +const process = require('process'); +const { NodeSDK, node, resources } = require('@opentelemetry/sdk-node'); +const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); + +// 初始化一个 open-telemetry 的 SDK +const sdk = new NodeSDK({ + // 配置当前的导出方式,比如这里配置了一个输出到控制台的,也可以配置其他的 Exporter,比如 Jaeger + traceExporter: new node.ConsoleSpanExporter(), + // 这里配置了默认自带的一些监控模块,比如 http 模块等 + instrumentations: [getNodeAutoInstrumentations()] +}); + +// 初始化 SDK +sdk.start() + +// 在进程关闭时,同时关闭数据采集 +process.on('SIGTERM', () => { + sdk.shutdown() + .then(() => console.log('Tracing terminated')) + .catch((error) => console.log('Error terminating tracing', error)) + .finally(() => process.exit(0)); +}); +``` + +修改 `package.json` 中的启动命令。 + +```json +{ + // ... + "scripts": { + "start": "egg-scripts start --daemon --title=**** --framework=@midwayjs/web --require=./otel.js", + }, +} +``` + +### 开发调试入口 + +`midway-bin` 使用 `--entryFile` 参数指定入口文件 + +例如 `package.json` 文件 +```json +{ + "scripts": { + "start": "cross-env NODE_ENV=local midway-bin dev --ts --entryFile=bootstrap.js" + } +} +``` + +## 常用概念 + +[open-telemetry](https://opentelemetry.io/) 提供了一些抽象封装,将监控的整个过程包装为几个步骤,每个步骤都可自定义配置,其也有一些用户不太理解的术语,在下面做一些解释。 + +完整的英文概念请参考 [Concepts](https://opentelemetry.io/docs/concepts/)。 + + + +### API + +用于生成和关联 Tracing、Metrics 和 Logs 记录数据的数据类型和操作的一组 API 抽象,具体表现为 `@opentelemetry/api` 这个包,里面是一些接口和空实现。 + +### SDK + +API 的特定语言实现,比如 Node.js 的实现(`@opentelemetry/sdk-node`),其他监控平台的采集 SDK 实现等等。 + +### Instrumentations + +[open-telemetry](https://opentelemetry.io/) 提供了一些常见库的 shim 代码,使用 hooks 或者 monkey-patching 的方法来拦截方法,自动在特定方法调用时保存链路数据,支持 http,gRPC , redis,mysql 等模块,用户直接配置即可使用。 + +比如上面示例引入的 `@opentelemetry/auto-instrumentations-node` 就是一个已经默认封装好常用库的 instrumentations 集合包,里面包括了大部分会用到的库,具体的依赖请参考 [Github](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/metapackages/auto-instrumentations-node/package.json)。 + +### Exporter + +将接收到的链路数据发送到特定端的实现,比如 Jaeger,zipkin 等。 + + + +## 示例 + + + +### 添加三方 instrumentation + +在 SDK 初始化时,添加到 `instrumentations ` 数组中即可。 + +```typescript +const { RedisInstrumentation } = require('@opentelemetry/instrumentation-redis'); +// ... + +// 初始化一个 open-telemetry 的 SDK +const sdk = new NodeSDK({ + // ... + + // 这里仅是添加的示例,如果使用了 auto-instrumentations-node,已经包含了下面的 instrumentation + instrumentations: [ + new RedisInstrumentation(), + ] +}); +``` + + + +### 添加 Jaeger Exporter + +这里以 Jaeger Exporter 作为示例,其他 Exporter 类似。 + +先添加依赖。 + +```bash +$ npm install --save @opentelemetry/exporter-jaeger @opentelemetry/propagator-jaeger +``` + +在 SDK 中配置。 + +```typescript +const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); +const { JaegerPropagator } = require('@opentelemetry/propagator-jaeger'); +// ... + +const exporter = new JaegerExporter({ + tags: [], // optional + // You can use the default UDPSender + host: 'localhost', // optional + port: 6832, // optional + // OR you can use the HTTPSender as follows + // endpoint: 'http://localhost:14268/api/traces', + maxPacketSize: 65000 // optional +}); + +// 初始化一个 open-telemetry 的 SDK +const sdk = new NodeSDK({ + traceExporter: exporter, + textMapPropagator: new JaegerPropagator() + // ... +}); +``` + +具体参数请参考: + +- [opentelemetry-exporter-jaeger](https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-exporter-jaeger/README.md) +- [opentelemetry-propagator-jaeger](https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-propagator-jaeger/README.md) + + + +### 阿里云 ARMS + +阿里云应用实时监控服务([ARMS](https://www.aliyun.com/product/arms/))已经支持了 open-telemetry 格式的指标,同时提供一个 sdk 进行接入。 + +首先,安装 `opentelemetry-arms`。 + +```bash +# arms sdk +$ npm install --save opentelemetry-arms +``` + +然后在启动时添加环境变量参数以及 `-r` 参数即可。 + +```bash +$ SERVICE_NAME=nodejs-opentelemetry-express AUTHENTICATION=**** ENDPOINT=grpc://**** node -r opentelemetry-arms bootstrap.js +``` + +:::tip + +- 1、这种方式接入,无需在` bootstrap.js` 中添加代码。 +- 2、默认 sdk 仅提供了 http/express/koa 模块的链路支持,未包含其他 instrumentations,如有需求,可以拷贝源码至 `bootstrap.js` 中自定义。 + +::: + + + +## 框架能力支持 + +注意,组件只是包裹了 otel 的接口,如果不需要下述接口使用,无需安装本组件 + +先安装依赖。 + +```bash +$ npm i @midwayjs/otel@3 --save +``` + +启用 `otel` 组件。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as otel from '@midwayjs/otel'; + +@Configuration({ + imports: [ + // ... + otel + ] +}) +export class MainConfiguration { +} +``` + + + +### ctx.traceId + +组件提供了 `ctx.traceId` 字段。 + +你可以在支持的组件下进行获取(egg/koa)。 + +```typescript +ctx.traceId => ***** +``` + + + +### 装饰器支持 + +Midway 针对用户侧的需求,添加一个装饰器用于增加链路节点。 + +Otel 组件提供了一个 @Trace 装饰器,可以添加在方法上。 + +```typescript +export class UserService { + + @Trace('user.get') + async getUser() { + // ... + } +} +``` + +该装饰器需要传入一个节点名字,这样链路会自动添加一个该方法的链路节点,并记录执行的时间,方法执行成功或者失败。 + + + diff --git a/site/versioned_docs/version-3.0.0/extensions/passport.md b/site/versioned_docs/version-3.0.0/extensions/passport.md new file mode 100644 index 000000000000..f30ff8d37bea --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/passport.md @@ -0,0 +1,491 @@ +# 身份验证 + +身份验证是大多数 Web 应用程序的重要组成部分。因此 Midway 封装了目前 Nodejs 中最流行的 Passport 库。 + +相关信息: + +| web 支持情况 | | +| ----------------- | --- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + +从 v3.4.0 开始 Midway 自行维护 passport,将不再需要引入社区包和类型包。 + +## 一些概念 + +passport 是社区使用较多的身份验证库,通过称为策略的可扩展插件进行身份验证请求。 + +它本身包含几个部分: + +- 1、验证的策略,比如 jwt 验证,github 验证,oauth 验证等,passport 最为丰富的也是这块 +- 2、执行策略之后,中间件的逻辑处理和配置,比如成功或者失败后的跳转,报错等 + +## 安装依赖 + +安装 `npm i @midwayjs/passport` 和相关策略依赖。 + +```bash +## 必选 +$ npm i @midwayjs/passport@3 --save + +## 可选 +## 下面安装本地策略 +$ npm i passport-local --save +$ npm i @types/passport-local --save-dev +## 下面安装 Github 策略 +$ npm i passport-github --save +## 下面安装 Jwt 策略 +$ npm i passport-jwt --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/passport": "^3.0.0", + // 本地策略 + "passport-local": "^1.0.0" + // Jwt 策略 + "passport-jwt": "^4.0.0", + // Github 策略 + "passport-github": "^1.1.0", + // ... + }, + "devDependencies": { + // 本地策略 + "@types/passport-local": "^1.0.34", + // Jwt 策略 + "@types/passport-jwt": "^3.0.6", + // Github 策略 + "@types/passport-github": "^1.1.7", + // ... + } +} +``` + +## 启用组件 + +首先启用组件。 + +```typescript +// src/configuration.ts + +import { join } from 'path'; +import { ILifeCycle } from '@midwayjs/core'; +import { Configuration } from '@midwayjs/core'; +import * as passport from '@midwayjs/passport'; + +@Configuration({ + imports: [ + // ... + passport, + ], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration implements ILifeCycle {} +``` + +## 策略示例 + +这里我们以使用本地认证策略,和 Jwt 策略作为演示。 + +### 示例:本地策略 + +我们以 `passport-local` 来介绍 Passport 策略在 Midway 中如何使用, `passport-local` 的官方文档示例如下,通过 `passport.use` 加载一个策略,策略的验证逻辑是一个 `verify` 方法,包含 callback 参数,其余的策略的参数都在构造器中。 + +```typescript +passport.use( + // 初始化一个策略 + new LocalStrategy({ + usernameField: 'username', + passwordField: 'password', + passReqToCallback: true, + session: false + }, + function verify(username, password, done) { + User.findOne({ username: username }, function (err, user) { + if (err) { return done(err); } + if (!user) { return done(null, false); } + if (!user.verifyPassword(password)) { return done(null, false); } + return done(null, user); + }); + } + ) +); +``` + +Midway 对此进行了改造,通过 `@CustomStrategy` 和 `PassportStrategy` 类继承一个 Passport 现有策略。 + +异步的 `validate` 方法代替原有的 `verify` 方法,`validate` 方法返回验证后的用户结果,方法的参数和原有对应的策略一致。 + +在 Midway 中编写的效果如下: + +```typescript +// src/strategy/local.strategy.ts + +import { CustomStrategy, PassportStrategy } from '@midwayjs/passport'; +import { Strategy, IStrategyOptions } from 'passport-local'; +import { Repository } from 'typeorm'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { UserEntity } from './user'; +import * as bcrypt from 'bcrypt'; + +@CustomStrategy() +export class LocalStrategy extends PassportStrategy(Strategy) { + @InjectEntityModel(UserEntity) + userModel: Repository; + + // 策略的验证 + async validate(username, password) { + const user = await this.userModel.findOneBy({ username }); + if (!user) { + throw new Error('用户不存在 ' + username); + } + if (!await bcrypt.compare(password, user.password)) { + throw new Error('密码错误 ' + username); + } + + return user; + } + + // 当前策略的构造器参数 + getStrategyOptions(): IStrategyOptions { + return { + usernameField: 'username', + passwordField: 'password', + passReqToCallback: true, + session: false + }; + } +} +``` + +:::tip + +注意:validate 方法是社区策略 verify 的 Promise 化替代方法,你无需在最后传递 callback 参数。 + +::: + +在 `passport-local` 的官方文档中,实现完策略后,需要作为中间件加载到业务中,比如: + +```typescript +app.post('/login/password', passport.authenticate('local', { + successRedirect: '/', + failureRedirect: '/login' +})); +``` + +:::tip + +这里的 `local` 是 `passport-local` 内部的名字。 + +::: + +在 Midway 中,也需要将上述实现的 `LocalStrategy` 通过中间件加载。 + +自定义一个中间件继承 `PassportMiddleware` 扩展出的基础中间件,示例如下。 + +```typescript +// src/middleware/local.middleware.ts + +import { Middleware } from '@midwayjs/core'; +import { PassportMiddleware, AuthenticateOptions } from '@midwayjs/passport'; +import { LocalStrategy } from '../strategy/local.strategy'; + +@Middleware() +export class LocalPassportMiddleware extends PassportMiddleware(LocalStrategy) { + // 设置 AuthenticateOptions + getAuthenticateOptions(): Promise | AuthenticateOptions { + return { + failureRedirect: '/login', + }; + } +} +``` +将中间件加载到全局或者路由。 + +```typescript +// src/controller.ts +import { Post, Inject, Controller } from '@midwayjs/core'; +import { LocalPassportMiddleware } from '../middleware/local.middleware'; + +@Controller('/') +export class LocalController { + @Post('/passport/local', { middleware: [LocalPassportMiddleware] }) + async localPassport() { + console.log('local user: ', this.ctx.state.user); + return this.ctx.state.user; + } +} +``` + +使用 curl 模拟一次请求。 + +```bash +curl -X POST http://localhost:7001/passport/local -d '{"username": "demo", "password": "1234"}' -H "Content-Type: application/json" + +结果 {"username": "demo", "password": "1234"} +``` + +:::caution + +注意:如果将中间件放到全局,记得忽略需要登录的路由,否则请求会死循环。 + +::: + + + +### 示例:Jwt 策略 + +首先需要 **额外安装** 依赖和策略: + +```bash +$ npm i @midwayjs/jwt passport-jwt --save +``` + +额外启用 jwt 组件。 + +```typescript +// configuration.ts + +import { join } from 'path'; +import * as jwt from '@midwayjs/jwt'; +import { Configuration, ILifeCycle } from '@midwayjs/core'; +import * as passport from '@midwayjs/passport'; + +@Configuration({ + imports: [ + // ... + jwt, + passport, + ], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration implements ILifeCycle {} +``` + +然后在配置中设置,默认未加密,请不要把敏感信息存放在 payload 中。 + +```typescript +// src/config/config.default.ts +export default { + // ... + jwt: { + secret: 'xxxxxxxxxxxxxx', // fs.readFileSync('xxxxx.key') + expiresIn: '2d', // https://github.com/vercel/ms + }, +}; +``` + +```typescript +// src/strategy/jwt.strategy.ts + +import { CustomStrategy, PassportStrategy } from '@midwayjs/passport'; +import { Strategy, ExtractJwt } from 'passport-jwt'; +import { Config } from '@midwayjs/core'; + +@CustomStrategy() +export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { + @Config('jwt') + jwtConfig; + + async validate(payload) { + return payload; + } + + getStrategyOptions(): any { + return { + secretOrKey: this.jwtConfig.secret, + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + }; + } +} +``` + +:::tip + +注意:validate 方法是社区策略 verify 的 Promise 化替代方法,你无需在最后传递 callback 参数。 + +::: + +```typescript +// src/middleware/jwt.middleware.ts + +import { Middleware } from '@midwayjs/core'; +import { PassportMiddleware, AuthenticateOptions } from '@midwayjs/passport'; +import { JwtStrategy } from '../strategy/jwt.strategy'; + +@Middleware() +export class JwtPassportMiddleware extends PassportMiddleware(JwtStrategy) { + getAuthenticateOptions(): Promise | AuthenticateOptions { + return {}; + } +} +``` + +```typescript +import { Post, Inject, Controller } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { JwtService } from '@midwayjs/jwt'; +import { JwtPassportMiddleware } from '../middleware/jwt.middleware'; + +@Controller('/') +export class JwtController { + @Inject() + jwt: JwtService; + + @Inject() + ctx: Context; + + @Post('/passport/jwt', { middleware: [JwtPassportMiddleware] }) + async jwtPassport() { + console.log('jwt user: ', this.ctx.state.user); + return this.ctx.state.user; + } + + @Post('/jwt') + async genJwt() { + return { + t: await this.jwt.sign({ msg: 'Hello Midway' }), + }; + } +} +``` + +使用 curl 模拟请求 + +```bash +curl -X POST http://127.0.0.1:7001/jwt + +结果 {"t": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} + +curl http://127.0.0.1:7001/passport/jwt -H "Authorization: Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +结果 {"msg": "Hello Midway","iat": 1635468727,"exp": 1635468827} + +``` + +## 自定义其他策略 + +`@midwayjs/passport` 支持自定义[其他策略](http://www.passportjs.org/packages/),这里以 Github OAuth 为例。 +首先 `npm i passport-github`,之后编写如下代码: + +```typescript +// github-strategy.ts + +import { CustomStrategy, PassportStrategy } from '@midwayjs/passport'; +import { Strategy, StrategyOptions } from 'passport-github'; + +const GITHUB_CLIENT_ID = 'xxxxxx', + GITHUB_CLIENT_SECRET = 'xxxxxxxx'; + +@CustomStrategy() +export class GithubStrategy extends PassportStrategy(Strategy, 'github') { + async validate(...payload) { + return payload; + } + + getStrategyOptions(): StrategyOptions { + return { + clientID: GITHUB_CLIENT_ID, + clientSecret: GITHUB_CLIENT_SECRET, + callbackURL: 'https://127.0.0.1:7001/auth/github/cb', + }; + } +} +``` + +```typescript +// src/middleware/github.middleware.ts + +import { AuthenticateOptions, PassportMiddleware } from '@midwayjs/passport'; +import { Middleware } from '@midwayjs/core'; +import { GithubStrategy } from './githubStrategy'; + +@Middleware() +export class GithubPassportMiddleware extends PassportMiddleware(GithubStrategy) { + getAuthenticateOptions(): AuthenticateOptions | Promise { + return {}; + } +} +``` + +```typescript +// src/controller/auth.controller.ts + +import { Controller, Get, Inject } from '@midwayjs/core'; +import { GithubPassportMiddleware } from '../../middleware/github'; + +@Controller('/oauth') +export class AuthController { + @Inject() + ctx; + + @Get('/github', { middleware: [GithubPassportMiddleware] }) + async githubOAuth() {} + + @Get('/github/cb', { middleware: [GithubPassportMiddleware] }) + async githubOAuthCallback() { + return this.ctx.state.user; + } +} +``` + +## 策略选项 + +| 选项 | 类型 | 描述 | +| ------------------- | ------- | ------------------------------------------------- | +| failureRedirect | string | 失败跳转的 url | +| session | boolean | 默认 true,开启后,会自动将用户信息设置到 session | +| sessionUserProperty | string | 设置到 session 上的 key,默认 user | +| userProperty | string | 设置到 ctx.state 或者 req 上的 key,默认 user | +| successRedirect | string | 用户认证成功后跳转的地址 | + +## 常见问题 + +### 1、Failed to serialize user into session + +由于 passport 默认会尝试将 user 数据写入 session,如果无需将用户保存到 session,可以将 session 支持关闭。 + +```typescript +// src/config/config.default +export default { + // ... + passport: { + session: false, + }, +}; +``` + +如果明确需要保存数据到 Session,则需要重写 `PassportStrategy`的 User 的序列化方法,请不要保存特别大的数据。 + +比如自己实现的本地策略。 + +```typescript +// src/strategy/local.strategy.ts + +import { CustomStrategy, PassportStrategy } from '@midwayjs/passport'; +import { Repository } from 'typeorm'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { UserEntity } from './user'; +import * as bcrypt from 'bcrypt'; + +@CustomStrategy() +export class LocalStrategy extends PassportStrategy(Strategy) { + // ... + serializeUser(user, done) { + // 可以只保存用户名 + done(null, user.username); + } + + deserializeUser(id, done) { + // 这里不是异步方法,你可以从其他地方根据用户名,反查用户数据。 + const user = getUserFromDataBase(id); + + done(null, user); + } +} +``` diff --git a/site/versioned_docs/version-3.0.0/extensions/pm2.md b/site/versioned_docs/version-3.0.0/extensions/pm2.md new file mode 100644 index 000000000000..8a90c41d9601 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/pm2.md @@ -0,0 +1,81 @@ +# pm2 + +[PM2](https://github.com/Unitech/pm2) 是带有内置负载平衡器的 Node.js 应用程序的生产过程管理器。可以利用它来简化很多 Node 应用管理的繁琐任务,如性能监控、自动重启、负载均衡等。 + +## 安装 + +我们一般会把 pm2 安装到全局。 + +```bash +$ npm install pm2 -g # 命令行安装 pm2 +``` + +## 常用命令 + +```bash +$ pm2 start # 启动一个服务 +$ pm2 list # 列出当前的服务 +$ pm2 stop # 停止某个服务 +$ pm2 restart # 重启某个服务 +$ pm2 delete # 删除某个服务 +$ pm2 logs # 查看服务的输出日志 +``` + +比如, `pm2 list`,就会以表格显示。 + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1616560437389-b193a0d0-b463-49f1-a347-8dec20e7504d.png) + +pm2 的服务都有一个数组 id,你可以用 id 快速操作它。 + +比如: + +```bash +$ pm2 stop 1 # 停止编号为 1 的服务 +$ pm2 delete 1 # 删除编号为 1 的服务 +``` + +使用 `--name` 参数添加一个应用名。 + +```bash +$ pm2 start ./bootstrap.js --name test_app +``` + +然后你可以用这个应用名来操作启停。 + +```bash +$ pm2 stop test_app +$ pm2 restart test_app +``` + +## 启动应用 + +Midway 应用一般使用 `npm run start` 做线上部署。其对应的命令为 `NODE_ENV=production node bootstrap.js`。 + +:::info +部署前需要执行编译 npm run build +::: + +对应的 pm2 命令为 + +```bash +$ NODE_ENV=production pm2 start ./bootstrap.js --name midway_app -i 4 +``` + +- --name 用于指定应用名 +- -i 用于指定启动的实例数(进程),会使用 cluster 模式启动 + +效果如下: + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1616562075255-088155ee-7c4f-4eae-b5c5-db826f78b519.png) + +## Docker 容器启动 + +在 Docker 容器中,后台启动的代码都会被退出,达不到预期效果。pm2 使用另一个命令来支持容器启动。 + +请将命令修改为 pm2-runtime start 。 + +```bash +$ NODE_ENV=production pm2-runtime start ./bootstrap.js --name midway_app -i 4 +``` + +具体的 pm2 行为请参考 [pm2 容器部署说明](https://www.npmjs.com/package/pm2#container-support)。 diff --git a/site/versioned_docs/version-3.0.0/extensions/process_agent.md b/site/versioned_docs/version-3.0.0/extensions/process_agent.md new file mode 100644 index 000000000000..95049211d162 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/process_agent.md @@ -0,0 +1,132 @@ +# 进程 Agent + +midway 封装了 `@midwayjs/process-agent` 用来解决 node 场景中,多进程部分场景数据进程间数据不一致,或者无法指定 master 进程执行某个方法。 + + +举例: + +- 如果使用 pm2、cluster、多进程进行部署方式,使用内存的 cache,那这个 cache 在自己的进程内。 +- prometheus,获取 `/metrics` 的时候,需要把所有进程的数据收集上来,而不是某个进程的 +- 健康检查,如果有4个进程,如果有一个进程不正常了,健康检查应该检查失败。 + + + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ❌ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 安装方法 + +使用方法: + +```bash +$ npm install @midwayjs/process-agent@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/process-agent": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## 引入组件 + +`configuration.ts` 使用方法: + +```typescript +import * as processAgent from '@midwayjs/process-agent'; + +@Configuration({ + imports: [ + // ... + processAgent + ], +}) +export class MainConfiguration { +} + +``` +## 使用方法 + +业务代码 UserService: + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { TestService } from './test'; + +@Provide() +export class UserService { + + @Inject() + testService: TestService; + + async getUser() { + let result = await this.testService.setData(1); + return result; + } +} + +``` +然后调用 testService 的时候,希望只在主进程执行: + +```typescript +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; +import { RunInPrimary } from '@midwayjs/process-agent'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class TestService { + + data: any = 0; + + @RunInPrimary() + async setData(b){ + this.data = b; + return this.data; + } + + @RunInPrimary() + async getData(){ + return this.data; + } +} + +``` +注意,执行返回的数据只限于可序列化的数据,比如普通 JSON,不支持包含方法等无法序列化的数据。 + + +## 效果描述 +假设采用pm2 或者 egg-script 等多进程方式启动,假设这是个请求 + +首先: + +- 1、设置 setData +- 2、然后获取 getData + + +如果没有 RunInPrimary 这个装饰器,那请求可能落在进程2,或者进程3,那可能没有获取更新的data。 + +所以 RunInPrimary 能确保这个函数执行能落到主进程去。 + + +## 功能征集 +如果有其他类似相关功能,觉得可以放在这个包里面的,欢迎在评论区,或者 [issue](https://github.com/midwayjs/midway/issues) 里面帮忙提一下,我们会跟大家一起讨论和实现。 + diff --git a/site/versioned_docs/version-3.0.0/extensions/prometheus.md b/site/versioned_docs/version-3.0.0/extensions/prometheus.md new file mode 100644 index 000000000000..8bc7173a83be --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/prometheus.md @@ -0,0 +1,302 @@ +# Prometheus + +Prometheus(普罗米修斯)是一个最初在 SoundCloud 上构建的监控系统。 自 2012 年成为社区开源项目,拥有非常活跃的开发人员和用户社区。为强调开源及独立维护,Prometheus 于 2016 年加入云原生云计算基金会(CNCF),成为继 Kubernetes 之后的第二个托管项目。 + +Grafana 是一个开源的度量分析与可视化套件。纯 Javascript 开发的前端工具,通过访问库(如 InfluxDB),展示自定义报表、显示图表等。Grafana 支持许多不同的数据源。每个数据源都有一个特定的查询编辑器,该编辑器定制的特性和功能是公开的特定数据来源。而 Prometheus 正好是其支持的数据源之一。 + +本篇介绍了 Midway 如何接入 Grafana + Prometheus。 + +接入效果如下: + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617259935548-a2df4339-3229-4391-bd3d-4ba8e6979d4d.png) + +## 安装依赖 + +首先安装 Midway 提供的指标监控组件: + +```bash +$ npm install @midwayjs/prometheus@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/prometheus": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## 引入组件 + +在 `configuration.ts` 中,引入这个组件: + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as prometheus from '@midwayjs/prometheus'; // 导入模块 +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + prometheus + ], + importConfigs: [join(__dirname, 'config')], +}) +export class MainConfiguration {} +``` + +启动我们的应用,此时访问的时候多了一个 `${host}:${port}/metrics` 。 + +:::info +Prometheus 基于 HTTP 获取监控数据,请包含 web/koa/express 其中任一组件。 +::: + +访问接口,返回如下,里面的内容是当前的指标。 + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260048533-4f725824-9471-40c9-be8b-6dcbf27d9cca.png) + +## 其他配置 + +指标组件也提供了相关的配置,方便开发者进行配置。 + +可以在 `config.default.ts` 中,修改 prometheus 的配置。 + +```typescript +// src/config/config.default +export default { + // ... + prometheus: { + labels: { + APP_NAME: 'demo_project', + }, + }, +} +``` + +更多的配置,我们可以查看定义进行配置。 + +通过配置,我们例如可以归类哪些 node 是同一个应用,因为我们部署的时候,node 程序是分布式的。例如上面我们加了 APP_NAME,用来区分不同的应用,这样在监控指标中,我们可以区分不同的应用。 + +## 数据采集 + +我们前面在 Midway 中引入的组件主要是在 Node 中加了指标模块。接下来我们需要让 Prometheus 来采集我们的指标数据。 + +如果开发者所在部门已经有 Prometheus+grafana 了,则只需将应用的指标地址上报给 PE 或者通过接口上报即可。此处我们假设大家没有 Prometheus+grafana,然后按照下面描述进行操作。 + +## 搭建 Prometheus + +此处我们通过 docker-compose 来搭建 Prometheus, docker-compose.yml 文件如下: + +```yaml +version: '2.2' +services: + tapi: + logging: + driver: 'json-file' + options: + max-size: '50m' + image: prom/prometheus + restart: always + volumes: + - ./prometheus_data:/prometheus_data:rw + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - ./targets.json:/etc/prometheus/targets.json + command: + - '--storage.tsdb.path=/prometheus_data' + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.retention=10d' + - '--web.enable-lifecycle' + ports: + - '9090:9090' +``` + +`prometheus.yml` 文件如下: + +```yaml +global: + scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. +scrape_configs: + - job_name: 'node' + file_sd_configs: + - refresh_interval: 1m + files: + - '/etc/prometheus/targets.json' + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] +``` + +然后采集的 `targets.json` 如下:下面文件里面 `${ip}` 替换为 Node.js 应用所在服务器的 ip 地址。 + +```json +[ + { + "targets": ["${ip}:7001"], + "labels": { + "env": "prod", + "job": "api" + } + } +] +``` + + + +然后我们启动 `docker-compose.yml` 文件, + +```bash +$ docker-compose up +``` + +至此,Prometheus 已经会去拉取我们 Node 应用程序的指标数据了。 + +如果想要更新 target 怎么做: +修改了这个 targets.json 文件后,通过 prometheus 的 reload 方法进行热加载。 +方法如下: + +```bash +$ curl -X POST http://${prometheus的ip}:9090/-/reload +``` + +然后我们可以查看 prometheus 的页面也可以确认是否生效,界面地址: + +```text +http://${prometheus的ip}:9090/classic/targets +``` + +接下来就是如何展示这些采集到的数据了。 + + + +## 数据展示 + +我们可以借助 Grafana 来展示我们的数据。 + +此处我们简单通过 Docker 来搭建一下 Grafana: + +```bash +$ docker run -d --name=grafana -p 3000:3000 grafana/grafana +``` +也可以将 grafana 和 prometheus 放在一起使用 docker-compose 统一管理。 + +将 grafana 添加到 `docker-compose.yml`, 示例如下: + +```yaml +version: '2.2' +services: + tapi: + logging: + driver: 'json-file' + options: + max-size: '50m' + image: prom/prometheus + restart: always + volumes: + - ./prometheus_data:/prometheus_data:rw # prometheus Data mapping directory + - ./prometheus.yml:/etc/prometheus/prometheus.yml # prometheus Configuration mapping file + - ./targets.json:/etc/prometheus/targets.json + command: + - '--storage.tsdb.path=/prometheus_data' + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.retention=10d' + - '--web.enable-lifecycle' + ports: + - '9090:9090' + // highlight-start + grafana: + image: grafana/grafana + container_name: "grafana0" + ports: + - "3000:3000" + restart: always + volumes: + - "./grafana_data:/var/lib/grafana" # grafana data mapping directory + - "./grafana_log:/var/log/grafana" # grafana log mapping directory + // highlight-end +``` +重启 `docker-compose.yml` 文件 + +```bash +docker-compose restart +``` +![](https://cdn.nlark.com/yuque/0/2022/png/525744/1667300763153-5ee476a7-00ff-4899-92ba-5985995b4862.png) + +完成以上任意一种, 然后我们访问 127.0.0.1:3000,默认账号密码:admin:admin。 + +访问后效果如下: + + + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260561047-c2643a69-6258-491b-937d-9bfc4558252f.png) + +然后我们让 Grafana 接入我们的 Prometheus 数据源: + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260581029-1e2e06a8-3054-4ad8-96b5-d50ab9bb1612.png) + +然后我们点击 Grafana 添加图表: + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725466020-28793a78-c03b-48fa-bf16-0c9c8ecc1a94.png) + +这边 ID 选择 14403,然后点击 load,然后点击下一步,然后点击 import 后,就能看到我们刚刚接入的效果了。 + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725497338-a32a8982-d51f-4e74-b511-dc10a7c66d80.png) + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725514630-4f654f10-ef3a-41f7-b403-02832d3ef7d8.png) + +这样开发者可以运维自己的 Node 程序了,例如,是否最近引入了一个 NPM 包导致了什么内存泄漏的情况,是否最近有应用重启的情况了。 + +当然还能支持其他的自定义操作。 + + +## Socket-io 场景 + +使用方法: + +```bash +$ npm install @midwayjs/prometheus-socket-io@3 --save +``` + +使用方法: + +```typescript +import { Configuration } from '@midwayjs/core'; +import { join } from 'path'; +import * as prometheus from '@midwayjs/prometheus'; +import * as prometheusSocketIo from '@midwayjs/prometheus-socket-io'; + +@Configuration({ + imports: [prometheus, prometheusSocketIo], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration {} +``` + +然后在/metrics 这边就能看到 socket-io 的数据了。 + +![](https://cdn.nlark.com/yuque/0/2021/png/187105/1631090438583-d925c13c-371a-4037-9f53-edaa34580aab.png) + +一共新增 8 个指标。 +后续会提供 Grafana 的模版 ID 给大家使用。 + + +## 功能介绍 + +- [x] 根据 appName 进行分类 +- [x] 查看不同 path 的 qps 情况 +- [x] 查看不同 status 的分布情况 +- [x] 查询不同 path 的 rt 情况 +- [x] 进程的 CPU 使用情况 +- [x] 进程的内存使用情况 +- [x] 堆栈情况 +- [x] Event Loop +- [ ] 等 diff --git a/site/versioned_docs/version-3.0.0/extensions/rabbitmq.md b/site/versioned_docs/version-3.0.0/extensions/rabbitmq.md new file mode 100644 index 000000000000..78c28a3a2315 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/rabbitmq.md @@ -0,0 +1,601 @@ +# RabbitMQ + +在复杂系统的架构中,会有负责处理消息队列的微服务,如下图:服务A负责产生消息给消息队列,而服务B则负责消费消息队列中的任务。 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01SYMbCz1moVSVLl7S2_!!6000000005001-2-tps-646-251.png) + +在Midway中,我们提供了订阅rabbitMQ的能力,专门来满足用户的这类需求。 + +相关信息: + +**订阅服务** + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ❌ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ✅ | +| 包含独立日志 | ❌ | + + + +## 基础概念 + + +RabbitMQ 的概念较为复杂,其基于高级消息队列协议即 Advanced Message Queuing Protocol(AMQP),如果第一次接触请阅读一下相关的参考文档。 + + +AMQP 有一些概念,Queue、Exchange 和 Binding 构成了 AMQP 协议的核心,包括: + +- Producer:消息生产者,即投递消息的程序。 +- Broker:消息队列服务器实体。 + - Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。 + - Binding:绑定,它的作用就是把 Exchange 和 Queue 按照路由规则绑定起来。 + - Queue:消息队列载体,每个消息都会被投入到一个或多个队列。 +- Consumer:消息消费者,即接受消息的程序。 + + + +简单的理解,消息通过 Publisher 发布到 Exchange(交换机),Consumer 通过订阅 Queue 来接受消息,Exchange 和 Queue 通过路由做连接。 + + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01fLrucw1FVNbCx4NqG_!!6000000000492-2-tps-700-328.png) + + + +## 消费者(Consumer)使用方法 + + +### 安装依赖 + + +Midway 提供了订阅 rabbitMQ 的能力,并能够独立部署和使用。安装 `@midwayjs/rabbitmq` 模块及其定义。 +```bash +$ npm i @midwayjs/rabbitmq@3 --save +$ npm i amqplib --save +$ npm i @types/amqplib --save-dev +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/rabbitmq": "^3.0.0", + "amqplib": "^0.10.1", + // ... + }, + "devDependencies": { + "@types/amqplib": "^0.8.2", + // ... + } +} +``` + +## 开启组件 + +`@midwayjs/rabbmitmq` 可以作为独立主框架使用。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as rabbitmq from '@midwayjs/rabbitmq'; + +@Configuration({ + imports: [ + rabbitmq + ], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} +``` + +也可以附加在其他的主框架下,比如 `@midwayjs/koa` 。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as rabbitmq from '@midwayjs/rabbitmq'; + +@Configuration({ + imports: [ + koa, + rabbitmq + ], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} +``` + +### 目录结构 + + +我们一般把能力分为生产者和消费者,而订阅正是消费者的能力。 + + +我们一般把消费者放在 consumer 目录。比如 `src/consumer/userConsumer.ts` 。 +``` +➜ my_midway_app tree +. +├── src +│ ├── consumer +│ │ └── user.consumer.ts +│ ├── interface.ts +│ └── service +│ └── user.service.ts +├── test +├── package.json +└── tsconfig.json +``` +代码示例如下。 + +```typescript +import { Consumer, MSListenerType, RabbitMQListener, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/rabbitmq'; +import { ConsumeMessage } from 'amqplib'; + +@Consumer(MSListenerType.RABBITMQ) +export class UserConsumer { + + @Inject() + ctx: Context; + + @RabbitMQListener('tasks') + async gotData(msg: ConsumeMessage) { + this.ctx.channel.ack(msg); + } + +} + +``` +`@Consumer` 装饰器,提供消费者标识,并且它的参数,指定了某种消费框架的类型,比如,我们这里指定了 `MSListenerType.RABBITMQ` 这个类型,指的就是 rabbitMQ 类型。 + + +标识了 `@Consumer` 的类,对方法使用 `@RabbitMQListener` 装饰器后,可以绑定一个 RabbitMQ 的队列(Queue)。 + + +方法的参数为接收到的消息,类型为 `ConsumeMessage` 。如果返回值需要确认,则需要对服务端进行 `ack` 操作,明确接收到的数据。 + + +如果需要订阅多个队列,可以使用多个方法,也可以使用多个文件。 + + +### RabbitMQ 消息上下文 + + +订阅 `RabbitMQ` 数据的上下文,和 Web 同样的,其中包含一个 `requestContext` ,和每次接收消息的数据绑定。 + + +从 ctx 上可以取到 `channel` ,整个 ctx 的定义为: +```typescript +export type Context = { + channel: amqp.Channel; + requestContext: IMidwayContainer; +}; +``` + + +可以从框架获取定义 +```typescript +import { Context } from '@midwayjs/rabbitmq'; +``` + + +### 配置消费者 + +我们需要在配置中指定 rabbitmq 的地址。 + +```typescript +// src/config/config.default +import { MidwayConfig } from '@midwayjs/core'; + +export default { + // ... + rabbitmq: { + url: 'amqp://localhost' + } +} as MidwayConfig; +``` + +更多配置: + +| 属性 | 描述 | +| --- | --- | +| url | rabbitMQ 的连接信息 | +| socketOptions | amqplib.connect 的第二个参数 | +| reconnectTime | 队列断连后的重试时间,默认 10 秒 | + + + +### Fanout Exchange + + +Fanout 是一种特定的交换机,如果满足匹配(binding),就往 Exchange 所绑定的 Queue 发送消息。Fanout Exchange 会忽略 RoutingKey 的设置,直接将 Message 广播到所有绑定的 Queue 中。 + +即所有订阅该交换机的 Queue 都会收到消息。 + +比如,下面我们添加了两个 Queue,订阅了相同的交换机。 +```typescript +import { Consumer, MSListenerType, RabbitMQListener, Inject, App } from '@midwayjs/core'; +import { Context, Application } from '@midwayjs/rabbitmq'; +import { ConsumeMessage } from 'amqplib'; + +@Consumer(MSListenerType.RABBITMQ) +export class UserConsumer { + + @App() + app: Application; + + @Inject() + ctx: Context; + + @Inject() + logger; + + @RabbitMQListener('abc', { + exchange: 'logs', + exchangeOptions: { + type: 'fanout', + durable: false, + }, + exclusive: true, + consumeOptions: { + noAck: true, + } + }) + async gotData(msg: ConsumeMessage) { + this.logger.info('test output1 =>', msg.content.toString('utf8')); + // TODO + } + + @RabbitMQListener('bcd', { + exchange: 'logs', + exchangeOptions: { + type: 'fanout', + durable: false, + }, + exclusive: true, + consumeOptions: { + noAck: true, + } + }) + async gotData2(msg: ConsumeMessage) { + this.logger.info('test output2 =>', msg.content.toString('utf8')); + // TODO + } + +} + +``` + + +订阅的 abc 和 bcd 队列,绑定了相同的交换机 logs,最终的结果是,两个方法都会被调用。 + + +### Direct Exchange + + +Direct Exchange 是 RabbitMQ 默认的 Exchange,完全根据 RoutingKey 来路由消息。设置 Exchange 和 Queue 的 Binding 时需指定 RoutingKey(一般为 Queue Name),发消息时也指定一样的 RoutingKey,消息就会被路由到对应的Queue。 + + +下面的示例代码,我们不填写 Queue Name,只添加一个 routingKey,交换机类型为 direct。 +```typescript +import { Consumer, MSListenerType, RabbitMQListener, Inject, App } from '@midwayjs/core'; +import { Context, Application } from '../../../../../src'; +import { ConsumeMessage } from 'amqplib'; + +@Consumer(MSListenerType.RABBITMQ) +export class UserConsumer { + + @App() + app: Application; + + @Inject() + ctx: Context; + + @Inject() + logger; + + @RabbitMQListener('', { + exchange: 'direct_logs', + exchangeOptions: { + type: 'direct', + durable: false, + }, + routingKey: 'direct_key', + exclusive: true, + consumeOptions: { + noAck: true, + } + }) + async gotData(msg: ConsumeMessage) { + // TODO + } +} + +``` + + +direct 类型的消息,会根据 routerKey 做定向过滤,所以只有特定订阅能收到消息。 + + + + +### 装饰器参数 + + +`@RabbitMQListener` 装饰器的第一个参数为 queueName,代表需要监听的队列。 + + +第二个参数是一个对象,包含队列,交换机等参数,详细定义如下: +```typescript +export interface RabbitMQListenerOptions { + exchange?: string; + /** + * queue options + */ + exclusive?: boolean; + durable?: boolean; + autoDelete?: boolean; + messageTtl?: number; + expires?: number; + deadLetterExchange?: string; + deadLetterRoutingKey?: string; + maxLength?: number; + maxPriority?: number; + pattern?: string; + /** + * prefetch + */ + prefetch?: number; + /** + * router + */ + routingKey?: string; + /** + * exchange options + */ + exchangeOptions?: { + type?: 'direct' | 'topic' | 'headers' | 'fanout' | 'match' | string; + durable?: boolean; + internal?: boolean; + autoDelete?: boolean; + alternateExchange?: string; + arguments?: any; + }; + /** + * consumeOptions + */ + consumeOptions?: { + consumerTag?: string; + noLocal?: boolean; + noAck?: boolean; + exclusive?: boolean; + priority?: number; + arguments?: any; + }; +} +``` + + + + +### 本地测试 + + +Midway 提供了一个简单的测试方法用于测试订阅某个数据。 `@midwayjs/mock` 工具提供了一个 `createRabbitMQProducer` 的方法,用于创建一个生产者,通过它,你可以创建一个队列(Queue),以及向这个队列发消息。 + + +然后,我们启动一个 app,就可以自动监听到这个队列中的数据,并执行后续逻辑。 + +```typescript +import { createRabbitMQProducer, close, creatApp } from '@midwayjs/mock'; + +describe('/test/index.test.ts', () => { + it('should test create message and get from app', async () => { + // create a queue and channel + const channel = await createRabbitMQProducer('tasks', { + isConfirmChannel: true, + mock: false, + url: 'amqp://localhost', + }); + + // send data to queue + channel.sendToQueue('tasks', Buffer.from('something to do')) + + // create app and got data + const app = await creatApp(); + + // wait a moment + + await close(app); + }); +}); + +``` + + +**示例一** + + +创建一个 fanout exchange。 +```typescript +const manager = await createRabbitMQProducer('tasks-fanout', { + isConfirmChannel: false, + mock: false, + url: 'amqp://localhost', +}); + +// Name of the exchange +const ex = 'logs'; +// Write a message +const msg = "Hello World!"; + +// 声明交换机 +manager.assertExchange(ex, 'fanout', { durable: false }) // 'fanout' will broadcast all messages to all the queues it knows + +// 启动服务 +const app = await creatApp('base-app-fanout', { + url: 'amqp://localhost', + reconnectTime: 2000 +}); + +// 发送到交换机,由于不持久化,需要等订阅服务起来之后再发 +manager.sendToExchange(ex, '', Buffer.from(msg)) + +// 等一段时间 +await sleep(5000); + +// 校验结果 + +// 关闭 producer +await manager.close(); + +// 关闭 app +await close(app); +``` + + +**示例二** + + +创建一个 direct exchange。 +```typescript +/** + * direct 类型的消息,根据 routerKey 做定向过滤 + */ +const manager = await createRabbitMQProducer('tasks-direct', { + isConfirmChannel: false, + mock: false, + url: 'amqp://localhost', +}); + +// Name of the exchange +const ex = 'direct_logs'; +// Write a message +const msg = "Hello World!"; + +// 声明交换机 +manager.assertExchange(ex, 'direct', { durable: false }) // 'fanout' will broadcast all messages to all the queues it knows + +const app = await creatApp('base-app-direct', { + url: 'amqp://localhost', + reconnectTime: 2000 +}); + +// 这里指定 routerKey,发送到交换机 +manager.sendToExchange(ex, 'direct_key', Buffer.from(msg)) + +// 校验结果 + +await manager.close(); +await close(app); +``` + + +## 生产者(Producer)使用方法 + + +生产者(Producer)也就是第一节中的消息产生者,简单的来说就是会创建一个客户端,将消息发送到 RabbitMQ 服务。 + + +注意:当前 Midway 并没有使用组件来支持消息发送,这里展示的示例只是使用纯 SDK 在 Midway 中的写法。 + + +### 安装依赖 + + +```bash +$ npm i amqplib amqp-connection-manager --save +$ npm i @types/amqplib --save-dev +``` + + +### 调用服务发送消息 + + +比如,我们在 service 文件下,新增一个 `rabbitmq.ts` 文件。 +```typescript +import { Provide, Scope, ScopeEnum, Init, Autoload, Destroy } from '@midwayjs/core'; +import * as amqp from 'amqp-connection-manager' + +@Autoload() +@Provide() +@Scope(ScopeEnum.Singleton) // Singleton 单例,全局唯一(进程级别) +export class RabbitmqService { + + private connection: amqp.AmqpConnectionManager; + + private channelWrapper; + + @Init() + async connect() { + // 创建连接,你可以把配置放在 Config 中,然后注入进来 + this.connection = await amqp.connect('amqp://localhost'); + + // 创建 channel + this.channelWrapper = this.connection.createChannel({ + json: true, + setup: function(channel) { + return Promise.all([ + // 绑定队列 + channel.assertQueue("tasks", { durable: true }), + ]); + } + }); + } + + // 发送消息 + public async sendToQueue(queueName: string, data: any) { + return this.channelWrapper.sendToQueue(queueName, data); + } + + @Destroy() + async close() { + await this.channelWrapper.close(); + await this.connection.close(); + } +} + +``` +大概就是创建了一个用来封装消息通信的 service,同时他是全局唯一的 Singleton 单例。由于增加了 `@AutoLoad` 装饰器,可以自执行初始化。 + + +这样基础的调用服务就抽象好了,我们只需要在用到的地方,调用 `sendToQueue` 方法即可。 + + +比如: + + +```typescript +@Provide() +export class UserService { + + @Inject() + rabbitmqService: RabbitmqService; + + async invoke() { + // TODO + + // 发送消息 + await this.rabbitmqService.sendToQueue('tasks', {hello: 'world'}); + } +} +``` + + +## 参考文档 + + +- [理解 RabbitMQ Exchange](https://zhuanlan.zhihu.com/p/37198933) +- [RabbitMQ for Node.js in 30 steps](https://github.com/Gurenax/node-rabbitmq) diff --git a/site/versioned_docs/version-3.0.0/extensions/redis.md b/site/versioned_docs/version-3.0.0/extensions/redis.md new file mode 100644 index 000000000000..45e6950d89f9 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/redis.md @@ -0,0 +1,224 @@ +# Redis + +这里介绍如何快速在 Midway 中使用 Redis。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 安装依赖 + +`@midwayjs/redis` 是主要的功能包。 + +```bash +$ npm i @midwayjs/redis@3 --save +``` +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/redis": "^3.0.0", + // ... + } +} +``` + + + + +## 引入组件 + + +首先,引入 组件,在 `src/configuration.ts` 中导入: +```typescript +import { Configuration } from '@midwayjs/core'; +import * as redis from '@midwayjs/redis'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + redis // 导入 redis 组件 + ], + importConfigs: [ + join(__dirname, 'config') + ], +}) +export class MainConfiguration { +} +``` + + +## 配置 Redis + + +**单客户端配置** +```typescript +// src/config/config.default.ts +export default { + // ... + redis: { + client: { + port: 6379, // Redis port + host: "127.0.0.1", // Redis host + password: "auth", + db: 0, + }, + }, +} +``` +**Sentinel 配置** +```typescript +// src/config/config.default.ts +export default { + // ... + redis: { + client: { + sentinels: [{ // Sentinel instances + port: 26379, // Sentinel port + host: '127.0.0.1', // Sentinel host + }], + name: 'mymaster', // Master name + password: 'auth', + db: 0 + }, + }, +} +``` + + +**Cluster 模式配置,需要配置多个** +```typescript +// src/config/config.default.ts +export default { + // ... + redis: { + // Cluster Redis + client: { + cluster: true, + nodes: [{ + host: 'host', + port: 'port', + },{ + host: 'host', + port: 'port', + }], + redisOptions: { + family: '', + password: 'xxxx', + db: 'xxx' + } + } + }, +} +``` + +**多个客户端配置,需要配置多个** +```typescript +// src/config/config.default.ts +export default { + // ... + redis: { + // Multi Redis + clients: { + instance1: { + host: 'host', + port: 'port', + password: 'password', + db: 'db', + }, + instance2: { + host: 'host', + port: 'port', + password: 'password', + db: 'db', + }, + }, + }, +} +``` +更多参数可以查看 [ioredis 文档](https://github.com/luin/ioredis/blob/master/API.md#new_Redis_new)。 + + +## 使用 Redis 服务 + + +我们可以在任意的代码中注入使用。 +```typescript +import { Provide, Controller, Inject, Get } from '@midwayjs/core'; +import { RedisService } from '@midwayjs/redis'; + +@Provide() +export class UserService { + + @Inject() + redisService: RedisService; + + async invoke() { + + // 简单设置 + await this.redisService.set('foo', 'bar'); + + // 设置过期时间,单位秒 + await this.redisService.set('foo', 'bar', 'EX', 10); + + // 获取数据 + const result = await this.redisService.get('foo'); + + // result => bar + } +} +``` + + +可以使用 `RedisServiceFactory` 获取不同的实例。 +```typescript +import { RedisServiceFactory } from '@midwayjs/redis'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + redisServiceFactory: RedisServiceFactory; + + async save() { + const redis1 = this.redisServiceFactory.get('instance1'); + const redis2 = this.redisServiceFactory.get('instance3'); + + //... + + } +} +``` + +也可以通过装饰器获取。 + +```typescript +import { RedisServiceFactory, RedisService } from '@midwayjs/redis'; +import { InjectClient } from '@midwayjs/core'; + +@Provide() +export class UserService { + + @InjectClient(RedisServiceFactory, 'instance1') + redis1: RedisService; + + @InjectClient(RedisServiceFactory, 'instance3') + redis2: RedisService; + + async save() { + //... + } +} +``` + diff --git a/site/versioned_docs/version-3.0.0/extensions/render.md b/site/versioned_docs/version-3.0.0/extensions/render.md new file mode 100644 index 000000000000..765da901bc7b --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/render.md @@ -0,0 +1,453 @@ +# 模板渲染 + +本组件用于在 midway 体系使用服务端渲染 ejs,nunjucks 模板。 + +相关信息: + +| web 支持情况 | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ❌ | + + + + +## 使用 ejs + + +### 安装依赖 + + +选择对应的模板安装依赖。 +```bash +$ npm i @midwayjs/view-ejs@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/view-ejs": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +### 引入组件 + + +首先,引入组件,在 `configuration.ts` 中导入: +```typescript +import { Configuration } from '@midwayjs/core'; +import * as view from '@midwayjs/view-ejs'; +import { join } from 'path' + +@Configuration({ + imports: [ + view // 导入 ejs 组件 + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` +### 配置 + +配置后缀,映射到指定的引擎。 + +```typescript +// src/config/config.default.ts +export default { + // ... + view: { + mapping: { + '.ejs': 'ejs', + }, + }, + // ejs config + ejs: {} +} +``` +### 使用 + + +注意,默认的 view 目录为 `${appDir}/view` ,在其中创建一个 `hello.ejs` 文件。 + + +目录结构如下: +``` +➜ my_midway_app tree +. +├── src +│ └── controller ## Controller 目录 +│ └── home.ts +├── view ## 模板目录 +│ └── hello.ejs +├── test +├── package.json +└── tsconfig.json +``` + + +我们在模板里写一些 ejs 格式的内容,比如: +```typescript +// view/hello.ejs +hello <%= data %> +``` + + +在 Controller 中渲染。 +```typescript +import { Inject, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async render(){ + await this.ctx.render('hello.ejs', { + data: 'world', + }); + } +} +``` + +### 配置后缀 + +默认后缀为 `.html` ,为了改成习惯的 `.ejs` 后缀,我们可以加一个 `defaultExtension` 配置。 + +```typescript +// src/config/config.default.ts +export default { + // ... + view: { + defaultExtension: '.ejs', + mapping: { + '.ejs': 'ejs', + }, + }, + // ejs config + ejs: {} +} +``` + +这样我们在渲染时不需要增加后缀。 + +```typescript +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async render(){ + await this.ctx.render('hello', { + data: 'world', + }); + } +} +``` + + +### 默认渲染引擎 + +我们可以通过 `defaultViewEngine` 来设置默认的渲染引擎。 + +其作用是,当遇到的模板后缀,比如 `.html` 未在配置的 `mapping` 字段中找到时,使用该 `defaultViewEngine` 字段指定的引擎来渲染。 + +```typescript +// src/config/config.default.ts +export default { + // ... + view: { + defaultViewEngine: 'ejs', + mapping: { + '.ejs': 'ejs', + }, + }, + // ejs config + ejs: {} +} +``` + +这样,如果模板是 `.html` 后缀,由于 `mapping` 中未指定,依旧会使用 `ejs` 来渲染。 + +### 配置多个模板目录 + +如果我们需要将代码封装为组件提供,就需要支持不同的模板目录。 + +默认的模板目录在 `${appDir}/view`。我们可以在 `rootDir` 字段增加其他的目录。 + +```typescript +// src/config/config.default.ts + +// 修改默认 view 组件的 default 目录 +export default { + // ... + view: { + rootDir: { + default: path.join(__dirname, './view'), + } + }, +} + +// 其他组件需要增加目录的配置 +export default { + // ... + // view 组件的配置 + view: { + rootDir: { + anotherRoot: path.join(__dirname, './view'), + } + }, +} +``` + +通过对象合并的机制,使得所有的 `rootDir` 都能合并到一起,组件内部会获取 values 做匹配。 + + + +## 使用 Nunjucks + + +和 ejs 类似,引入对应组件即可。 + + +1、选择对应的模板安装依赖。 +```bash +$ npm i @midwayjs/view-nunjucks@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/view-nunjucks": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +2、引入组件,在 `configuration.ts` 中导入: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as view from '@midwayjs/view-nunjucks'; +import { join } from 'path' + +@Configuration({ + imports: [ + view // 导入 nunjucks 组件 + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + +3、增加 nunjucks 的配置,比如默认使用 nunjucks。 +```typescript +export default { + // ... + view: { + defaultViewEngine: 'nunjucks', + mapping: { + '.nj': 'nunjucks', + }, + }, +} +``` + + +4、在 view 目录增加模板 +```typescript +// view/test.nj +hi, {{ user }} +``` + + +在 Controller 中渲染。 +```typescript +import { Inject, Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async render(){ + await ctx.render('test.nj', { user: 'midway' }); + } +} +``` +访问后会输出 `hi, midway` 。 + + +如果有自定义 filter 的需求,可以在入口处增加,比如下面增加了一个名为 `hello` 的 filter。 +```typescript +import { App, Configuration, Inject } from '@midwayjs/core'; +import * as view from '@midwayjs/view-nunjucks'; +import { join } from 'path' + +@Configuration({ + imports: [view], + importConfigs: [join(__dirname, 'config')] +}) +export class MainConfiguration { + + @App() + app; + + @Inject() + env: view.NunjucksEnvironment; + + async onReady(){ + this.env.addFilter('hello', (str) => { + return 'hi, ' + str; + }); + } +} + +``` +在模板里可以使用 +```typescript +{{ name | hello }} +``` +然后渲染 +```typescript +// controller +// ... +await ctx.render('test.nj', { name: 'midway' }); +``` +也会输出 `hi, midway` 。 + + + +## 自定义模板引擎 + +默认我们只提供了 ejs 和 nunjucks 的模板引擎,你也可以编写自己的模板引擎代码。 + +### 实现模板引擎 + +首先需要创建一个请求作用域的模板引擎类,它将在每个请求执行时初始化。你需需要实现其中的 `render` 和 `renderString` 方法。如果你的模板引擎不支持某个方法,可以抛出异常。 + +```typescript +// lib/view.ts +import { Provide, Config } from '@midwayjs/core'; +import { IViewEngine } from '@midwayjs/view'; + +@Provide() +export class MyView implements IViewEngine { + + @Config('xxxx') + viewConfig; + + async render(name: string, locals?: Record, options?: RenderOptions) { + return myengine.render(name, locals, options); + } + + async renderString(tpl: string, + locals?: Record, + options?: RenderOptions) { + + throw new Error('not implement'); + } +}; +``` + +这两个方法接受类似的三个参数,`renderString` 第一个参数需要传入待解析的模板内容本身,而 `render` 方法会解析模板文件。 + +`render(name, locals, viewOptions)` + +- name: 从 `root`(默认是 `/view` ) 相对的 path +- locals: 模板需要的数据 +- viewOptions: 每次渲染的模板参数,可覆盖的配置,可以在配置文件中重写,其中包含几个参数: + - root: 模板的绝对路径 + - name: 调用 render 方法的原始 name 值 + - locals: 调用 render 方法的原始 locals 值 + +`renderString(tpl, locals, viewOptions)` + +- tpl: 模板名 +- locals: 和 `render` 一样 +- viewOptions: 和 `render` 一样 + +### 注册模板引擎 + +在实现自定义的模板引擎后,我们需要在启动入口注册它。 + +通过引入 `ViewManager` ,我们可以使用 `use` 方法注册自定义模板引擎。 + +```typescript +// src/configuration.ts +import { Configuration, Inject, Provide } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as view from '@midwayjs/view'; +import { MyView } from './lib/my'; + +@Configuration({ + imports: [koa, view], + importConfigs: [join(__dirname, 'config')] +}) +export class MainConfiguration { + + @Inject() + viewManager: view.ViewManager; + + async onReady(){ + this.viewManager.use('ejs', MyView); + } +} + +``` + + + +## 注意事项 + + +如需在 egg(@midwayjs/web) 场景下使用,请在 `plugin.ts` 中关闭 view 和其相关插件。 + + +```typescript +import { EggPlugin } from 'egg'; +export default { + // ... + view: false, +} as EggPlugin; + +``` + + +否则会出现下面类似的错误。 +``` +TypeError: Cannot set property view of # which has only a getter +``` + diff --git a/site/versioned_docs/version-3.0.0/extensions/security.md b/site/versioned_docs/version-3.0.0/extensions/security.md new file mode 100644 index 000000000000..021d47ca5bc9 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/security.md @@ -0,0 +1,362 @@ +# 安全 + +适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用安全组件,支持 `csrf` 、`xss` 等多种安全策略。 + +相关信息: + +| web 支持情况 | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + + + +## 安装使用 + +1、安装依赖 + +```bash +$ npm i @midwayjs/security --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/security": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +2、在 configuration 中引入组件 + +```typescript +import * as security from '@midwayjs/security'; +@Configuration({ + imports: [ + // ...other components + security + ], +}) +export class MainConfiguration {} +``` + +--- + +## 防范常见的安全威胁 + + +### 一、CSRF + +CSRF(Cross-site request forgery 跨站请求伪造),是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。 + + +#### 1. 令牌同步模式 +通过响应页面时将 token 渲染到页面上,在开启 `csrf` 配置后,通过 `ctx.csrf` 可以获取到 `csrf token`,可以再返回页面 html 时同步输出 + +```ts +@Controller('/') +export class HomeController { + @Inject() + ctx; + + @Get('/home') + async home() { + return `
+ title: + +
`; + } +} +``` + +传递 CSRF token 的字段(上述示例中的 `_csrf`)可以在配置中改变,请查看下述 `配置 -> csrf`。 + + + +#### 2. Cookies 模式 + +在 CSRF 默认配置下,token 会被设置在 Cookie 中,可以再前端页面中通过 JS 从 Cookies 中获取,然后再 ajax/fetch 等请求中添加到 `header`、`query` 或 `body` 中。 + +```js +const csrftoken = Cookies.get('csrfToken'); +fetch('/api/post', { + method: 'POST', + headers: { + 'x-csrf-token': csrftoken + }, + ... +}); +``` + +默认配置下,框架会将 `CSRF token` 存在 `Cookie` 中,以方便前端 JS 发起请求时获取到。但是所有的子域名都可以设置 Cookie,因此当我们的应用处于无法保证所有的子域名都受控的情况下,存放在 `Cookie` 中可能有被 `CSRF` 攻击的风险。框架提供了一个配置项 `useSession`,可以将 token 存放到 Session 中。 + + +当 `CSRF token` 存储在 `Cookie` 中时,一旦在同一个浏览器上发生用户切换,新登陆的用户将会依旧使用旧的 token(之前用户使用的),这会带来一定的安全风险,因此在每次用户登陆的时候都必须调用 `ctx.rotateCsrfSecret()` 刷新 `CSRF token`,例如: + + +```js +@Controller('/') +export class HomeController { + @Inject() + ctx; + + @Inject() + userService; + + @Get('/login') + async login(@Body('username') username: string, @Body('password') password: string) { + const user = await userService.login({ username, password }); + this.ctx.session = { user }; + this.ctx.rotateCsrfSecret(); + return { success: true }; + } +} +``` + +### 二、XSS + +`XSS`(cross-site scripting 跨站脚本攻击)攻击是最常见的 Web 攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。 + +`XSS` 攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。 + + +#### 1. 反射型的 XSS 攻击 + +主要是由于服务端接收到客户端的不安全输入,在客户端触发代码执行从而发起 `Web` 攻击。 + +例如:在某搜索网站搜索时,搜索结果会显示搜索的关键词。搜索关键词填入 ``, 点击搜索后,若页面程序没有对关键词进行处理,这段代码就会直接在页面上执行,弹出 alert。 + +框架提供了 `ctx.security.escape()` 方法对字符串进行 XSS 过滤。 + +```ts +@Controller('/') +export class HomeController { + @Inject() + ctx; + + @Get('/home') + async home() { + const str = ``; + const escapedStr = this.ctx.security.escape(str); + // <script>alert("xss") </script> + return escapedStr; + } +} +``` + +另外当网站输出的内容是作为 js 脚本的。这个时候需要使用 `ctx.security.js()` 来进行过滤。 + +还有一种情况,有时候我们需要在 `js` 中输出 `json` ,若未做转义,易被利用为 `XSS` 漏洞。框架提供了 `ctx.security.json(变量)` 来提供 json encode,防止 XSS 攻击。 + + +```ts +@Controller('/') +export class HomeController { + @Inject() + ctx; + + @Get('/home') + async home() { + return ``; + } +} +``` + +#### 2. 存储型的 XSS 攻击 + +通过提交带有恶意脚本的内容存储在服务器上,当其他人看到这些内容时发起 Web 攻击,比如一些网站的评论框中,用户恶意将一些代码作为评论内容,若没有过滤,其他用户看到这个评论时恶意代码就会执行。 + + +框架提供了 `ctx.security.html()` 来进行过滤。 + + +#### 3. 其他 XSS 的防范方式 + +浏览器自身具有一定针对各种攻击的防范能力,他们一般是通过开启 Web 安全头生效的。框架内置了一些常见的 Web 安全头的支持。 + +**CSP** + +`Content Security Policy`,简称 `CSP`,主要是用来定义页面可以加载哪些资源,减少 `XSS` 的发生。 + +默认关闭(可通过 `csp: {enable: true}` 配置开启),开启后可以有效的防止 `XSS` 攻击的发生。要配置 `CSP` , 需要对 `CSP` 的 `policy` 策略有了解,具体细节可以参考 [阿里聚安全 - CSP是什么?](https://www.zhihu.com/question/21979782/answer/122682029) + + +**X-Download-Options:noopen** + +默认开启(可通过 `noopen: {enable: false}` 配置关闭),禁用 IE 下下载框 Open 按钮,防止 IE 下下载文件默认被打开 XSS。 + +**X-Content-Type-Options:nosniff** +禁用 IE8 自动嗅探 mime 功能,默认关闭(可通过 `nosniff: {enable: true}` 配置开启),例如 text/plain 却当成 text/html 渲染,特别当本站点 serve 的内容未必可信的时候。 + +**X-XSS-Protection** +IE 提供的一些 XSS 检测与防范,默认开启(可通过 `xssProtection: {enable: false}` 配置关闭) + +close 默认值 false,即设置为 1; mode=block + +--- + + +## 配置 + +默认配置如下: + +```ts +// src/config/config.default +export default { + // ... + + // 默认配置 + security: { + csrf: { + enable: true, + type: 'ctoken', + useSession: false, + cookieName: 'csrfToken', + sessionName: 'csrfToken', + headerName: 'x-csrf-token', + bodyName: '_csrf', + queryName: '_csrf', + refererWhiteList: [], + }, + xframe: { + enable: true, + value: 'SAMEORIGIN', + }, + csp: { + enable: false, + }, + hsts: { + enable: false, + maxAge: 365 * 24 * 3600, + includeSubdomains: false, + }, + noopen: { + enable: false, + }, + nosniff: { + enable: false, + }, + xssProtection: { + enable: true, + value: '1; mode=block', + }, + }, +} + +``` + +### csrf + +| 配置项 | 类型 | 作用描述 | 默认值 | +| --- |--------------------------------------| --- | --- | +| enable | boolean | 是否开启 | true | +| type | 'all' / 'any' / 'ctoken' / 'referer' | csrf 校验类型,all/any 等于 ctoken + referer | 'ctoken' 从query/header/body 中获取 csrf token;;'referer' 则可以通过 refererWhiteList 配置白名单 | +| useSession | boolean | csrf token 是否存放在 session 中 | false,默认存放在 cookies 中 | +| cookieName | string | token 在 cookie 中存放的 字段 | 'csrfToken' | +| sessionName | string | token 在 session 中存放的 字段 | 'csrfToken' | +| headerName | string | token 在 header 中存放的 字段 | 'x-csrf-token' | +| bodyName | string | token 在 body 中存放的 字段 | '_csrf' | +| queryName | string | token 在 query 中存放的 字段 | '_csrf' | +| refererWhiteList | Array\ | 允许的来源白名单 | [] | + +#### 配置 refererWhiteList 不生效? ++ 原因一:refererWhiteList 中需要配置 referer 的 host 部分,例如 referer 为 `https://midway-demo.com:1234/docs`,则 refererWhiteList 中需要配置 `midway-demo.com:1234`。 ++ 原因二:refererWhiteList 仅在 csrf 配置中 type 为 `referer` 的情况下生效,默认 type 为 `ctoken`,需要修改为 `referer`。 ++ 原因三:发送的http请求中的 referer 字段不是一个标准的 url 地址(例如没有加上请求协议等),参考 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referer) 文档 + +### xframe + + +xframe 用来配置 `X-Frame-Options` 响应头,用来给浏览器指示允许一个页面可否在 `frame`, `iframe`, `embed` 或者 `object` 中展现的标记。站点可以通过确保网站没有被嵌入到别人的站点里面,从而避免 `clickjacking` 攻击。 + +`X-Frame-Options` 有三个可能的值: + ++ X-Frame-Options: deny:页面不允许在 frame 中展示 ++ X-Frame-Options: sameorigin:该页面可以在相同域名页面的 frame 中展示 ++ X-Frame-Options: allow-from https://example.com/:该页面可以在指定来源的 frame 中展示 + + + +| 配置项 | 类型 | 作用描述 | 默认值 | +| --- | --- | --- | --- | +| enable | boolean | 是否开启 | true | +| value | string | X-Frame-Options 值 | 'SAMEORIGIN' | + + + +### hsts + +`HTTP Strict Transport Security`(通常简称为 `HSTS` )是一个安全功能,它告诉浏览器只能通过 `HTTPS` 访问当前资源,而不是 `HTTP`。 + +| 配置项 | 类型 | 作用描述 | 默认值 | +| --- | --- | --- | --- | +| enable | boolean | 是否开启 | false | +| maxAge | number | 在浏览器收到这个请求后的多少 `秒` 时间内凡是访问这个域名下的请求都使用HTTPS请求 | `365 * 24 * 3600` 即一年 | +| includeSubdomains | boolean | 此规则是否适用于该网站的所有子域名 | false | + + +### csp + +HTTP 响应头 `Content-Security-Policy` 允许站点管理者控制指定的页面加载哪些资源。这将帮助防止跨站脚本攻击(XSS)。 + + +| 配置项 | 类型 | 作用描述 | 默认值 | +| --- |----------------------------------------------------------| --- | --- | +| enable | boolean | 是否开启 | false | +| policy | Object\ | 策略列表 | {} | +| reportOnly | boolean | 是否开启 | false | +| supportIE | boolean | 是否支持IE浏览器 | false | + +详细的 `policy` 配置可以参考: [Content Security Policy (CSP) 是什么?阿里聚安全](https://www.zhihu.com/question/21979782/answer/122682029) + + +### noopen + +用于指定 `IE 8` 以上版本的用户不打开文件而直接保存文件。在下载对话框中不显式“打开”选项。 + +| 配置项 | 类型 | 作用描述 | 默认值 | +| --- | --- | --- | --- | +| enable | boolean | 是否开启 | false | + + + + +### nosniff + +开启后,如果从 `script` 或 `stylesheet` 读入的文件的 `MIME` 类型与指定 `MIME` 类型不匹配,不允许读取该文件。用于防止 `XSS` 等跨站脚本攻击。 + +| 配置项 | 类型 | 作用描述 | 默认值 | +| --- | --- | --- | --- | +| enable | boolean | 是否开启 | false | + + + + +### xssProtection + +用于启用浏览器的XSS过滤功能,以防止 `XSS` 跨站脚本攻击。 + +`X-XSS-Protection` 响应头是 `IE`,`Chrome` 和 `Safari` 的一个特性,当检测到跨站脚本攻击 (XSS (en-US))时,浏览器将停止加载页面。若网站设置了良好的 `Content-Security-Policy` 来禁用内联 JavaScript ('unsafe-inline'),现代浏览器不太需要这些保护, 但其仍然可以为尚不支持 `CSP` 的旧版浏览器的用户提供保护。 + +`X-XSS-Protection` 可以配置下述四个值 + ++ `0`: 禁止XSS过滤。 ++ `1`:启用XSS过滤(通常浏览器是默认的)。 如果检测到跨站脚本攻击,浏览器将清除页面(删除不安全的部分)。 ++ `1;mode=block`:启用XSS过滤。 如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载。 ++ `1; report=`: Chromium only,启用XSS过滤。 如果检测到跨站脚本攻击,浏览器将清除页面并使用CSP report-uri (en-US)指令的功能发送违规报告。 + +| 配置项 | 类型 | 作用描述 | 默认值 | +| --- | --- | --- | --- | +| enable | boolean | 是否开启 | false | +| value | string | X-XSS-Protection 配置 | `1; mode=block` | + diff --git a/site/versioned_docs/version-3.0.0/extensions/sequelize.md b/site/versioned_docs/version-3.0.0/extensions/sequelize.md new file mode 100644 index 000000000000..8de3423f6562 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/sequelize.md @@ -0,0 +1,780 @@ +# Sequelize + +本文档介绍如何在 Midway 中使用 Sequelize。 + +:::tip + +当前模块从 v3.4.0 开始已经重构,历史写法兼容,如果查询历史文档,请参考 [这里](../legacy/sequelize)。 + +::: + +相关信息: + +| 描述 | | +| ----------------- | --- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 和老写法的区别 + +如果想使用新版本的用法,请参考下面的流程,将老代码进行修改,新老代码不能混用。 + +升级方法: + +- 1、请在业务依赖中显式添加 `sequelize` 和 `sequelize-typescript` +- 2、不再使用 `BaseTable` 装饰器,而直接使用 `sequelize-typescript` 包导出的 `Table` 装饰器 +- 3、在 `src/config.default` 的 `sequelize` 部分配置调整,参考下面的数据源配置部分 + - 3.1 修改为数据源的形式 `sequelize.dataSource` + - 3.2 将实体模型在数据源的 `entities` 字段中声明 + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/sequelize@3 sequelize sequelize-typescript --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/sequelize": "^3.0.0", + "sequelize": "^6.21.3", + "sequelize-typescript": "^2.1.0" + // ... + }, + "devDependencies": { + // ... + } +} +``` + +## 安装数据库 Driver + +常用数据库驱动如下,选择你对应连接的数据库类型安装: + +```bash +# for MySQL or MariaDB,也可以使用 mysql2 替代 +npm install mysql --save +npm install mysql2 --save + +# for PostgreSQL or CockroachDB +npm install pg --save + +# for SQLite +npm install sqlite3 --save + +# for Microsoft SQL Server +npm install mssql --save + +# for sql.js +npm install sql.js --save + +# for Oracle +npm install oracledb --save + +# for MongoDB(experimental) +npm install mongodb --save +``` + +下面的文档,我们将以 `mysql2` 作为示例。 + + +### Directory structure + +一个基础的参考目录结构如下。 + + +``` +MyProject +├── src +│ ├── config +│ │ └── config.default.ts +│ ├── entity +│ │ └── person.entity.ts +│ ├── configuration.ts +│ └── service +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + +## 启用组件 + +在 `src/configuration.ts` 文件中启用组件。 + +```typescript +import { Configuration, ILifeCycle } from '@midwayjs/core'; +import { join } from 'path'; +import * as sequelize from '@midwayjs/sequelize'; + +@Configuration({ + imports: [ + // ... + sequelize, + ], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration implements ILifeCycle { + // ... +} +``` + +## 模型定义 + +### 1、创建 Model(Entity) + +我们通过模型和数据库关联,在应用中的模型就是数据库表,在 Sequelize 中,模型是和实体绑定的,每一个实体(Entity) 文件,即是 Model,也是实体(Entity)。 + +在示例中,需要一个实体,我们这里拿 `person` 举例。新建 entity 目录,在其中添加实体文件 `person.entity.ts` ,一个简单的实体如下。 + +```typescript +// src/entity/person.entity.ts +import { Table, Model, Column, HasMany } from 'sequelize-typescript'; + +@Table +export class Hobby extends Model { + @Column + name: string; +} + +@Table +export class Person extends Model { + @Column + name: string; + + @Column + birthday: Date; + + @HasMany(() => Hobby) + hobbies: Hobby[]; +} +``` + +要注意,这里的实体文件的每一个属性,其实是和数据库表一一对应的,基于现有的数据库表,我们往上添加内容。 + +`@Table` 装饰器可以在不传递任何参数的情况下使用,更多参数请查看 [定义选项](https://sequelize.org/v5/manual/models-definition.html#configuration) 。 + +```typescript +@Table({ + timestamps: true, + ... +}) +export class Person extends Model {} +``` + + + +### 2、主键 + +主键 (id) 将从基类 Model 继承。 一般来说主键是 Integer 类型并且是自增的。 + +主键设置有两种方法,设置 `@Column({primaryKey: true})` 或者 `@PrimaryKey`。 + +比如: + +```typescript +import { Table, Model, PrimaryKey } from 'sequelize-typescript'; + +@Table +export class Person extends Model { + @PrimaryKey + name: string; +} +``` + +### 3、时间列 + +主要指代的是 `@CreatedAt`, `@UpdatedAt`, `@DeletedAt` 单个装饰器标注的列。 + +比如: + +```typescript +import { Table, Model, CreatedAt, UpdatedAt, DeletedAt } from 'sequelize-typescript'; + +@Table +export class Person extends Model { + @CreatedAt + creationDate: Date; + + @UpdatedAt + updatedOn: Date; + + @DeletedAt + deletionDate: Date; +} +``` + +| 装饰器 | 描述 | +| ------------ | ----------------------------------------------------------------------- | +| `@CreatedAt` | 会设置 `timestamps=true` 和 `createdAt='creationDate'` | +| `@UpdatedAt` | 会设置 `timestamps=true` 和 `updatedAt='updatedOn'` | +| `@DeletedAt` | 会设置 `timestamps=true`, `paranoid=true` 和 `deletedAt='deletionDate'` | + +### 4、普通列 + +@Column 装饰器用于标注普通列,可以在不传递任何参数的情况下使用。 但是因此需要能够自动推断 js 类型(详见[类型推断](https://github.com/sequelize/sequelize-typescript#type-inference))。 + +```typescript +import { Table, Model, Column } from 'sequelize-typescript'; + +@Table +export class Person extends Model { + @Column + name: string; +} +``` + +或者指定列类型。 + +```typescript +import { Table, Column, DataType } from 'sequelize-typescript'; + +@Table +export class Person extends Model { + @Column(DataType.TEXT) + name: string; +} +``` + +更多类型描述,请参考 [这里](https://sequelize.org/v5/manual/models-definition.html#configuration)。 + +比如: + +```typescript +import { Table, Model, Column, DataType } from 'sequelize-typescript' + +@Table +export class Person extends Model { + @Column({ + type: DataType.FLOAT, + comment: 'Some value', + ... + }) + value: number; +} +``` + +| 装饰器 | 描述 | +| ------------------------------------ | ------------------------------------------------------------------------------------------------- | +| `@Column` | 使用推导的 [dataType](https://sequelize.org/v5/manual/models-definition.html#data-types) 作为类型 | +| `@Column(dataType: DataType)` | 显式设置 [dataType](https://sequelize.org/v5/manual/models-definition.html#data-types) | +| `@Column(options: AttributeOptions)` | 设置 [attribute options](https://sequelize.org/v5/manual/models-definition.html#configuration) | + +## 数据源配置 + +新版本我们启用了 [数据源机制](../data_source),在 `src/config.default.ts` 中配置: + +```typescript +// src/config/config.default.ts + +import { Person } from '../entity/person.entity'; + +export default { + // ... + sequelize: { + dataSource: { + // 第一个数据源,数据源的名字可以完全自定义 + default: { + database: 'test4', + username: 'root', + password: '123456', + host: '127.0.0.1', + port: 3306, + encrypt: false, + dialect: 'mysql', + define: { charset: 'utf8' }, + timezone: '+08:00', + // 本地的时候,可以通过 sync: true 直接 createTable + sync: false, + + // 实体形式 + entities: [Person], + + // 支持如下的扫描形式,为了兼容我们可以同时进行.js和.ts匹配️ + entities: [ + 'entity', // 指定目录 + '**/entity/*.entity.{j,t}s', // 通配加后缀匹配 + ], + }, + + // 第二个数据源 + default2: { + // ... + }, + }, + }, +}; +``` + + + +## 模型关联 + +可以通过 `HasMany` 、`@HasOne` 、`@BelongsTo`、`@BelongsToMany` 和 `@ForeignKey` 装饰器在模型中直接描述关系。 + +:::tip + +你不需要在数据库中创建外键也可以使用这个功能。 + +::: + +### 一对多 + +```typescript +import { Table, Model, Column, ForeignKey, BelongsTo, HasMany } from 'sequelize-typescript'; + +@Table +export class Player extends Model { + @Column + name: string; + + @Column + num: number; + + @ForeignKey(() => Team) + @Column + teamId: number; + + @BelongsTo(() => Team) + team: Team; +} + +@Table +export class Team extends Model { + @Column + name: string; + + @HasMany(() => Player) + players: Player[]; +} +``` + +`sequelize-typescript` 会在内部进行关联,会自动查询出相关的依赖。 + +比如通过 `find` 查询。 + +```typescript +const team = await Team.findOne({ include: [Player] }); + +team.players.forEach((player) => { + console.log(`Player ${player.name}`); +}); +``` + +### 多对多 + +```typescript +import { Table, Model, Column, ForeignKey, BelongsToMany } from 'sequelize-typescript'; + +@Table +export class Book extends Model { + @BelongsToMany(() => Author, () => BookAuthor) + authors: Author[]; +} + +@Table +export class Author extends Model { + @BelongsToMany(() => Book, () => BookAuthor) + books: Book[]; +} + +@Table +export class BookAuthor extends Model { + @ForeignKey(() => Book) + @Column + bookId: number; + + @ForeignKey(() => Author) + @Column + authorId: number; +} +``` + +上面的类型,在某些场景下是不安全的,比如上面的 `BookAuthor`,`Author` 的 `books` 的类型,可能会丢失某些属性,需要手动设置。 + +```typescript +@BelongsToMany(() => Book, () => BookAuthor) +books: Array; +``` + +### 一对一 + +对于一对一,使用 `@HasOne(...)`(关系的外键存在于另一个模型上)和 `@BelongsTo(...)`(关系的外键存在于此模型上)。 + +比如: + +```typescript +import { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript'; +import { User } from './user.entity'; + +@Table +export class Photo extends Model { + @ForeignKey(() => User) + @Column({ + comment: '用户Id', + }) + userId: number; + + @BelongsTo(() => User) + user: User; + + @Column({ + comment: '名字', + }) + name: string; +} + +@Table +export class User extends Model { + @Column + name: string; +} +``` + + + +### 模型循环依赖 + +如果你使用了 `@BelongsTo` 装饰器,很容易触发一个模型循环依赖的错误,比如: + +``` +ReferenceError: Cannot access 'Photo' before initialization +``` + +你可以将类型使用 `ReturnType` 包裹起来。 + +```typescript +import { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript'; +import { User } from './user.entity'; + +@Table +export class Photo extends Model { + // ... + @BelongsTo(() => User) + user: ReturnType<() => User>; +} +``` + + + + + +## 静态操作方法 + +如果是单个数据源,可以使用下面的静态方法。 + +### 保存 + +在需要调用的地方,使用实体模型来操作。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { Person } from '../entity/person.entity'; + +@Provide() +export class PersonService { + async createPerson() { + const person = new Person({ name: 'bob', age: 99 }); + await person.save(); + } +} +``` + +### 查找和更新 + +```typescript +import { Provide } from '@midwayjs/core'; +import { Person } from '../entity/person.entity'; + +@Provide() +export class PersonService { + async updatePerson() { + const person = await Person.findOne(); + // 更新 + person.age = 100; + await person.save(); + + await Person.update( + { + name: 'bobby', + }, + { + where: { id: 1 }, + } + ); + } +} +``` + +## Repository 模式 + +Repository 模式可以将查找、创建等静态操作从模型定义中分离出来。它还支持与多个 sequelize 实例(多数据源)一起使用。 + +### 启动 Repository 模式 + +和数据源配置相同,只是多了一个属性。 + +```typescript +// src/config/config.default.ts + +import { Person } from '../entity/person.entity'; + +export default { + // ... + sequelize: { + dataSource: { + default: { + // ... + entities: [Person], + + // 多了这一个 + repositoryMode: true, + }, + }, + sync: false, + }, +}; +``` + +如果是多个数据源,务必在每个数据源都开启该属性,开启后,原有的静态操作方法不再可用。 + +你需要使用 `Repository` 的操作方式。 + +### 使用 Repository 模式 + +基本 API 和静态操作相同,Midway 对其进行了一些简单包裹,使用 `InjectRepository` 装饰器可以在服务中注入 `Repository`。 + +```typescript +import { Controller, Get } from '@midwayjs/core'; +import { InjectRepository } from '@midwayjs/sequelize'; +import { Photo } from '../entity/photo.entity'; +import { User } from '../entity/user.entity'; +import { Op } from 'sequelize'; +import { Repository } from 'sequelize-typescript'; + +@Controller('/') +export class HomeController { + @InjectRepository(User) + userRepository: Repository; + + @InjectRepository(Photo) + photoRepository: Repository; + + @Get('/') + async home() { + // 查询 + let result = await this.photoRepository.findAll(); + console.log(result); + + // 新增 + await this.photoRepository.create({ + name: '123', + }); + + // 删除 + await this.photoRepository.destroy({ + where: { + name: '123', + }, + }); + + // 联合查询 + // SELECT * FROM photo WHERE name = "23" OR name = "34"; + let result = await this.photoRepository.findAll({ + where: { + [Op.or]: [{ name: '23' }, { name: '34' }], + }, + }); + // => result + + // 连表查询 + let result = await this.userRepository.findAll({ include: [Photo] }); + // => result + } +} +``` + +关于 OP 的更多用法:[https://sequelize.org/v5/manual/querying.html](https://sequelize.org/v5/manual/querying.html) + +### 多库的支持 + +在 Repository 模式下,我们可以在 `InjectRepository` 参数中指定特定的数据源。 + +```typescript +import { Controller } from '@midwayjs/core'; +import { InjectRepository } from '@midwayjs/sequelize'; +import { Photo } from '../entity/photo.entity'; +import { User } from '../entity/user.entity'; +import { Repository } from 'sequelize-typescript'; + +@Controller('/') +export class HomeController { + // 指定某个数据源 + @InjectRepository(User, 'default') + userRepository: Repository; + // ... +} +``` + + +## 高级功能 + +### 数据源同步配置 + +sequelize 在同步数据源时可以添加 sync 的参数。 + +```typescript +export default { + // ... + sequelize: { + dataSource: { + default: { + sync: true, + syncOptions: { + force: false, + alter: true, + }, + }, + }, + // 多个数据源时可以用这个指定默认的数据源 + defaultDataSourceName: 'default', + }, +}; +``` + +### 指定默认数据源 + +在包含多个数据源时,可以指定默认的数据源。 + +```typescript +export default { + // ... + sequelize: { + dataSource: { + default1: { + // ... + }, + default2: { + // ... + }, + }, + // 多个数据源时可以用这个指定默认的数据源 + defaultDataSourceName: 'default1', + }, +}; +``` + + + +### 获取数据源 + +数据源即创建出的 sequelize 对象,我们可以通过注入内置的数据源管理器来获取。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import { SequelizeDataSourceManager } from '@midwayjs/sequelize'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + + async onReady(container: IMidwayContainer) { + const dataSourceManager = await container.getAsync(SequelizeDataSourceManager); + const conn = dataSourceManager.getDataSource('default'); + await conn.authenticate(); + } +} +``` + +从 v3.8.0 开始,也可以通过装饰器注入。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import { InjectDataSource } from '@midwayjs/sequelize'; +import { Sequelize } from 'sequelize-typescript'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + + // 注入默认数据源 + @InjectDataSource() + defaultDataSource: Sequelize; + + // 注入自定义数据源 + @InjectDataSource('default1') + customDataSource: Sequelize; + + async onReady(container: IMidwayContainer) { + // ... + } +} +``` + + + +## 常见问题 + +### 1、Dialect needs to be explicitly supplied as of v4.0.0 + +原因为配置中数据源没有指定 `dialect` 字段,确认数据源的结构,格式以及配置合并的结果。 + + + +### 2、生成实体列 + +请参考社区提供的模块,如 [sequelize-typescript-generator](https://github.com/spinlud/sequelize-typescript-generator) + + + +### 3、Raw Query + +如果遇到比较复杂的,可以使用 [raw query 方法](https://sequelize.org/v5/manual/raw-queries.html) + + + +### 4、TS2612 错误 + +如果你的模型列报了 TS2612 错误,比如: + +``` +src/entity/AesTenantConfigInfo.ts:29:6 - error TS2612: Property 'id' will overwrite the base property in 'Model'. If this is intentional, add an initializer. Otherwise, add a 'declare' modifier or remove the redundant declaration. + +29 id?: number; + ~~ +``` + +可以将其赋一个空值。 + +```typescript +import { Table, Column } from 'sequelize-typescript'; + +@Table +export class User extends Model { + @Column({ + primaryKey: true, + autoIncrement: true, + type: DataType.BIGINT, + }) + id?: number = undefined; +} +``` + + + +## 其他 + +- 上面的文档,翻译自 sequelize-typescript,更多 API ,请参考 [英文文档](<(https://github.com/sequelize/sequelize-typescrip)>) +- 一些 [案例](https://github.com/ddzyan/midway-practice) diff --git a/site/versioned_docs/version-3.0.0/extensions/socketio.md b/site/versioned_docs/version-3.0.0/extensions/socketio.md new file mode 100644 index 000000000000..5c0d64b29283 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/socketio.md @@ -0,0 +1,1098 @@ +# SocketIO + +Socket.io 是一个业界常用库,可用于在浏览器和服务器之间进行实时,双向和基于事件的通信。 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01YTye6U22gICvarVur_!!6000000007149-2-tps-1204-352.png) + + +Midway 提供了对 Socket.io 的支持和封装,能够简单的创建一个 Socket.io 服务。本篇内容演示了如何在 Midway 体系下,提供 Socket.io 服务的方法。 + +Midway 当前采用了最新的 [Socket.io (v4.0.0)](https://socket.io/docs/v4) 进行开发。 + + + +相关信息: + +**提供服务** + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ❌ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ✅ | +| 包含独立日志 | ❌ | + + + +## 安装依赖 + + +在现有项目中安装 Socket.io 的依赖。 + +```bash +$ npm i @midwayjs/socketio@3 --save +## 客户端可选 +$ npm i @types/socket.io-client socket.io-client --save-dev +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/socket.io": "^3.0.0", + // 客户端可选 + "socket.io-client": "^4.4.1", + // ... + }, + "devDependencies": { + // 客户端可选 + "@types/socket.io-client": "^1.4.36", + // ... + } +} +``` + + + +## 开启组件 + +`@midwayjs/socket.io` 可以作为独立主框架使用。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as socketio from '@midwayjs/socketio'; + +@Configuration({ + imports: [socketio], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + +``` + +也可以附加在其他的主框架下,比如 `@midwayjs/koa` 。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as socketio from '@midwayjs/socketio'; + +@Configuration({ + imports: [koa, socketio], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + + +``` + + + + +## 目录结构 + + +下面是 Socket.io 项目的基础目录结构,和传统应用类似,我们创建了 `socket` 目录,用户存放 Soscket.io 业务的服务代码。 +``` +. +├── package.json +├── src +│ ├── configuration.ts ## 入口配置文件 +│ ├── interface.ts +│ └── socket ## socket.io 服务的文件 +│ └── hello.controller.ts +├── test +├── bootstrap.js ## 服务启动入口 +└── tsconfig.json +``` + + +## Socket.io 工作原理 + + +Socket.io 服务器和 Socket.io 客户端(浏览器,Node.js 或另一种编程语言)之间的双向通道通过 [WebSocket连接](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) 建立起来,在不可用时,将使用 HTTP 长轮询作为备用手段。 + + +Socket.io 代码是基于 Engine.io 库搭建起来的,是属于 Engine.io 的上层实现。Engine.io 负责整个服务端和客户端连接的部分,包括连接检查,传输方式等等。而 Socket.io 负责上层的重连,封包缓冲,广播等等特性。 + + +Socket.io(Engine.io)实现了两种 Transports(传输方式)。 + + +第一种是 HTTP 长轮询。HTTP Get 请求用于 long-running(长连接),Post 请求用于 short-running(短连接)。 + + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01xhdZHA1XTEpUue7CQ_!!6000000002924-2-tps-1778-1068.png) + +第二种是 WebSocket 协议,直接基于 [WebSocket Connection](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) 实现。它在服务器和客户端之间提供了双向且低延迟的通信通道。 + + +在默认的情况下,Socket.io 会先采用 HTTP 长轮询进行连接,并发送一个类似下面结构的数据。 +```typescript +{ + "sid": "FSDjX-WRwSA4zTZMALqx", // 连接的 session id + "upgrades": ["websocket"], // 可升级的协议 + "pingInterval": 25000, // 心跳时间间隔 + "pingTimeout": 20000 // 心跳超时时间 +} +``` + +当当前的服务满足升级到 WebSocket 协议的要求时,会自动升级到 WebSocket 协议,如下图。 +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01QHZi9x1mz2ZLecco3_!!6000000005024-2-tps-585-216.png) + +- 1、第一次握手,传输 sid 等结构 +- 2、使用 HTTP 长轮询发送数据 +- 3、使用 HTTP 长轮询返回数据 +- 4、升级协议,使用 WebSocket 协议发送数据 +- 5、当协议升级后,关闭之前的长轮询 + + + +之后就开始正常的 WebSocket 通信了。 + + +## 提供 Socket 服务 + + +Midway 通过 `@WSController` 装饰器定义 Socket 服务。 +```typescript +@WSController('/') +export class HelloController { + // ... +} +``` +`@WSController` 的入参,指代了每个 Socket 的 Namespace(非 path)。如果不提供 Namespace,每个 Socket.io 会自动创建一个 `/` 的 Namespace,并且将客户端连接都归到其中。 + +:::info +这里的 namespace 支持字符串和正则。 +::: + + +当 Namespace 有客户端连接时,会触发 `connection` 事件,我们在代码中可以使用 `@OnWSConnection()` 装饰器来修饰一个方法,当每个客户端第一次连接到该 Namespace 时,将自动调用该方法。 +```typescript +import { WSController, OnWSConnection, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/socketio'; + +@WSController('/') +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSConnection() + async onConnectionMethod() { + console.log('on client connect', this.ctx.id); + } +} + +``` + + +:::info +这里的 ctx 等价于 socket 实例。 +::: + + +## 消息和响应 + + +Socket.io 是通过事件的监听方式来获取数据。Midway 提供了 `@OnWSMessage()` 装饰器来格式化接收到的事件,每次客户端发送事件,被修饰的方法都将被执行。 +```typescript +import { WSController, Provide, OnWSMessage, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/socketio'; + +@WSController('/') +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSMessage('myEvent') + async gotMessage(data) { + console.log('on data got', this.ctx.id, data); + } +} + +``` +注意,由于 Socket.io 在一个事件中可以传递多个数据,这里的参数可以是多个。 +```typescript + @OnWSMessage('myEvent') + async gotMessage(data1, data2, data3) { + // ... + } +``` +当获取到数据之后,通过业务逻辑处理数据,然后将结果返回给客户端,返回的时候,我们也是通过另一个事件发送给客户端。 + + +通过 `@WSEmit` 装饰器来将方法的返回值返回给客户端。 +```typescript +import { WSController, OnWSConnection, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/socketio'; + +@WSController('/') +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSMessage('myEvent') + @WSEmit('myEventResult') + async gotMessage() { + return 'hello world'; // 这里将 hello world 字符串返回给客户端 + } +} +``` +上面的代码,我们的方法返回值 hello world,将自动发送给客户端监听的 `myEventResult` 事件。 + + + +## Socket 中间件 + +Socket 中的中间件的写法和 [Web 中间件 ](../middleware)相似,但是加载的时机略有不同。 + +由于 Socket 有连接和接收消息两个阶段,所以中间件以此分为几类。 + +- 全局 Connection 中间件,会对所有 namespace 下的 connection 生效 +- 全局 Message 中间件,会对所有 namespace 下的 message 生效 +- Controller 中间件,会对单个 namespace 下的 connection 和 message 生效 +- Connection 中间件,会对单个 namespace 下的 connection 生效 +- Message 中间件,会对单个 namespace 下的 message 生效 + +### 中间件写法 + +注意,中间件必须通过 `return` 返回结果。 + +```typescript +// src/middleware/socket.middleware.ts +import { Middleware } from '@midwayjs/core'; +import { Context, NextFunction } from '@midwayjs/socketio'; + +@Middleware() +export class SocketMiddleware { + resolve() { + return async (ctx: Context, next: NextFunction) => { + // ... + return await next(); + } + } +} + +``` + + + +### 全局中间件 + +和 Web 中间件类似,通过 `socket.io` 的 app 实例,注册中间件。 + +```typescript +import * as socketio from '@midwayjs/socketio'; + +@Configuration({ + imports: [ + socketio + ], + // ... +}) +export class MainConfiguration { + + @App('socketIO') + app: Application; + + async onReady() { + // 可以注册全局 connection 中间件 + this.app.useConnectionMiddleware(SocketMiddleware); + // 也可以注册全局 Message 中间件 + this.app.useMiddleware(SocketMiddleware); + } +} + +``` + + + +### Namespace 中的中间件 + +通过装饰器,注册不同阶段的中间件。 + +比如 Namespace 级别的中间件,会对单个 namespace 下的 connection 和 message 生效。 + +```typescript +// ... + +// Namespace 级别的中间件 +@WSController('/api', { middleware: [SocketMiddleware]}) +export class APIController { +} + +``` + +Connection 中间件,在连接时生效。 + +```typescript +// ... + +@WSController('/api') +export class APIController { + + // Connection 触发时的中间件 + @OnWSConnection({ + middleware: [SocketMiddleware] + }) + init() { + // ... + } +} +``` + +Message 中间件,接收到特定消息时生效。 + +```typescript +// ... + +@WSController('/api') +export class APIController { + + // Message 触发时的中间件 + @OnWSMessage('my', { + middleware: [SocketMiddleware] + }) + @WSEmit('ok') + async gotMyMessage() { + // ... + } +} +``` + + + +## 本地测试 + +由于 socket.io 框架可以独立启动(依附于默认的 http 服务,也可以和其他 midway 框架一起启动)。 + +当作为独立框架启动时,需要指定端口。 + +```typescript +// src/config/config.default +export default { + // ... + socketIO: { + port: 3000, + }, +} +``` + +当作为副框架启动时(比如和 http ,由于 http 在单测时未指定端口(使用 supertest 自动生成),无法很好的测试,可以仅在测试环境显式指定一个端口。 + +```typescript +// src/config/config.unittest +export default { + // ... + koa: { + port: null, + }, + socketIO: { + port: 3000, + }, +} +``` + +:::tip + +- 1、这里的端口仅为 WebSocket 服务在测试时启动的端口 +- 2、koa 中的端口为 null,即意味着在测试环境下,不配置端口,不会启动 http 服务 + +::: + + +和其他 Midway 测试方法一样,我们使用 `createApp` 启动项目。 + + +```typescript +import { createApp, close } from '@midwayjs/mock' +// 这里使用的 Framework 定义,以主框架为准 +import { Framework } from '@midwayjs/koa'; + +describe('/test/index.test.ts', () => { + it('should create app and test socket.io', async () => { + const app = await createApp(); + + //... + + await close(app); + }); + +}); +``` + + +你可以直接使用 `socket.io-client` 来测试。也可以使用 Midway 提供的基于 `socket.io-client` 模块封装的测试客户端。 + + +假如我们的服务端处理逻辑如下(返回客户端传递的数据相加的结果): +```typescript +@OnWSMessage('myEvent') +@WSEmit('myEventResult') +async gotMessage(data1, data2, data3) { + return { + name: 'harry', + result: data1 + data2 + data3, + }; +} +``` + + +测试代码如下: +```typescript +import { createApp, close } from '@midwayjs/mock' +import { Framework } from '@midwayjs/koa'; +import { createSocketIOClient } from '@midwayjs/mock'; +import { once } from 'events'; + +describe('/test/index.test.ts', () => { + it('should test create socket app', async () => { + + // 创建一个服务 + const app = await createApp(); + + // 创建一个对应的客户端 + const client = await createSocketIOClient({ + port: 3000, + }); + + // 拿到结果返回 + const data = await new Promise(resolve => { + client.on('myEventResult', resolve); + // 发送事件 + client.send('myEvent', 1, 2, 3); + }); + + // 判断结果 + expect(data).toEqual({ + name: 'harry', + result: 6, + }); + + // 关闭客户端 + await client.close(); + // 关闭服务端 + await close(app); + }); + +}); +``` +如果多个客户端,也可以使用更简单的写法,使用 node 自带的 `events` 模块的 `once` 方法来优化,就会变成下面的代码。 +```typescript +import { createApp, close } from '@midwayjs/mock' +import { Framework } from '@midwayjs/koa'; +import { createSocketIOClient } from '@midwayjs/mock'; +import { once } from 'events'; + +describe('/test/index.test.ts', () => { + + it('should test create socket app', async () => { + + // 创建一个服务 + const app = await createApp(); + + // 创建一个对应的客户端 + const client = await createSocketIOClient({ + port: 3000, + }); + + // 用事件的 promise 写法监听 + const gotEvent = once(client, 'myEventResult'); + // 发送事件 + client.send('myEvent', 1, 2, 3); + // 等待返回 + const [data] = await gotEvent; + // 判断结果 + expect(data).toEqual({ + name: 'harry', + result: 6, + }); + + // 关闭客户端 + await client.close(); + // 关闭服务端 + await close(app); + }); + +}); +``` +两种写法效果相同,按自己理解的写就行。 + + +## 等待回执(ack)的消息 + + +Socket.io 支持一种直接返回消息的写法。当客户端传递消息的时候,如果最后一个参数为一个 function(callback),则服务端可以拿到这个 callback,将数据直接返回给客户端,不需要创建一个新的消息。 + + +我们的服务代码不需要变化, `@midwayjs/socketio` 内部会判断最后一个参数,自动返回给客户端。 + + +比如,服务端代码: +```typescript +@OnWSMessage('myEvent') +@WSEmit('myEventResult') +async gotMessage(data1, data2, data3) { + return { + name: 'harry', + result: data1 + data2 + data3, + }; +} +``` +客户端测试代码: +```typescript +import { createApp, close } from '@midwayjs/mock' +import { Framework } from '@midwayjs/koa'; +import { createSocketIOClient } from '@midwayjs/mock'; +import { once } from 'events'; + +describe('/test/index.test.ts', () => { + + it('should test create socket app', async () => { + + // 创建一个服务 + const app = await createApp(); + + // 创建一个对应的客户端 + const client = await createSocketIOClient({ + port: 3000, + }); + + // 发送事件,这里使用了 await 的写法 + const data = await client.sendWithAck('myEvent', 1, 2, 3); + + // 判断结果 + expect(data).toEqual({ + name: 'harry', + result: 6, + }); + + // 关闭客户端 + await client.close(); + // 关闭服务端 + await close(app); + }); + +}); +``` + + + +## 常见的消息和广播 + + +以下面的代码示例举例: + + +```typescript +import { Context, Application } from '@midwayjs/socketio'; +import { WSController, OnWSMessage, WSEmit, App, Inject } from '@midwayjs/core'; + +@WSController('/') +export class HelloSocketController { + + @Inject() + ctx: Context; + + @App('socketIO') + app: Application; + + @OnWSMessage('myEvent') + @WSEmit('myEventResult') + async gotMessage() { + // TODO + } +} +``` + + +发送给客户端(也可以用装饰器形式直接 return)。 +```typescript +this.ctx.emit("hello", "can you hear me?", 1, 2, "abc"); +``` +发送给的所有除发件人以外的所有客户端。 +```typescript +this.ctx.broadcast.emit("broadcast", "hello friends!"); +``` +发送给所有在 `game` 房间的客户端(除了发送者)。 +```typescript +this.ctx.to("game").emit("nice game", "let's play a game"); +``` +发送给所有的 `game1` 和 `game2` 房间的客户端(除了发送者)。 +```typescript +this.ctx.to("game1").to("game2").emit("nice game", "let's play a game (too)"); +``` +发送给所有 `game` 房间的客户端,包括发送者。 +```typescript +this.app.in("game").emit("big-announcement", "the game will start soon"); +``` +给 `myNamespace` 命名空间的客户端广播,包括发送者。 +```typescript +// 从 app 发送 +this.app.of("myNamespace").emit("bigger-announcement", "the tournament will start soon"); +// 从 ctx 发送 +this.ctx.nsp.emit("bigger-announcement", "the tournament will start soon"); +``` +发送到特定的 namespace 和 room,包括发送者。 +```typescript +// 从 app 发送 +this.app.of("myNamespace").to("room").emit("event", "message"); +// 从 ctx 发送 +this.ctx.nsp.emit("bigger-announcement", "the tournament will start soon"); +``` +发送给所有连接到当前节点上的客户端(多个节点的时候,就是多进程) +```typescript +this.app.local.emit("hi", "my lovely babies"); +``` + +## Application(io 对象) + + +传统的 Socket.io 服务端创建代码如下: + +```typescript +const io = require("socket.io")(3000); + +io.on("connection", socket => { + // ... +}); +``` + +在 `@midwayjs/socketio` 框架中,Application 实例即为该 io 实例,类型和能力保持一致。即通过 `@App` 装饰器注入的 app 实例,即为 io 对象。 + + +我们可以通过该对象做一些全局的事情。 + + +比如获取所有的 socket 实例。 + +```typescript +// 返回所有的 socket 实例 +const sockets = await app.fetchSockets(); + +// 返回所有的在 room1 的 socket 实例 +const sockets = await app.in("room1").fetchSockets(); + +// 返回特定 socketId 的实例 +const sockets = await app.in(theSocketId).fetchSockets(); +``` + +多框架下,主框架一般为 Web 框架,我们可以通过指定 key 获取 Socket.io 的 app。 + +```typescript +import { Application as SocketApplication } from '@midwayjs/socketio'; +import { Controller, App } from '@midwayjs/core'; + +@Controller() +export class UserController { + + @App('socketIO') + socketApp: SocketApplication; +} +``` + + +这样我们可以通过 `@midwayjs/socketio` 的 app 对象(等价于 io),调用现有的 socket 连接。 + + +比如,HTTP 请求调用进来对特定 namespace 下的所有客户端广播: + +```typescript +import { Application as SocketApplication } from '@midwayjs/socketio'; +import { Provide, Controller, App, Get } from '@midwayjs/core'; + +@Controller() +export class UserController { + + @App('socketIO') + socketApp: SocketApplication; + + @Get() + async invoke() { + // 对 / 下的连接做广播 + this.socketApp.of('/').emit('hi', 'everyone'); + } +} +``` + +更多的 io API,请参考 [Socket.io Server instance 文档](https://socket.io/docs/v4/server-instance/)。 + + + +## Socket 部署 + +### Socket 服务端口 + +`@midwayjs/socketio` 的配置样例如下: + +```typescript +// src/config/config.default +export default { + // ... + socketIO: { + port: 7001, + }, +} +``` + +当 `@midwayjs/socketio` 和其他 `@midwayjs/web` , `@midwayjs/koa` , `@midwayjs/express` 同时启用时,可以复用http 端口。 + +```typescript +// src/config/config.default +export default { + // ... + koa: { + port: 7001, + }, + socketIO: { + // 这里不配置即可 + }, +} +``` + + + +### Nginx 配置 + +一般来说,我们的 Node.js 服务前都会有 Nginx 等类似的反向代理服务,这里以 Nginx 的配置为例。 + +```nginx +http { + server { + listen 80; + server_name example.com; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + + proxy_pass http://localhost:7001; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + } +} +``` + + + +## 配置 + +### 可用配置 + +| 属性 | 类型 | 描述 | +| --- | --- | --- | +| port | number | 可选,如果传递了该端口,socket.io 内部会创建一个该端口的 HTTP 服务,并将 socket 服务 attach 在其之上。如果希望和 midway 其他的 web 框架配合使用,请不要传递该参数。 | +| path | string | 可选,服务端 path | +| adapter | object | 分布式处理的适配器,比如可以配置 redis-adapter | +| connectTimeout | number | 客户端超时时间,单位 ms,默认值 _45000_ | + +更多的启动选项,请参考 [Socket.io 文档](https://socket.io/docs/v4/server-api/#new-Server-httpServer-options)。 + + + +## 适配器 + +适配器是用于 Socket.io 在分布式部署时,在多台机器,多个进程能够进行通信的一层适配层,当前 socket.io 官方提供的适配器有几种: + + + +- 1、cluster-adapter 用于在单台机器,多进程之间适配 +- 2、redis-adapter 用于在多台机器,多个进程之间适配 + + + +在分布式场景下,我们一般使用 redis-adapater 来实现功能。 + + + + +### 配置 redis 适配器 + +`@midwayjs/socketio` 提供了一个适配器(adapter)的入口配置,只需要初始化适配器实例,传入即可。 + +:::tip + +Socket.io 官方已经更新了原有的适配器包名,现在的包名为 `@socket.io/redis-adapter`(原来叫 `socket.io-redis`),配置有更新,迁移参考请查看 [官方文档](https://github.com/socketio/socket.io-redis-adapter#migrating-from-socketio-redis)。 + +::: + +安装如下: + +```bash +$ npm i @socket.io/redis-adapter --save +``` + + + +新版本配置的示例如下,更多的配置可以参考 [官方文档](https://github.com/socketio/socket.io-redis-adapter): + +```typescript +// src/config/config.default +import { createAdapter } from '@socket.io/redis-adapter'; +import Redis from 'ioredis'; + +// github 文档创建 redis 实例 + +const pubClient = new Redis(/* redis 配置 */); +const subClient = pubClient.duplicate(); + +export default { + // ... + socketIO: { + adapter: createAdapter(pubClient, subClient) + }, +} +``` + +通过使用 `@socket.io/redis-adapter` 适配器运行 Socket.io,可以在不同的进程或服务器中运行多个 Socket.io 实例,这些实例都可以相互广播和发送事件。 + +此外,还有一些 Adapter 上的特殊 API,具体可以查看 [文档](https://github.com/socketio/socket.io-redis-adapter#api)。 + + + +## 粘性会话 + +由于 Node.js 经常在启动时使用多进程(cluster)模式,如果同一个会话(sid)无法多次访问到同一个进程上,socket.io 就会报错。 + +解决办法有两种。 + + + +### 使用 WebSocket 协议 + +最简单的方法,只启用 WebSocket 协议(禁用长轮询),这样就可以规避上述问题。 + +你需要在服务端和客户端同时配置。 + +```typescript +// 服务端 +export default { + // ... + socketIO: { + // ... + transports: ['websocket'], + }, +} + +// 客户端 +const socket = io("http://127.0.0.1:7001", { + transports: ['websocket'] +}); +``` + + + +### 调整进程模型 + +这是相对复杂的方法,但是在 pm2 部署的场景下,既要支持粘性会话又要启用轮询支持,这是唯一的解法。 + +第一步,禁用配置中启动的端口,比如: + +```typescript +// src/config/config.default +export default { + koa: { + // port: 7001, + }, + socketIO: { + // ... + }, +}; + +``` + +如果开发需要,可以在 `config.local` 中加上端口,或者直接在 `package.json` 的 scripts 中加上端口。 + +```json +"scripts": { + "dev": "cross-env NODE_ENV=local midway-bin dev --ts --port=7001", +}, +``` + + + +第二步,调整你的 `bootstrap.js` 文件内容,使其变为下面的代码。 + +```typescript +const { Bootstrap, ClusterManager, setupStickyMaster } = require('@midwayjs/bootstrap'); +const http = require('http'); + +// 创建一个进程管理器,处理子进程 +const clusterManager = new ClusterManager({ + exec: __filename, + count: 4, + sticky: true, // 开启粘性会话支持 +}); + +if (clusterManager.isPrimary()) { + // 主进程启动一个 http server 做监听 + const httpServer = http.createServer(); + setupStickyMaster(httpServer); + + // 启动子进程 + clusterManager.start().then(() => { + // 监听端口 + httpServer.listen(7001); + console.log('main process is ok'); + }); + + clusterManager.onStop(async () => { + // 停止时关闭 http server + await httpServer.close(); + }); +} else { + // 子进程逻辑 + Bootstrap + .run() + .then(() => { + console.log('child is ready'); + }); +} + +``` + +在 pm2 启动时,无需指定 `-i` 参数来启动 worker,直接 `pm2 --name=xxx ./bootstrap.js` 使其只启动一个进程。 + + + + +## 常见 API + + +### 获取连接数 +```typescript +const count = app.engine.clientsCount; // 获取所有的连接数 +const count = app.of('/').sockets.size; // 获取单个 namespace 里的连接数 +``` + + +### 修改 sid 生成 +```typescript +const uuid = require("uuid"); + +app.engine.generateId = (req) => { + return uuid.v4(); // must be unique across all Socket.IO servers +} +``` + + + +## 常见问题 + + +### 服务端/客户端没连上,没响应 + + +1、端口服务端和客户端一致 + + +```typescript +export default { + koa: { + port: 7001, // 这里的端口 + } +} + +// 或者 + +export default { + socketIO: { + port: 7001, // 这里的端口 + } +} +``` + + +和下面的端口要一致。 +```typescript +// socket.io client +const socket = io('************:7001', { + //... +}); + +// midway 的 socket.io 测试客户端 +const client = await createSocketIOClient({ + port: 7001 +}); +``` + +2、服务端的 path 和客户端的 path 要保持一致。path 指的是启动参数的部分。 + +```typescript +// config.default +export default { + socketIO: { + path: '/testPath' // 这里是服务端 path + } +} +``` +和下面的 path 要一致 + +```typescript +// socket.io client +const socket = io('************:7001', { + path: '/testPath' // 这里是客户端的 path +}); + +// midway 的 socket.io 测试客户端 +const client = await createSocketIOClient({ + path: '/testPath' +}); +``` + + + +3、服务端的 namespace 和客户端的 namespace 要保持一致。 + +```typescript +// server +@WSController('/test') // 这里是服务端的 namespace +export class HelloController { +} + +// socket.io client +const io = require("socket.io-client") +io('*****:3000/test', {}); // 这里是客户端的 namespace + + +// midway 的 socket.io 测试客户端 +const client = await createSocketIOClient({ + namespace: '/test', +}); +``` + + + +### 配置 CORS + + +如果出现跨域错误,需要在启动的时候配置 cors 信息。 +```typescript +// config.default +export default { + socketIO: { + cors: { + origin: "http://localhost:8080", + methods: ["GET", "POST"] + } + } +} +``` +具体参数可以参考 [Socket.io Handling CORS](https://socket.io/docs/v4/handling-cors/)。 diff --git a/site/versioned_docs/version-3.0.0/extensions/static_file.md b/site/versioned_docs/version-3.0.0/extensions/static_file.md new file mode 100644 index 000000000000..6045e57b3690 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/static_file.md @@ -0,0 +1,226 @@ +# 静态文件托管 + +midway 提供了基于 [koa-static-cache](https://github.com/koajs/static-cache) 模块的静态资源托管组件。 + +相关信息: + +| web 支持情况 | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | 💬 | +| @midwayjs/web | ✅ | +| @midwayjs/express | ❌ | + +:::caution + +💬 部分函数计算平台不支持流式请求响应,请参考对应平台能力。 + +::: + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/static-file@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/static-file": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## 引入组件 + + +首先,引入 组件,在 `configuration.ts` 中导入: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as staticFile from '@midwayjs/static-file'; +import { join } from 'path' + +@Configuration({ + imports: [ + koa, + staticFile + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + + +## 使用 + +默认情况下,会托管项目根目录下的 `public` 目录中的内容。 + +比如: + +``` +➜ my_midway_app tree +. +├── src +├── public +| ├── index.html +│ └── hello.js +│ +├── test +├── package.json +└── tsconfig.json +``` + +我们可以直接使用路径访问 `GET /public/index.html` 并获取相应的结果。 + + + +## 配置 + +### 修改默认行为 + +资源的托管使用的是 `dirs` 字段,其中有一个 `default` 属性,我们可以修改它。 + +```typescript +// {app_root}/src/config/config.default.ts +export default { + // ... + staticFile: { + dirs: { + default: { + prefix: '/', + dir: 'xxx', + }, + } + }, +} +``` + +`dirs` 中的对象值,会和 `staticFile` 下的值合并后,传入 `koa-static-cache` 中间件中。 + +### 增加新的目录 + +可以对 dirs 做修改,增加一个新的目录。key 不重复即可,value 会和默认的配置合并。 + +```typescript +// {app_root}/src/config/config.default.ts +export default { + // ... + staticFile: { + dirs: { + default: { + prefix: '/', + dir: 'xxx', + }, + another: { + prefix: '/', + dir: 'xxx', + }, + } + // ... + }, +} +``` + + + +### 可用配置 + +支持所有的 [koa-static-cache](https://github.com/koajs/static-cache) 配置,默认配置如下: + +| 属性名 | 默认值 | 描述 | +| ------- |---------------------------------------------------| ------------------------------------------------------------ | +| dirs | \{"default": \{prefix: "/public", "dir": "xxxx"}} | 托管的目录,为了支持多个目录,是个对象。
除了 default 之外,其他的 key 可以随意添加,dirs 中的对象值会和外部默认值做合并 | +| dynamic | true | 动态加载文件,而不是在初始化读取后做缓存 | +| preload | false | 是否在初始化缓存 | +| maxAge | prod 为 31536000,其他为 0 | 缓存的最大时间 | +| buffer | prod 为 true,其余为 false | 使用 buffer 字符返回 | + +更多配置,请参考 [koa-static-cache](https://github.com/koajs/static-cache) 。 + + + +## 常见问题 + +### 1、函数下路由未生效 + +函数路由需要显式配置才能生效,一般来说,会添加一个通配的路由用于静态文件,如 `/*`,或者 `/public/*`。 + +```typescript +import { + Provide, + ServerlessTrigger, + ServerlessTriggerType, +} from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloHTTPService { + + @ServerlessTrigger(ServerlessTriggerType.HTTP, { + path: '/public/*', + method: 'get', + }) + async handleStaticFile() { + // 这个函数可以没有方法体,只是为了让网关注册一个额外的路由 + } +} + +``` + + + +### 2、默认 index.html + +由于 [koa-static-cache](https://github.com/koajs/static-cache) 不支持默认 `index.html` 的配置,可以通过它的 alias 功能来解决。 + +可以配置把 `/` 指向到 `/index.html` 即可,不支持通配和正则。 + +```typescript +export default { + // ... + staticFile: { + dirs: { + default: { + prefix: '/', + alias: { + '/': '/index.html', + }, + }, + }, + // ... + }, +} +``` + + + +### 3、egg(@midwayjs/web)下不生效的情况 + +由于 egg 自带了静态托管插件,如果开启了 static 插件,会和此组件冲突。 + +如需使用本组件,请务必关闭 egg 插件。 + +```typescript +// src/config/plugin.ts +import { EggPlugin } from 'egg'; +export default { + // ... + static: false, +} as EggPlugin; +``` diff --git a/site/versioned_docs/version-3.0.0/extensions/swagger.md b/site/versioned_docs/version-3.0.0/extensions/swagger.md new file mode 100644 index 000000000000..ba39a60b46c8 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/swagger.md @@ -0,0 +1,1288 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Swagger +基于最新的 [OpenAPI 3.0.3](https://swagger.io/specification/) 实现了新版的 Swagger 组件。 + +相关信息: + +| 描述 | | +| ----------------- | --- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ❌ | +| 可用于一体化 | ❌ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 安装依赖 + +```bash +$ npm install @midwayjs/swagger@3 --save +$ npm install swagger-ui-dist --save-dev +``` + +如果想要在服务器上输出 Swagger API 页面,则需要将 `swagger-ui-dist` 安装到依赖中。 + +```bash +$ npm install swagger-ui-dist --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/swagger": "^3.0.0", + // 如果你希望在服务器上使用 + "swagger-ui-dist": "^4.2.1", + // ... + }, + "devDependencies": { + // 如果你不希望在服务器上使用 + "swagger-ui-dist": "^4.2.1", + // ... + } +} +``` + + + +## 开启组件 + +在 ```configuration.ts``` 中增加组件。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as swagger from '@midwayjs/swagger'; + +@Configuration({ + imports: [ + // ... + swagger + ] +}) +export class MainConfiguration { + +} +``` + +可以配置启用的环境,比如下面的代码指的是 **只在 local 环境下启用**。 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as swagger from '@midwayjs/swagger'; + +@Configuration({ + imports: [ + // ... + { + component: swagger, + enabledEnvironment: ['local'] + } + ] +}) +export class MainConfiguration { + +} +``` + +然后启动项目,访问地址: + +- UI: http://127.0.0.1:7001/swagger-ui/index.html +- JSON: http://127.0.0.1:7001/swagger-ui/index.json + +路径可以通过 `swaggerPath` 参数配置。 + + + +## 数据类型 + +### 自动类型提取 + +Swagger 组件会识别各个 `@Controller` 中每个路由方法的 `@Body()`、`@Query()`、`@Param()` 装饰器,提取路由方法参数和类型。 + +比如下面的代码: + +```typescript +@Get('/') +async home( + @Query('uid') uid: number, + @Query('tid') tid: string, + @Query('isBoolean') isBoolean: boolean, +) { + // ... +} +``` + +基础的布尔,字符串,数字类型展示效果如下: + +![](https://img.alicdn.com/imgextra/i2/O1CN01KGk0B325xe6cV5HCo_!!6000000007593-2-tps-1110-854.png) + + + +### 类型和 Schema + +我们常在参数使用对象,并使用定义好的类作为类型,这个时候 swagger 组件也能自动识别,同时也能和普通的类型进行组合识别。 + +比如下面的代码: + +```typescript +@Post('/:id', { summary: 'test'}) +async create(@Body() createCatDto: CreateCatDto, @Param('id') id: number) { + // ... +} +``` + +`CreateCatDto` 类型的定义如下,我们使用 `ApiProperty` 将其中的每个属性都进行了定义。 + +```typescript +import { ApiProperty } from "@midwayjs/swagger"; + +export class CreateCatDto { + @ApiProperty({ example: 'Kitty', description: 'The name of the Catname'}) + name: string; + + @ApiProperty({ example: '1', description: 'The name of the Catage'}) + age: number; + + @ApiProperty({ example: 'bbbb', description: 'The name of the Catbreed'}) + breed: string; +} +``` + +效果如下,组件会自动提取其中的两个参数: + +![swagger1](https://img.alicdn.com/imgextra/i2/O1CN01qpyb7k1uheVEFq8CI_!!6000000006069-2-tps-1220-1046.png) + +同时,由于在类中定义了每个属性的 example,会自动填入示例值。 + +在 Swagger 中,每个类型都会有一个 `Schema` 来描述,我们已经定义了一个 `CreateCatDto` 的 Schema,看起来就像是下面的样子。 + +注意,我们会重复用到这些 Schema。 + +![swagger2](https://img.alicdn.com/imgextra/i2/O1CN01iZYONb1tAqW35GM3C_!!6000000005862-2-tps-1050-694.png) + + + +### 基础类型 + +通过给 `@ApiProperty(...)` 装饰器中设置 type,我们可以定义常见的类型。 + +大多数情况下,基础类型无需显式声明 `type` ,可自动识别。 + +**字符串** + +```typescript +@ApiProperty({ + type: 'string', + // ... +}) +name: string; +``` + +**布尔类型** + +```typescript +@ApiProperty({ + type: 'boolean', + example: 'true', + // ... +}) +isPure: boolean; +``` + +**数字类型** + +```typescript +@ApiProperty({ + type: 'number', + example: '1', + description: 'The name of the Catage' +}) +age: number; +``` + +此外,也可以使用 format 字段来定义更为精确的长度。 + +```typescript +@ApiProperty({ + type: 'integer', + format: 'int32', + example: '1', + description: 'The name of the Catage' +}) +age: number; +``` + + + +### 数组类型 + +如果是数组类型,我们可以配置 type 字段来定义,同时通过 `items` 的 `type` 来指定类型。 + +```typescript +@ApiProperty({ + type: 'array', + items: { + type: 'string', + }, + example: ['1'], + description: 'The name of the Catage' +}) +breeds: string[]; +``` + +### 枚举类型 + +如果是枚举类型,可以通过配置 enmu 字段来定义。 + +```typescript +enum HelloWorld { + One = 'One', + Two = 'Two', + Three = 'Three', +} + +@ApiProperty({ + enum: ['One', 'Two', 'Three'], + description: 'The name of the Catage' +}) +hello: HelloWorld; +``` + +如果该字段在最顶层,展示效果如下: + +![swagger3](https://img.alicdn.com/imgextra/i1/O1CN015M37MU1KgtdNfqsgp_!!6000000001194-0-tps-1406-426.jpg) + + + +### 复杂对象类型 + +如果某个属性的类型是个现有的复杂类型,我们可以使用 `type` 来指定这个复杂的类型。 + +```typescript +export class Cat { + /** + * The name of the Catcomment + * @example Kitty + */ + @ApiProperty({ example: 'Kitty', description: 'The name of the Cat'}) + name: string; + + @ApiProperty({ example: 1, description: 'The age of the Cat' }) + age: number; + + @ApiProperty({ example: '2022-12-12 11:11:11', description: 'The age of the CatDSate' }) + agedata?: Date; + + @ApiProperty({ + example: 'Maine Coon', + description: 'The breed of the Cat', + }) + breed: string; +} + +export class CreateCatDto { + + // ... + + @ApiProperty({ + type: Cat, // 这里无需指定 example + }) + related: Cat; +} +``` + +效果如下: + +![](https://img.alicdn.com/imgextra/i3/O1CN01KADwTb1rkS4gJExuP_!!6000000005669-2-tps-1376-1070.png) + + + +### 复杂对象数组类型 + +如果某个属性的类型是个复杂的数组类型,写法略有不同。 + +首先`type` 必须声明为 `array`,除了设置`type`,我们还可以使用 `getSchemaPath` 方法额外导入一个不同的类型(上面的复杂对象也可以使用它设置$ref)。 + +此外,如果 `Cat` 类型没有在其他属性的 `type` 字段中声明过,需要使用 `@ApiExtraModel` 装饰器额外声明引入外部类型。 + +```typescript +import { ApiProperty, getSchemaPath, ApiExtraModel } from '@midwayjs/swagger'; + +class Cat { + // ... +} + +@ApiExtraModel(Cat) +export class CreateCatDto { + // ... + + @ApiProperty({ + type: 'array', + items: { + $ref: getSchemaPath(Cat), + } + }) + relatedList: Cat[]; +} + + +``` + +效果如下: + +![](https://img.alicdn.com/imgextra/i1/O1CN01h4sQJ41dP0uq4fgi7_!!6000000003727-2-tps-1332-666.png) + + + +### 循环依赖 + +当类之间具有循环依赖关系时,请使用惰性函数提供类型信息。 + +比如 `type` 字段的循环。 + +```typescript +class Photo { + // ... + @ApiProperty({ + type: () => Album + }) + album: Album; +} +class Album { + // ... + @ApiProperty({ + type: () => Photo + }) + photo: Photo; +} +``` + +`getSchemaPath` 也可以使用。 + +```typescript +export class CreateCatDto { + // ... + + @ApiProperty({ + type: 'array', + items: { + $ref: () => getSchemaPath(Cat) + } + }) + relatedList: Cat[]; +} +``` + + + +## 请求定义 + +[OpenAPI](https://swagger.io/specification/) 定义的 paths 就是各个路由路径,且每个路由路径都有 HTTP 方法的定义,比如 GET、POST、DELETE、PUT 等。 + +### Query 定义 + +使用 `@ApiQuery` 来定义 Query 数据。 + +基础使用,会自动识别 `@Query` 装饰器。 + +```typescript +@Get('/get_user') +async getUser(@Query('name') name: string) { + return 'hello'; +} +``` + +如果 `@Query` 以对象形式,需要在 `@ApiQuery` 指定一个 name 参数,对象类型需要配合 `@ApiProperty` 使用,否则表单会变为只读形式。 + +```typescript +export class UserDTO { + @ApiProperty() + name: string; +} + +@Get('/get_user') +@ApiQuery({ + name: 'query' +}) +async getUser(@Query() dto: UserDTO) { + // ... +} +``` + + + +### Body 定义 + +使用 `@ApiBody` 来定义 Body 数据。 + + `@Body` 对象类型需要配合 `@ApiProperty` 使用。 + +```typescript +export class UserDTO { + @ApiProperty() + name: string; +} + +@Post('/update_user') +async upateUser(@Body() dto: UserDTO) { + // ... +} +``` + +如需其他细节,请使用 `@ApiBody` 增强。 + +注意,Swagger 规定,`Body` 定义只能存在一个,如果配置了 `@ApiBody` ,则类型提取的数据会自动被覆盖。 + +比如下面示例中,`Body` 的类型会被替换为 `Cat`。 + +```typescript +@ApiBody({ + type: Cat +}) +async upateUser(@Body() dto: UserDTO) { + // ... +} +``` + + + +### 文件上传定义 + +文件上传是 Post 请求中较为特殊的一类场景。 + +可以通过在 DTO 中定义属性来实现多个文件以及 `Fields` 的类型。 + +```typescript +import { ApiProperty, BodyContentType } from "@midwayjs/swagger"; + +export class CreateCatDto { + // ... + @ApiProperty({ + type: 'array', + items: { + type: 'string', + format: 'binary', + } + }) + files: any; +} + +// ... + +@Post('/test1') +@ApiBody({ + contentType: BodyContentType.Multipart, + schema: { + type: CreateCatDto, + } +}) +async upload1(@Files() files, @Fields() fields) { + // ... +} +``` + +Swagger UI 中展示: +![swagger6](https://img.alicdn.com/imgextra/i3/O1CN01w9dZxe1YQJv3uOycZ_!!6000000003053-0-tps-1524-1118.jpg) + +如果不需要多个文件,使用 schema 定义即可。 + +```typescript +export class CreateCatDto { + // ... + @ApiProperty({ + type: 'string', + format: 'binary', + }) + file: any; +} +``` + +Swagger UI 中展示: +![swagger4](https://img.alicdn.com/imgextra/i3/O1CN01KlDHNt24mMglN1fyH_!!6000000007433-0-tps-1598-434.jpg) + + + +### 请求 Header + +通过 ```@ApiHeader({...})``` 装饰器来定义 Header 参数。 + +```typescript +@ApiHeader({ + name: 'x-test-one', + description: 'this is test one' +}) +@ApiTags(['hello']) +@Controller('/hello') +export class HelloController {} +``` + +![](https://img.alicdn.com/imgextra/i1/O1CN01n8Xgn729GphI6XzXk_!!6000000008041-2-tps-1234-584.png) + +### 请求 Response + +可以使用 ```@ApiResponse({...})``` 来自定义请求 Response。 + +```typescript +@Get('/:id') +@ApiResponse({ + status: 200, + description: 'The found record', + type: Cat, +}) +findOne(@Param('id') id: string, @Query('test') test: any): Cat { + return this.catsService.findOne(+id); +} +``` + +还提供了其他不需要设置 status 的装饰器: + +* ```@ApiOkResponse()``` +* ```@ApiCreatedResponse()``` +* ```@ApiAcceptedResponse()``` +* ```@ApiNoContentResponse()``` +* ```@ApiMovedPermanentlyResponse()``` +* ```@ApiBadRequestResponse()``` +* ```@ApiUnauthorizedResponse()``` +* ```@ApiNotFoundResponse()``` +* ```@ApiForbiddenResponse()``` +* ```@ApiMethodNotAllowedResponse()``` +* ```@ApiNotAcceptableResponse()``` +* ```@ApiRequestTimeoutResponse()``` +* ```@ApiConflictResponse()``` +* ```@ApiTooManyRequestsResponse()``` +* ```@ApiGoneResponse()``` +* ```@ApiPayloadTooLargeResponse()``` +* ```@ApiUnsupportedMediaTypeResponse()``` +* ```@ApiUnprocessableEntityResponse()``` +* ```@ApiInternalServerErrorResponse()``` +* ```@ApiNotImplementedResponse()``` +* ```@ApiBadGatewayResponse()``` +* ```@ApiServiceUnavailableResponse()``` +* ```@ApiGatewayTimeoutResponse()``` +* ```@ApiDefaultResponse()``` + +HTTP 请求返回的数据模型定义也可以通过指定 type,当然这个数据模型需要通过装饰器 ```@ApiProperty``` 来描述各个字段。 + +```typescript +import { ApiProperty } from '@midwayjs/swagger'; + +export class Cat { + @ApiProperty({ example: 'Kitty', description: 'The name of the Cat'}) + name: string; + + @ApiProperty({ example: 1, description: 'The age of the Cat' }) + age: number; + + @ApiProperty({ + example: 'Maine Coon', + description: 'The breed of the Cat', + }) + breed: string; +} +``` + +Swagger 还支持带前缀 ```x-``` 的扩展字段,可以使用 ```@ApiExtension(x-..., {...})``` 装饰器。 + +```typescript +@ApiExtension('x-hello', { hello: 'world' }) +``` + +当不希望通过 type 来定义 model 类型时,我们可以通过在 Controller 中或者 Model Class 中加入 `@ApiExtraModel` 来增加额外的 `schema` 类型描述。 + +```typescript +@ApiExtraModel(TestExtraModel) +@Controller() +class HelloController { + @Post('/:id', { summary: 'test'}) + @ApiResponse({ + status: 200, + content: { + 'application/json': { + schema: { + properties: { + data: { '$ref': getSchemaPath(TestExtraModel)} + } + } + } + } + }) + async create(@Body() createCatDto: CreateCatDto, @Param('id') id: number): Promise { + return this.catsService.create(createCatDto); + } +} + +// or +@ApiExtraModel(TestExtraModel) +class TestModel { + @ApiProperty({ + item: { + $ref: getSchemaPath(TestExtraModel) + }, + description: 'The name of the Catage' + }) + one: TestExtraModel; +} +``` + +### 泛型返回数据 + +Swagger 本身不支持泛型数据,泛型作为 Typescript 的一种类型,会在构建期抹掉,在运行时无法读取。 + +我们可以用一些取巧的方式来定义。 + +比如,我们需要将返回值增加一些通用的包裹结构。 + +```typescript +{ + code: 200, + message: 'xxx', + data: any +} +``` + +为此,我们可以编写一个方法,入参是返回的 data,返回一个包裹的类。 + +```typescript +import { Type } from '@midwayjs/swagger'; + +type Res = { + code: number; + message: string; + data: T; +} + +export function SuccessWrapper(ResourceCls: Type): Type> { + class Successed { + @ApiProperty({ description: '状态码' }) + code: number; + + @ApiProperty({ description: '消息' }) + message: string; + + @ApiProperty({ + type: ResourceCls, + }) + data: T; + } + + return Successed; +} +``` + +我们可以基于这个方法,来实现我们自己的返回类。 + +```typescript +class ViewCat extends SuccessWrapper(Cat) {} +``` + +在使用的时候,可以直接指定这个类即可。 + +```typescript +@Get('/:id') +@ApiResponse({ + status: 200, + description: 'The found record', + type: ViewCat, +}) +async findOne(@Param('id') id: string, @Query('test') test: any): ViewCat { + // ... +} +``` + + + +## 更多的定义示例 + +Swagger 中还有更多的写法,框架都进行了支持,更多用法可以查看我们的 [测试用例](https://github.com/midwayjs/midway/blob/main/packages/swagger/test/parser.test.ts)。 + + + +## 更多配置 + +### 路由标签 +Swagger 可以对每个路由添加标签,进行分组。 + +标签添加有两种形式。 + + + +默认情况下,框架会根据 Controller 的路径来生成标签,比如下面的代码,会生成一个 `hello` 的标签,这个标签会应用到这个控制器所有的路由上。 + +```typescript +@Controller('/hello') +export class HelloController {} +``` + +如果需要自定义标签,可以通过 ```@ApiTags([...])``` 来自定义 Controller 标签。 + +```typescript +@ApiTags(['hello']) +@Controller('/hello') +export class HelloController {} +``` + +从 `v3.17.3` 开始,可以通过配置 `isGenerateTagForController` 来控制是否自动生成 Controller 标签。 + +```typescript +// src/config/config.default.ts +export default { + swagger: { + isGenerateTagForController: false + } +} +``` + + + + + +可以将 `@ApiTags` 标签直接加在路由方法上。 + +```typescript +// ... +export class HomeController { + @ApiTags(['bbb']) + @Get('/') + async home(): Promise { + // ... + } +} +``` + +也可以通过 `@ApiOperation` 来添加标签。 + +```typescript +// ... +export class HomeController { + @ApiOperation({ tags: ['bbb'] }) + @Get('/') + async home(): Promise { + // ... + } +} +``` + +`@ApiTags` 的优先级比 `@ApiOperation` 更高,如果两者同时存在,`@ApiTags` 会覆盖 `@ApiOperation`。 + +同理,路由上的 `@ApiTags` 也会覆盖控制器上的 `@ApiTags`。 + + + + + + +可以通过配置给 Tag 添加描述。 + +```typescript +// src/config/config.default.ts + +export default { + swagger: { + tags: [ + { + name: 'api', + description: 'API Document' + }, + { + name: 'hello', + description: 'Other Router' + }, + ] + } +} + +``` + + +### 授权验证 + +组件可以通过添加授权验证配置来设置验证方式,我们支持配置 ```basic```、```bearer```、```cookie```、```oauth2```、```apikey```、```custom```。 + + + +#### basic + +启用 basic 验证 + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'basic', + }, + }, +} +``` + +关联 Controller + +```typescript +@ApiBasicAuth() +@Controller('/hello') +export class HelloController {} +``` + +#### bearer + +启用 bearer 验证(bearerFormat 为 JWT) + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'bearer', + }, + }, +} +``` + +关联 Controller + +```typescript +@ApiBearerAuth() +@Controller('/hello') +export class HelloController {} +``` + +#### oauth2 + +启用 oauth2 验证 + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'oauth2', + flows: { + implicit: { + authorizationUrl: 'http://example.org/api/oauth/dialog', + scopes: { + 'write:pets': 'modify pets in your account', + 'read:pets': 'read your pets' + } + }, + authorizationCode: { + authorizationUrl: 'https://example.com/api/oauth/dialog', + tokenUrl: 'https://example.com/api/oauth/token', + scopes: { + 'write:pets': 'modify pets in your account', + 'read:pets': 'read your pets' + } + }, + }, + }, + }, +} +``` + +关联 Controller + +```typescript +@ApiOAuth2() +@Controller('/hello') +export class HelloController {} +``` + +#### cookie +启用 cookie 验证 + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'cookie', + securityName: 'testforcookie', + cookieName: 'connect.sid', + }, + }, +} +``` + +关联 Controller + +```typescript +@ApiCookieAuth('testforcookie') +@Controller('/hello') +export class HelloController {} +``` + +#### apikey + +启用 cookie 验证 + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'apikey', + name: 'api_key' + }, + }, +} +``` + +关联 Controller + +```typescript +@ApiSecurity('api_key') +@Controller('/hello') +export class HelloController {} +``` + +#### custom 验证 + +自定义验证方式,需要自己设计参数配置 + +```typescript +// src/config/config.default.ts +export default { + // ... + swagger: { + auth: { + authType: 'custom', + name: 'mycustom' + // ... + }, + }, +} +``` + +关联 Controller + +```typescript +@ApiSecurity('mycustom') +@Controller('/hello') +export class HelloController {} +``` + + + + + +### 忽略路由 + +配置 `@ApiExcludeController` 可以忽略整个 Controller 的路由。 + +```typescript +@ApiExcludeController() +@Controller('/hello') +export class HelloController {} +``` + +配置 `@ApiExcludeEndpoint` 可以忽略单个路由。 + +```typescript +@Controller('/hello') +export class HelloController { + + @ApiExcludeEndpoint() + @Get() + async getUser() { + // ... + } +} +``` + +如果需要满足更加动态的场景,可以通过配置路由过滤器来批量过滤。 + +```typescript +// src/config/config.default.ts +import { RouterOption } from '@midwayjs/core'; + +export default { + // ... + swagger: { + routerFilter: (url: string, options: RouterOption) => { + return url === '/hello/getUser'; + } + }, +} +``` + +`routerFilter` 用来传入一个过滤函数,包含 `url` 和 `routerOptions` 两个参数。`routerOptions` 中包含了路由基础信息。 + +每当匹配到一个路由时,会自动执行 `routerFilter` 方法,当 `routerFilter` 返回 true 时,代表这个路由将会被过滤。 + + + +### 完整参数配置 + +Swagger 组件提供了和 [OpenAPI](https://swagger.io/specification/) 一致的参数配置能力,可以通过自定义配置来实现。 + +配置项如下: + +```typescript +/** + * see https://swagger.io/specification/ + */ +export interface SwaggerOptions { + /** + * 默认值: My Project + * https://swagger.io/specification/#info-object title 字段 + */ + title?: string; + /** + * 默认值: This is a swagger-ui for midwayjs project + * https://swagger.io/specification/#info-object description 字段 + */ + description?: string; + /** + * 默认值: 1.0.0 + * https://swagger.io/specification/#info-object version 字段 + */ + version?: string; + /** + * https://swagger.io/specification/#info-object contact 字段 + */ + contact?: ContactObject; + /** + * https://swagger.io/specification/#info-object license 字段 + */ + license?: LicenseObject; + /** + * https://swagger.io/specification/#info-object termsOfService 字段 + */ + termsOfService?: string; + /** + * https://swagger.io/specification/#openapi-object externalDocs 字段 + */ + externalDocs?: ExternalDocumentationObject; + /** + * https://swagger.io/specification/#openapi-object servers 字段 + */ + servers?: Array; + /** + * https://swagger.io/specification/#openapi-object tags 字段 + */ + tags?: Array; + /** + * 可以参考 https://swagger.io/specification/#security-scheme-object + */ + auth?: AuthOptions | AuthOptions[]; + /** + * 默认值: /swagger-ui + * 访问 swagger ui 的路径 + */ + swaggerPath?: string; + /** + * 对路由 tag 进行 ascii 排序 + * 可以使用 1-xxx、2-xxx、3-xxx 来定义 tag + */ + tagSortable?: boolean; + /** + * UI 展示中需要用到的配置 + * 可以参考 https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md#display + */ + displayOptions?: { + deepLinking?: boolean; + displayOperationId?: boolean; + defaultModelsExpandDepth?: number; + defaultModelExpandDepth?: number; + defaultModelRendering?: 'example' | 'model'; + displayRequestDuration?: boolean; + docExpansion?: 'list' | 'full' | 'none'; + filter?: boolean | string; + maxDisplayedTags?: number; + showExtensions?: boolean; + showCommonExtensions?: boolean; + useUnsafeMarkdown?: boolean; + tryItOutEnabled?: boolean; + }; + + documentOptions?: { + /** + * 自定义 operationIdFactory,用于生成 operationId + * @default () => controllerKey_webRouter.methodKey + */ + operationIdFactory?: ( + controllerKey: string, + webRouter: RouterOption + ) => string; + }; +} + +/** + * 继承自 https://swagger.io/specification/#security-scheme-object + */ +export interface AuthOptions extends Omit { + /** + * 验权类型 + * basic => http basic 验证 + * bearer => http jwt 验证 + * cookie => cookie 方式验证 + * oauth2 => 使用 oauth2 + * apikey => apiKey + * custom => 自定义方式 + */ + authType: AuthType; + /** + * https://swagger.io/specification/#security-scheme-object type 字段 + */ + type?: SecuritySchemeType; + /** + * authType = cookie 时可以修改,通过 ApiCookie 装饰器关联的名称 + */ + securityName?: string; + /** + * authType = cookie 时可以修改,cookie 的名称 + */ + cookieName?: string; +} +``` + + + +## 装饰器列表 + +组件所有装饰器参考了 [@nestjs/swagger](https://github.com/nestjs/swagger) 的设计,都带 ```Api``` 前缀。这里列出全部装饰器: + +| 装饰器 | 支持模式 | +| --------------------------- | ----------------- | +| ```@ApiBody``` | Method | +| ```@ApiExcludeEndpoint``` | Method | +| ```@ApiExcludeController``` | Controller | +| ```@ApiHeader``` | Controller/Method | +| ```@ApiHeaders``` | Controller/Method | +| ```@ApiOperation``` | Method | +| ```@ApiProperty``` | Model Property | +| ```@ApiPropertyOptional``` | Model Property | +| ```@ApiResponseProperty``` | Model Property | +| ```@ApiQuery``` | Method | +| ```@ApiResponse``` | Method | +| ```@ApiTags``` | Controller/Method | +| ```@ApiExtension``` | Method | +| ```@ApiBasicAuth``` | Controller | +| ```@ApiBearerAuth``` | Controller | +| ```@ApiCookieAuth``` | Controller | +| ```@ApiOAuth2``` | Controller | +| ```@ApiSecurity``` | Controller | +| ```@ApiExcludeSecurity``` | Method | +| ```@ApiParam``` | Method | +| ```@ApiExtraModel``` | Controller | + + + +## UI 渲染 + +### 从 Swagger-ui-dist 渲染 + +默认情况下,如果安装了 `swagger-ui-dist` 包,组件会默认会调用 `renderSwaggerUIDist` 渲染 swagger ui,如果需要传递 swagger-ui 的 options,可以 通过 `swaggerUIRenderOptions` 选项。 + +```typescript +// src/config/config.default.ts +import { renderSwaggerUIDist } from '@midwayjs/swagger'; + +export default { + // ... + swagger: { + swaggerUIRender: renderSwaggerUIDist, + swaggerUIRenderOptions: { + // ... + } + }, +} +``` + +如果希望调整 UI 的配置,可以使用自定义文件的方式替换默认的 `swagger-initializer.js`。 + +```typescript +// src/config/config.default.ts +import { AppInfo } from '@midwayjs/core'; +import { renderSwaggerUIDist } from '@midwayjs/swagger'; +import { join } from 'path'; + +export default (appInfo: AppInfo) { + return { + // ... + swagger: { + swaggerUIRender: renderSwaggerUIDist, + swaggerUIRenderOptions: { + customInitializer: join(appInfo.appDir, 'resource/swagger-initializer.js'), + } + }, + } +} +``` + +自定义的 `swagger-initializer.js` 内容大致如下: + +```javascript +window.onload = function() { + window.ui = SwaggerUIBundle({ + url: "/index.json", + dom_id: '#swagger-ui', + deepLinking: true, + presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIStandalonePreset + ], + plugins: [ + SwaggerUIBundle.plugins.DownloadUrl + ], + layout: "StandaloneLayout", + persistAuthorization: true, + }); +}; + +``` + +其中的 url 指向当前的 swagger json,可以自行修改,完整的 `swagger-ui` 配置请参考 [这里](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md)。 + +### 从 unpkg 等 CDN 地址渲染 + +如果未安装 `swagger-ui-dist` 包,会自动使用 `renderSwaggerUIRemote` 方法进行渲染,默认由 `unpkg.com` 提供 cdn 资源。 + +```typescript +// src/config/config.default.ts +import { renderSwaggerUIRemote } from '@midwayjs/swagger'; + +export default { + // ... + swagger: { + swaggerUIRender: renderSwaggerUIRemote, + swaggerUIRenderOptions: { + // ... + } + }, +} +``` + + + +### 仅提供 Swagger JSON + +如果仅希望提供 Swagger JSON,可以配置 `renderJSON` 仅渲染 JSON ,无需引入 `swagger-ui-dist` 包。 + +```typescript +// src/config/config.default.ts +import { renderJSON } from '@midwayjs/swagger'; + +export default { + // ... + swagger: { + swaggerUIRender: renderJSON, + }, +} +``` + + + +## 常见问题 + +### `@Get` 等路由注解中的 `summary` 或者 `description` 不生效 + +当存在 `@ApiOperation` 时候,将优先使用 `@ApiOperation` 中的 `summary` 或者 `description`,所以在 `@ApiOperation` 与 `@Get` 等路由注解中,只需要写一个即可。 diff --git a/site/versioned_docs/version-3.0.0/extensions/tablestore.md b/site/versioned_docs/version-3.0.0/extensions/tablestore.md new file mode 100644 index 000000000000..d866b671365b --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/tablestore.md @@ -0,0 +1,186 @@ +# TableStore + +本文介绍了如何使用 midway 接入阿里云 TableStore。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + +## 安装依赖 + +```bash +$ npm i @midwayjs/tablestore@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/tablestore": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## 引入组件 + + +首先,引入组件,在 `configuration.ts` 中导入: +```typescript +import { Configuration } from '@midwayjs/core'; +import * as tablestore from '@midwayjs/tablestore'; +import { join } from 'path' + +@Configuration({ + imports: [ + tablestore // 导入 tablestore 组件 + ], + importConfigs: [ + join(__dirname, 'config') + ] +}) +export class MainConfiguration { +} +``` + + +## 配置 + + +比如: + +**单客户端配置** +```typescript +// src/config/config.default +export default { + // ... + tableStore: { + client: { + accessKeyId: '', + secretAccessKey: '', + stsToken: '', /*When you use the STS authorization, you need to fill in. ref:https://help.aliyun.com/document_detail/27364.html*/ + endpoint: '', + instancename: '' + }, + }, +} +``` + + +**多个客户端配置,需要配置多个** + +```typescript +// src/config/config.default +export default { + // ... + tableStore: { + clients: { + db1: { + accessKeyId: '', + secretAccessKey: '', + stsToken: '', /*When you use the STS authorization, you need to fill in. ref:https://help.aliyun.com/document_detail/27364.html*/ + endpoint: '', + instancename: '' + }, + db2: { + accessKeyId: '', + secretAccessKey: '', + stsToken: '', /*When you use the STS authorization, you need to fill in. ref:https://help.aliyun.com/document_detail/27364.html*/ + endpoint: '', + instancename: '' + }, + }, + }, +} +``` +更多参数可以查看 [aliyun tablestore sdk](https://github.com/aliyun/aliyun-tablestore-nodejs-sdk) 文档。 + + +## 使用 TableStore 服务 + + +我们可以在任意的代码中注入使用。 +```typescript +import { Provide, Controller, Inject, Get } from '@midwayjs/core'; +import { TableStoreService } from '@midwayjs/tablestore'; + +@Provide() +export class UserService { + + @Inject() + tableStoreService: TableStoreService; + + async invoke() { + await this.tableStoreService.putRow(params); + } +} +``` + + +可以使用 `TableStoreServiceFactory` 获取不同的实例。 +```typescript +import { TableStoreServiceFactory } from '@midwayjs/tablestore'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + tableStoreServiceFactory: TableStoreServiceFactory; + + async save() { + const db1 = await this.tableStoreServiceFactory.get('db1'); + const db2 = await this.tableStoreServiceFactory.get('db2'); + + //... + + } +} +``` + + +示例:getRow +```typescript +import { join } from 'path'; +import { + TableStoreService, + Long, + CompositeCondition, + SingleColumnCondition, + LogicalOperator, + ComparatorType +} from '@midwayjs/tablestore'; + +@Provide() +export class UserService { + + @Inject() + tableStoreService: TableStoreService; + + async getInfo() { + + const data = await tableStoreService.getRow({ + tableName: "sampleTable", + primaryKey: [{ 'gid': Long.fromNumber(20013) }, { 'uid': Long.fromNumber(20013) }], + columnFilter: condition + }); + + // TODO + + } +} +``` +如示例所示,原有的 tablestore 包中导出的类型,应该都已经被 @midwayjs/tablestore 代理和接管,更多具体方法参数可以查看 [示例](https://github.com/midwayjs/midway/tree/2.x/packages/tablestore/test/sample)。 diff --git a/site/versioned_docs/version-3.0.0/extensions/tags.md b/site/versioned_docs/version-3.0.0/extensions/tags.md new file mode 100644 index 000000000000..9e6548c64fde --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/tags.md @@ -0,0 +1,424 @@ +# 标签组件 + +适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用标签组件。 + +### 使用场景 +标签是一种抽象化的服务端常用系统化能力,可用于多种用途,如: ++ 组织管理资源 + - 实现分类系统(面向内容、人群等) + - 资源管理系统 + + 图片添加各种颜色标签、物体和场景标签,通过标签筛选图片 + + 视频等素材标签 ++ 访问控制 + - 权限系统(管理员、编辑、游客) ++ 状态系统(编辑中、已发布等) + +基于标签系统提供的增删改查,以及通过标签,对绑定了标签的 `实体` 进行增删改查,能够很方便的实现更多高级的业务逻辑。 + +标签系统就是为了这种业务场景,让服务端基于标签能力,实现更高效、便捷的业务开发。 + +相关信息: + +| web 支持情况 | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | ✅ | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + + +### 如何使用? + +1. 安装依赖 + +```bash +$ npm i @midwayjs/tags --save +``` + +2. 在 configuration 中引入组件 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as tags from '@midwayjs/tags'; +@Configuration({ + imports: [ + // ... + tags + ], +}) +export class MainConfiguration {} +``` + +3. 添加配置 + +```typescript +// src/config/config.local.ts +export default { + tags: { + clients: { + 'tagGroup1': { + // 使用 本机内存 作为数据存储 + dialectType: 'memory', + }, + }, + } +} +``` + +4. 在代码中调用 +```typescript +// src/testTags.ts +import { Provide, Inject, InjectClient } from '@midwayjs/core'; +import { TagServiceFactory, TagClient } from '@midwayjs/tags'; +@Provide() +export class TestTagsService { + @Inject() + tags: TagServiceFactory; + + // 相当于 this.tags.get('tagGroup1') + @InjectClient(TagServiceFactory, 'tagGroup1') + tagClient: TagClient; + + @ServerlessTrigger(ServerlessTriggerType.HTTP, { path: '/tags/list', method: 'get'}) + async listTags() { + // 也可以直接使用 this.tagClient + const tagClient: TagClient = this.tags.get('tagGroup1'); + // add new tag + const tagInfo = await tagClient.new({ + name: 'test-tag-name', + desc: 'tag desc', + }); + /* + tagInfo = { + success: true, + id: 1, + } + */ + // list top 20 tags + const tags = await tagClient.list({ count: true }); + /* + tags: { + list: [ + { + id: 1, + name: 'test-tag-name', + desc: 'tag desc' + } + ], + total: 1 + } + */ + return tags; + } +} + +``` + +### 方法 + +#### 新增标签 new + +```typescript +new(tagDefine: { + // 标签名,在同一个 group 里面不能重复 + name: string; + // 标签描述 + desc?: string; +}): Promise<{ + success: boolean; + message: string; + // 标签id + id?: number; +}>; +``` +#### 删除标签 remove +删除标签也会删除和这个标签绑定的实体关系 + +```typescript +remove(tagIdOrName: number | string): Promise<{ + success: boolean; + message: string; + // 标签id + id?: number; +}>; +``` +#### 更新标签 update +更细一个标签的基础信息 +```typescript +update(tagIdOrName: number | string, params: Partial< { + name: string; + desc?: string; +}>): Promise<{ + success: boolean; + message: string; + // 标签id + id?: number; +}>; +``` +#### 列举标签 list +搜索标签,支持分页 + +```typescript +list(listOptions?: { + // 搜索的标签,支持传入标签 id 和标签名 + tags?: Array; + // 检索的时候标签是采用交集还是并集,取值为 and 和 or + type?: MATCH_TYPE; + count?: boolean; + pageSize?: number; + page?: number; +}): Promise<{ + // 标签列表 + list: { + id: number; + name: string; + desc: string; + createAt: number; + updateAt: number; + }[]; + // 标签总数 + total?: number; +}>; +``` +#### 绑定实体 bind +绑定实体的意思就是将其他的任何东西绑定到一个标签上,这里的实体可以是一张图片、也可以是一个文件,实体的id由用户自己控制 + +```typescript +bind(bindOptions: { + // 标签列表 + tags: Array; + // 不存在标签的话自动创建标签,并绑定,默认为false + autoCreateTag?: boolean; + // 实体id + objectId: number, +}): Promise<{ + success: boolean; + message: string; +}> +``` +#### 解绑实体 unbind + +```typescript +unbind(unbindOptions: { + // 解绑的多个标签,标签id或者是标签 name + tags: Array, + // 实体id + objectId: number, +}): Promise<{ + success: boolean; + message: string; +}> +``` +#### 根据标签列举实体 listObjects + +```typescript +listObjects(listOptions?: { + // 标签id或者是标签 name + tags?: Array; + count?: boolean; + // 检索的时候标签是采用交集还是并集,取值为 and 和 or + type?: MATCH_TYPE; + pageSize?: number; + page?: number; +}): Promise<{ + // 实体的 id 列表 + list: number[]; + // 实体总数 + total?: number; +}>; +``` +#### 根据实体获取标签 listObjectTags + + +```typescript +listObjectTags(listOptions?: { + // 实体id + objectId: number; + count?: boolean; + pageSize?: number; + page?: number; + +}): Promise<{ + list: { // 标签列表 + name: string; + desc?: string; + id: number; + createAt: number; + updateAt: number; + }[]; + // 标签总数 + total?: number; +}>; +``` +### 配置 + +Tags 支持内存存储(默认)和 mysql 数据库存储两种方式,下面是一个配置的示例: +```typescript +// src/config/config.local.ts +export default { + tags: { + clients: { + 'tagGroup1': { + // 使用 本机内存 作为数据存储 + dialectType: 'memory', + }, + 'tagGroup2': { + // 使用 mysql 作为数据存储 + dialectType: 'mysql', + // 自动同步表结构 + sync: true, + // mysql 连接实例 + instance: mysqlConnection.promise(), + }, + }, + } +} +``` + +#### 内存存储配置 + +| 配置 | 值类型 | 默认值 | 配置描述 | +| -- | -- | -- | -- | +| dialectType | string `memory` | - | 配置为 `memory`,则启用内存存储 | + +#### Mysql 存储配置 + +如果要使用 Mysql 数据库作为数据存储,那么需要将 Mysql 的 `数据库连接对象` 传入 tags 的配置中。 + + +| 配置 | 值类型 | 默认值 | 配置描述 | +| -- | -- | -- | -- | +| dialectType | string `mysql` | - | 配置为 `mysql`,则启用 Mysql 存储 | +| sync | boolean | `false` | 自动同步 Tags 的表结构,Tags组件会创建两张数据表,详见下方的数据表信息 | +| instance | `{ query: (sql: string, placeholder?: any[])}: Promise<[]>` | - | Mysql 连接的示例,需要提供一个 query 方法,可以查看下面的示例 | +| tablePrefix | string | - | 数据表前缀 | +| tableSeparator | string | `_` | 数据表的拼接分隔符 | + +下面是使用 `mysql2` 这个 npm 包进行数据库连接的示例: + +```typescript +// src/config/config.local.ts +const mysql = require('mysql2'); +export default () => { + const connection = mysql.createConnection({ + host: 'db4free.net', + user: 'tag***', + password: 'tag***', + database: 'tag***', + charset: 'utf8', + }); + return { + tags: { + clients: { + 'tagGroup': { + dialectType: 'mysql', + sync: true, + instance: { // 包含 query 的mysql连接实例 + query: (...args) => { + return connection.promise().query(...args); + } + }, + }, + }, + } + } +} +``` + +你也可以考虑在 `configuration.ts` 中的 `onConfigLoad` 生命周期中进行数据库连接,这样的好处是在关闭时,可以关闭数据库连接: + +```typescript +// src/configuration.ts +import { Config, Configuration } from '@midwayjs/core'; +import { join } from 'path'; +import * as tags from '@midwayjs/tags'; +import { ITagMysqlDialectOption } from '@midwayjs/tags'; +const mysql = require('mysql2'); + +@Configuration({ + imports: [ + tags + ], +}) +export class MainConfiguration { + connection; + + @Config() + tags; + + async onConfigLoad(container) { + // 创建 mysql 连接 + this.connection = mysql.createConnection({ + host: 'db4free.net', + user: 'tag***', + password: 'tag***', + database: 'tag***', + charset: 'utf8', + }); + let dialect: ITagMysqlDialectOption = { + dialectType: 'mysql', + sync: true, + instance: { + query: (...args) => { + return this.connection.promise().query(...args); + } + } + }; + + return { + tags: dialect + } + } + + async onStop() { + // 关闭 mysql 连接 + this.connection.close(); + } +} + +``` + + +##### 数据表信息 + +Tags 组件需要两种数据表来存储数据,分别是 `tag` 和 `relationship`,这两张表在数据库中真实的表名,是通过配置中的 `表名前缀`、`表名分隔符` 和 `客户端名/分组名` 进行拼接的,例如: + + +```typescript +const clientName = 'local-test'; +const { tablePrefix = 'a', tableSeparator = '_' } = tagOptions; +const tagTableName = `${tablePrefix}${tableSeparator}${clientName}${tableSeparator}tag`; +// tagTableName: a_local-test_tag +const relationshipTableName = `${tablePrefix}${tableSeparator}${clientName}${tableSeparator}relationship` +// relationshipTableName: a_local-test-relationship +``` + + +当你在配置中启用 `sync` 的自动表结构同步时,如果没有这两张表,就会根据下述的表结构创建对应的数据表: + +`tag` 表结构: +```sql +CREATE TABLE `tag` ( + `id` BIGINT unsigned NOT NULL AUTO_INCREMENT, + `group` varchar(32) NULL, + `name` varchar(32) NULL, + `descri` varchar(128) NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + `update_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY (id) +) +``` + + +`relationship` 表结构: +```sql +CREATE TABLE `relationship` ( + `id` BIGINT unsigned NOT NULL AUTO_INCREMENT, + `tid` BIGINT unsigned NOT NULL, + `oid` BIGINT unsigned NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + `update_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY (id) +) +``` diff --git a/site/versioned_docs/version-3.0.0/extensions/tenant.md b/site/versioned_docs/version-3.0.0/extensions/tenant.md new file mode 100644 index 000000000000..09bd0e9b47cc --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/tenant.md @@ -0,0 +1,142 @@ +# 租户 + +这里介绍如何快速在 Midway 中使用租户组件。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 租户定义 + +租户管理是中后台业务开发过程中经常需要的功能。 + +在开发中,不同的用户需要保存在不同的数据源、命名空间或是区域中,这些不同的数据区域我们统称为 “租户”。 + + + +## 安装依赖 + +`@midwayjs/tenant` 是主要的功能包。 + +```bash +$ npm i @midwayjs/tenant@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/tenant": "^3.0.0", + // ... + } +} +``` + + + + +## 引入组件 + + +首先,引入 组件,在 `src/configuration.ts` 中导入: + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as tenant from '@midwayjs/tenant'; + +@Configuration({ + imports: [ + // ... + tenant, + ], +}) +export class MainConfiguration { +} +``` + + + +## 租户信息存取 + +不同的租户数据相互隔离,一般来说,每个用户数据都会关联相关的租户信息,在用户认证拿到用户信息之后,获取其对应的租户数据,以便后续数据读写使用。 + +在 Midway 中,可以将租户数据保存在请求对象 ctx 中,后续的所有请求作用域对象可以使用。但是租户信息仅仅在请求链路中使用是不够的,需要在不同的作用域都生效,这就需要新的架构来支持。 + +组件提供了一个 `TenantManager` 来管理租户信息。 + +你需要在每个请求链路中保存租户信息,之后才能获取。 + +租户信息的格式可以按需求定义。 + +比如: + +```typescript +interface TenantInfo { + id: string; + name: string; +} +``` + +比如,在中间件中保存。 + +```typescript +import { TenantManager } from '@midwayjs/tenant'; +import { Middleware, Inject } from '@midwayjs/core'; + +@Middleware() +class TenantMiddleware { + @Inject() + tenantManager: TenantManager; + + resolve() { + return async(ctx, next) => { + // 请求链路中设置租户信息 + await this.tenantManager.setCurrentTenant({ + id: '123', + name: '我的租户' + }); + } + } +} +``` + +在后续的单例服务中获取。 + +```typescript +import { TenantManager } from '@midwayjs/tenant'; +import { Inject, Singleton } from '@midwayjs/core'; +import { TenantInfo } from '../interface'; + +@Singleton() +class TenantService { + @Inject() + tenantManager: TenantManager; + + async getTenantInfo() { + const tenantInfo = await this.tenantManager.getCurrentTenant(); + if (tenantInfo) { + console.log(tenantInfo.name); + // output => 我的租户 + } + } +} +``` + + + +:::tip + +* 1、租户信息一定会关联请求,如有需求,你可以在不同的 Framework 中都加入中间件 +* 2、每个请求保存的租户信息是隔离的 +* 3、不管是单例还是请求作用域,你都仅能获取到当前请求对应的租户数据 + +::: diff --git a/site/versioned_docs/version-3.0.0/extensions/upload.md b/site/versioned_docs/version-3.0.0/extensions/upload.md new file mode 100644 index 000000000000..163a72dc84cd --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/upload.md @@ -0,0 +1,450 @@ +# 文件上传 + +适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用上传组件,支持 `file` (服务器临时文件) 和 `stream` (流)两种模式。 + +相关信息: + +| web 支持情况 | | +| ----------------- | ---- | +| @midwayjs/koa | ✅ | +| @midwayjs/faas | 💬 | +| @midwayjs/web | ✅ | +| @midwayjs/express | ✅ | + +:::caution + +💬 部分函数计算平台不支持流式请求响应,请参考对应平台能力。 + +::: + + + +## 安装依赖 + +```bash +$ npm i @midwayjs/upload@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/upload": "^3.0.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## 启用组件 + +```typescript +import { Configuration } from '@midwayjs/core'; +import * as upload from '@midwayjs/upload'; + +@Configuration({ + imports: [ + // ...other components + upload + ], + // ... +}) +export class MainConfiguration {} +``` + +3、在代码中获取上传的文件 + +```typescript +import { Controller, Inject, Post, Files, Fields } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx; + + @Post('/upload') + async upload(@Files() files, @Fields() fields) { + /* + files = [ + { + filename: 'test.pdf', // 文件原名 + data: '/var/tmp/xxx.pdf', // mode 为 file 时为服务器临时文件地址 + fieldname: 'test1', // 表单 field 名 + mimeType: 'application/pdf', // mime + }, + { + filename: 'test.pdf', // 文件原名 + data: ReadStream, // mode 为 stream 时为服务器临时文件地址 + fieldname: 'test2', // 表单 field 名 + mimeType: 'application/pdf', // mime + }, + // ...file 下支持同时上传多个文件 + ] + + */ + return { + files, + fields + } + } +} +``` + +:::caution + +如果同时开启了 swagger 组件,请务必添加上传参数的类型(装饰器对应的类型,以及 @ApiBody 中的 type),否则会报错,更多请参考 swagger 的文件上传章节。 + +::: + +## 配置 + +### 默认配置 + +默认配置如下,一般情况下无需修改。 + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/upload'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + upload: { + // mode: UploadMode, 默认为file,即上传到服务器临时目录,可以配置为 stream + mode: 'file', + // fileSize: string, 最大上传文件大小,默认为 10mb + fileSize: '10mb', + // whitelist: string[],文件扩展名白名单 + whitelist: uploadWhiteList.filter(ext => ext !== '.pdf'), + // tmpdir: string,上传的文件临时存储路径 + tmpdir: join(tmpdir(), 'midway-upload-files'), + // cleanTimeout: number,上传的文件在临时目录中多久之后自动删除,默认为 5 分钟 + cleanTimeout: 5 * 60 * 1000, + // base64: boolean,设置原始body是否是base64格式,默认为false,一般用于腾讯云的兼容 + base64: false, + // 仅在匹配路径到 /api/upload 的时候去解析 body 中的文件信息 + match: /\/api\/upload/, + }, +} + +``` + + + +### 上传模式 - file + +`file` 为默认值,也是框架的推荐值。 + +配置 upload 的 mode 为 `file` 字符串,或使用 `@midwayjs/upload` 包导出的 `UploadMode.File` 来配置。 + +使用 file 模式时,通过 `this.ctx.files` 中获取的 `data` 为上传的文件在服务器的 `临时文件地址`,后续可以再通过 `fs.createReadStream` 等方式来获取到此文件内容。 + +使用 file 模式时,支持同时上传多个文件,多个文件会以数组的形式存放在 `this.ctx.files` 中。 + + + +:::caution + +当采取 `file` 模式时,由于上传组件会在接收到请求时,会根据请求的 `method` 和 `headers` 中的部分标志性内容进行匹配,如果认为是一个文件上传请求,就会对请求进行解析,将其中的文件 `写入` 到服务器的临时缓存目录,您可以通过本组件的 `match` 或 `ignore` 配置来设置允许解析文件的路径。 + +配置 `match` 或 `ignore`后,则可以保证您的普通 post 等请求接口,不会被用户非法用作上传,可以 `避免` 服务器缓存被充满的风险。 + +您可以查看下面的 `配置 允许(match) 或 忽略(ignore)的上传路径` 章节,来进行配置。 + +::: + + + + +### 上传模式 - stream + +配置 upload 的 mode 为 `stream` 字符串,或使用 `@midwayjs/upload` 包导出的 `UploadMode.Stream` 来配置。 + + +使用 stream 模式时,通过 `this.ctx.files` 中获取的 `data` 为 `ReadStream`,后续可以再通过 `pipe` 等方式继续将数据流转至其他 `WriteStream` 或 `TransformStream`。 + + +使用 stream 模式时,仅同时上传一个文件,即 `this.ctx.files` 数组中只有一个文件数据对象。 + +另外,stream 模式 `不会` 在服务器上产生临时文件,所以获取到上传的内容后无需手动清理临时文件缓存。 + +:::tip + +faas 场景实现方式视平台而定,如果平台不支持流式请求/响应但是业务开启了 `mode: 'stream'`,将采用先读取到内存,再模拟流式传输来降级处理。 + +::: + + + +### 上传白名单 + +通过 `whitelist` 属性,配置允许上传的文件后缀名,配置 `null` 则不校验后缀名。 + +:::caution + +如果配置为 `null`,则不对上传文件后缀名进行校验,如果采取文件上传模式 (mode=file),则会有可能被攻击者所利用,上传 `.php`、`.asp` 等后缀的 WebShell 实现攻击行为。 + +当然,由于 `@midwayjs/upload` 组件会对上传后的临时文件采取 `重新随机生成` 文件名写入,只要开发者 `不将` 上传后的临时文件地址返回给用户,那么即使用户上传了一些不被预期的文件,那也无需过多担心会被利用。 + +::: + + +如果上传的文件后缀不匹配,会响应 `400` error,默认值如下: +```ts +'.jpg', +'.jpeg', +'.png', +'.gif', +'.bmp', +'.wbmp', +'.webp', +'.tif', +'.psd', +'.svg', +'.js', +'.jsx', +'.json', +'.css', +'.less', +'.html', +'.htm', +'.xml', +'.pdf', +'.zip', +'.gz', +'.tgz', +'.gzip', +'.mp3', +'.mp4', +'.avi', +``` + +可以通过 `@midwayjs/upload` 包中导出的 `uploadWhiteList` 获取到默认的后缀名白名单。 + +另外,midway 上传组件,为了避免部分 `恶意用户`,通过某些技术手段来`伪造`一些可以被截断的扩展名,所以会对获取到的扩展名的二进制数据进行过滤,仅支持 `0x2e`(即英文点 `.`)、`0x30-0x39`(即数字 `0-9`)、`0x61-0x7a`(即小写字母 `a-z`) 范围内的字符作为扩展名,其他字符将会被自动忽略。 + +从 v3.14.0 开始,你可以传递一个函数,可以根据不同的条件动态返回白名单。 + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/upload'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + upload: { + whitelist: (ctx) => { + if (ctx.path === '/') { + return [ + '.jpg', + '.jpeg', + ]; + } else { + return [ + '.jpg', + ] + }; + }, + // ... + }, +} +``` + + + + + +### MIME 类型检查 + +部分`恶意用户`,会尝试将 `.php` 等 WebShell 修改扩展名为 `.jpg`,来绕过基于扩展名的白名单过滤规则,在某些服务器环境内,这个 jpg 文件依然会被作为 PHP 脚本来执行,造成安全风险。 + +因此,`@midwayjs/upload` 组件提供了 `mimeTypeWhiteList` 配置参数 **【请注意,此参数无默认值设置,即默认不校验】**,您可以通过此配置设置允许的文件 MIME 格式,规则为由数组 `[扩展名, mime, [...moreMime]]` 组成的 `二级数组`,例如: + +```typescript +// src/config/config.default.ts +import { uploadWhiteList } from '@midwayjs/upload'; +export default { + // ... + upload: { + // ... + // 扩展名白名单 + whitelist: uploadWhiteList, + // 仅允许下面这些文件类型可以上传 + mimeTypeWhiteList: { + '.jpg': 'image/jpeg', + // 也可以设置多个 MIME type,比如下面的允许 .jpeg 后缀的文件是 jpg 或者是 png 两种类型 + '.jpeg': ['image/jpeg', 'image/png'], + // 其他类型 + '.gif': 'image/gif', + '.bmp': 'image/bmp', + '.wbmp': 'image/vnd.wap.wbmp', + '.webp': 'image/webp', + } + }, +} +``` + +您也可以使用 `@midwayjs/upload` 组件提供的 `DefaultUploadFileMimeType` 变量,作为默认的 MIME 校验规则,它提供了常用的 `.jpg`、`.png`、`.psd` 等文件扩展名的 MIME 数据: + +```typescript +// src/config/config.default.ts +import { uploadWhiteList, DefaultUploadFileMimeType } from '@midwayjs/upload'; +export default { + // ... + upload: { + // ... + // 扩展名白名单 + whitelist: uploadWhiteList, + // 仅允许下面这些文件类型可以上传 + mimeTypeWhiteList: DefaultUploadFileMimeType, + }, +} +``` + +文件格式与对应的 MIME 映射,您可以通过 `https://mimetype.io/` 这个网站来查询,对于文件的 MIME 识别,我们使用的是 [file-type@16](https://www.npmjs.com/package/file-type) 这个 npm 包,请注意它支持的文件类型。 + +:::info + +MIME 类型校验规则仅适用于使用 文件上传模式 `mode=file`,同时设置此校验规则之后,由于需要读取文件内容进行匹配,所以会稍微影响上传性能。 + +但是,我们依然建议您在条件允许的情况下,设置 `mimeTypeWhiteList` 参数,这将提升您的应用程序安全性。 + +::: + +从 v3.14.0 开始,你可以传递一个函数,可以根据不同的条件动态返回 MIME 规则。 + +```typescript +// src/config/config.default.ts +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + upload: { + mimeTypeWhiteList: (ctx) => { + if (ctx.path === '/') { + return { + '.jpg': 'image/jpeg', + }; + } else { + return { + '.jpeg': ['image/jpeg', 'image/png'], + } + }; + } + }, +} + +``` + + + +### 配置 match 或 ignore + +当开启了 upload 组件后,当请求的 `method` 为 `POST/PUT/DELETE/PATCH` 之一时,如果判断请求的 `headers['content-type']` 中包含 `multipart/form-data` 及 `boundary` 时,将会 `**自动进入**` 上传文件解析逻辑。 + +这会造成:如果用户可能手动分析了网站的请求信息,手动调用任一一个 `post` 等类型的接口,将一个文件进行上传,就会触发 `upload` 组件的解析逻辑,在临时目录创建临时的已上传文件缓存,对网站服务器产生不必要的`负荷`,严重时可能会`影响`服务器正常业务逻辑处理。 + +所以,您可以在配置中添加 `match` 或 `ignore` 配置,来设置哪些 api 路径是允许进行上传的。 + + + +### 同名 Field + +从 v3.16.6 开始,组件支持同名 Field。 + +```typescript +// src/config/config.default.ts +import { tmpdir } from 'os'; +import { join } from 'path'; + +export default { + // ... + upload: { + allowFieldsDuplication: true + }, +} + +``` + +开启 `allowFieldsDuplication` 之后,同名的 Field 会被合并为数组。 + +```typescript +import { Controller, Inject, Post, Files, Fields } from '@midwayjs/core'; + +@Controller('/') +export class HomeController { + @Post('/upload') + async upload(@Files() files, @Fields() fields) { + /* + fields = { + name: ['name1', 'name2'], + otherName: 'nameOther' + // ... + } + + */ + } +} +``` + + + + +## 临时文件与清理 + + +如果你使用了 `file` 模式来获取上传的文件,那么上传的文件会存放在您于 `config` 文件中设置的 `upload` 组件配置中的 `tmpdir` 选项指向的文件夹内。 + +你可以通过在配置中使用 `cleanTimeout` 来控制自动的临时文件清理时间,默认值为 `5 * 60 * 1000`,即上传的文件于 `5 分钟` 后自动清理,设置为 `0` 则视为不开启自动清理功能。 + +你也可以在代码中通过调用 `await ctx.cleanupRequestFiles()` 来主动清理当前请求上传的临时文件。 + + + +## 安全提示 + +1. 请注意是否开启 `扩展名白名单` (whiteList),如果扩展名白名单被设置为 `null`,则会有可能被攻击者所利用上传 `.php`、`.asp` 等WebShell。 +2. 请注意是否设置 `match` 或 `ignore` 规则,否则普通的 `POST/PUT` 等接口会有可能被攻击者利用,造成服务器负荷加重和空间大量占用问题。 +3. 请注意是否设置 `文件类型规则` (fileTypeWhiteList),否则可能会被攻击者伪造文件类型进行上传。 + + + +## 前端文件上传示例 + +### 1. html form 的形式 + +```html +
+ Name:
+ File:
+ +
+``` + +### 2. fetch FormData 方式 +```js +const fileInput = document.querySelector('#your-file-input') ; +const formData = new FormData(); +formData.append('file', fileInput.files[0]); + +fetch('/api/upload', { + method: 'POST', + body: formData, +}); +``` + + + +## Postman 测试示例 + +![](https://img.alicdn.com/imgextra/i4/O1CN01iv9ESW1uIShNiRjBF_!!6000000006014-2-tps-2086-1746.png) + diff --git a/site/versioned_docs/version-3.0.0/extensions/validate.md b/site/versioned_docs/version-3.0.0/extensions/validate.md new file mode 100644 index 000000000000..5fa32e7c92dc --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/validate.md @@ -0,0 +1,882 @@ +# 参数校验 + +我们经常要在方法调用时执行一些类型检查,参数转换的操作,Midway 提供了一种简单的能力来快速检查参数的类型,这个能力来源于 [joi](https://joi.dev/api/) 。 + +相关信息: + +| 描述 | | +| ----------------- | --- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + +## 背景 + +最常用参数校验的地方是 控制器(Controller),同时你也可以在任意的 Class 中使用这个能力。 + +我们以控制器(Controller)中使用为例,还是那个 user。 + +```typescript +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ └── user.ts +│ ├── interface.ts +│ └── service +│ └── user.ts +├── test +├── package.json +└── tsconfig.json +``` + +普通情况下,我们从 `body` 上拿到所有 Post 结果,并进行一些校验。 + +```typescript +// src/interface.ts +export interface User { + id: number; + firstName: string; + lastName: string; + age: number; +} + +// src/controller/home.ts +import { Controller, Get, Provide } from '@midwayjs/core'; + +@Controller('/api/user') +export class HomeController { + @Post('/') + async updateUser(@Body() user: User) { + if (!user.id || typeof user.id !== 'number') { + throw new Error('id error'); + } + + if (user.age <= 30) { + throw new Error('age not match'); + } + // xxx + } +} +``` + +如果每个方法都需要这么校验,会非常的繁琐。 + +针对这种情况,Midway 提供了 Validate 组件。 配合 `@Validate` 和 `@Rule` 装饰器,用来 **快速定义校验的规则**,帮助用户 **减少这些重复的代码**。 + +注意,从 v3 开始,`@Rule` 和 `@Validate` 装饰器从 `@midwayjs/validate` 中导出。 + +## 安装依赖 + +```bash +$ npm i @midwayjs/validate@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/validate": "^3.0.0" + // ... + }, + "devDependencies": { + // ... + } +} +``` + +## 开启组件 + +在 `configuration.ts` 中增加组件。 + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as validate from '@midwayjs/validate'; +import { join } from 'path'; + +@Configuration({ + imports: [koa, validate], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration { + @App() + app: koa.Application; + + async onReady() { + // ... + } +} +``` + +## 定义检查规则 + +按照上面的逻辑,我们需要 **重新定义一个新的 Class**,因为装饰器只能装饰在实际的 Class 上,而不是 interface。 + +为了方便后续处理,我们将 user 放到一个 `src/dto` 目录中。 + +> Data Transfer Object(数据传输对象)DTO 是一组需要跨进程或网络边界传输的聚合数据的简单容器。它不应该包含业务逻辑,并将其行为限制为诸如内部一致性检查和基本验证之类的活动。 + +```typescript +// src/dto/user.ts +import { Rule, RuleType } from '@midwayjs/validate'; + +export class UserDTO { + @Rule(RuleType.number().required()) + id: number; + + @Rule(RuleType.string().required()) + firstName: string; + + @Rule(RuleType.string().max(10)) + lastName: string; + + @Rule(RuleType.number().max(60)) + age: number; +} +``` + +由于这个类属于一个 `PlainObject` ,也不需要被依赖注入管理,我们不需要提供 `@Provide` 装饰器。 + +这个 User Class 提供了三个属性和他们对应的校验规则。 + +- `id` 一个必填的数字类型 +- `firstName` 一个必填的字符串类型 +- `lastName` 一个可选的最长为 10 的字符串类型 +- `age` 一个最大不超过 60 的数字 + +`@Rule` 装饰器用于 **修饰需要被校验的属性**,它的参数为 `RuleType` 对象提供的校验规则的链式方法。 + +:::info +这里的 `RuleType` 即为 joi 对象本身。 +::: + +[joi](https://joi.dev/api/) 提供了非常多的校验类型,还可以对对象和数组中的字段做校验,还有例如字符串常用的 `RuleType.string().email()` ,以及 `RuleType.string().pattern(/xxxx/)` 正则校验等,具体可以查询 [joi](https://joi.dev/api/) 的 API 文档。 + + + +## 校验参数 + +定义完类型之后,就可以直接在业务代码中使用了。 + +```typescript +// src/controller/home.ts +import { Controller, Get, Provide, Body } from '@midwayjs/core'; +import { UserDTO } from './dto/user'; + +@Controller('/api/user') +export class HomeController { + @Post('/') + async updateUser(@Body() user: UserDTO) { + // user.id + } +} +``` + +所有的校验代码都通通不见了,业务变的更纯粹了,当然,记得要把原来的 user interface 换成 Class。 + +一旦校验失败,浏览器或者控制台就会报出类似的错误。 + +``` +ValidationError: "id" is required +``` + +同时,由于定义了 `id` 的类型,在拿到字符串的情况下,会自动将 id 变为数字。 + +```typescript +async updateUser(@Body() user: UserDTO ) { + // typeof user.id === 'number' +} +``` + +如果需要对方法级别单独配置信息,可以使用 `@Validate` 装饰器,比如单独配置错误状态。 + +```typescript +// src/controller/home.ts +import { Controller, Get, Provide } from '@midwayjs/core'; +import { Validate } from '@midwayjs/validate'; +import { UserDTO } from './dto/user'; + +@Controller('/api/user') +export class HomeController { + @Post('/') + @Validate({ + errorStatus: 422, + }) + async updateUser(@Body() user: UserDTO) { + // user.id + } +} +``` + +一般情况下,使用全局默认配置即可。 + + + +## 通用场景校验 + +如果参数不是 DTO,可以使用 `@Valid` 装饰器进行校验,`@Valid` 装饰器可以直接传递一个 Joi 规则。 + +```typescript +// src/controller/home.ts +import { Controller, Get, Query } from '@midwayjs/core'; +import { Valid, RuleType } from '@midwayjs/validate'; +import { UserDTO } from './dto/user'; + +@Controller('/api/user') +export class HomeController { + @Get('/') + async getUser(@Valid(RuleType.number().required()) @Query('id') id: number) { + // ... + } +} +``` + +在非 Web 场景下,没有 `@Body` 等 Web 类装饰器的情况下,也可以使用 `@Valid` 装饰器来进行校验,如果不传参数,也会复用 DTO 规则。 + +比如在服务中: + +```typescript +import { Valid } from '@midwayjs/validate'; +import { Provide } from '@midwayjs/core'; +import { UserDTO } from './dto/user'; + +@Provide() +export class UserService { + async updateUser(@Valid() user: UserDTO) { + // ... + } +} +``` + +如果参数不是 DTO,不存在规则,也可以通过参数传递一个 Joi 格式的校验规则。 + +```typescript +import { Valid, RuleType } from '@midwayjs/validate'; +import { Provide } from '@midwayjs/core'; + +@Provide() +export class UserService { + async updateUser(@Valid(RuleType.number().required()) userAge: number) { + // ... + } +} +``` + + + +## 校验管道 + +如果你的参数是基础类型,比如 `number`, `string`, `boolean`,则可以使用组件提供的管道进行校验。 + +默认的 Web 参数装饰器都可以在第二个参数传入管道。 + +比如: + +```typescript +import { ParseIntPipe } from '@midwayjs/validate'; +import { Controller, Post, Body } from '@midwayjs/core'; + +@Controller('/api/user') +export class HomeController { + @Post('/update_age') + async updateAge(@Body('age', [ParseIntPipe]) age: number) { + // ... + } +} +``` + +`ParseIntPipe` 管道可以将字符串,数字数据转换为数字,这样从请求参数获取到的 `age` 字段则会通过管道的校验并转换为数字格式。 + +可以使用 的内置管道有: + +- `ParseIntPipe` +- `ParseFloatPipe` +- `ParseBoolPipe` +- `DefaultValuePipe` + +`ParseIntPipe` 用于将参数转为整形数字。 + +```typescript +import { ParseIntPipe } from '@midwayjs/validate'; + +// ... +async update(@Body('age', [ParseIntPipe]) age: number) { + return age; +} + +update({ age: '12'} ); => 12 +update({ age: '12.2'} ); => Error +update({ age: 'abc'} ); => Error +``` + +`ParseFloatPipe` 用于将参数转为浮点型数字数字。 + +```typescript +import { ParseFloatPipe } from '@midwayjs/validate'; + +// ... +async update(@Body('size', [ParseFloatPipe]) size: number) { + return size; +} + +update({ size: '12.2'} ); => 12.2 +update({ size: '12'} ); => 12 +``` + +`ParseBoolPipe` 用于将参数转为布尔值。 + +```typescript +import { ParseBoolPipe } from '@midwayjs/validate'; + +// ... +async update(@Body('isMale', [ParseBoolPipe]) isMale: boolean) { + return isMale; +} + +update({ isMale: 'true'} ); => true +update({ isMale: '0'} ); => Error +``` + +`DefaultValuePipe` 用于设定默认值。 + +```typescript +import { DefaultValuePipe } from '@midwayjs/validate'; + +// ... +async update(@Body('nickName', [new DefaultValuePipe('anonymous')]) nickName: string) { + return nickName; +} + +update({ nickName: undefined} ); => 'anonymous' +``` + + + +## 自定义校验管道 + +如果默认的管道不满足需求,可以通过继承,快速实现一个自定义校验管道,组件已经提供了一个 `ParsePipe` 类用于快速编写。 + +```typescript +import { Pipe } from '@midwayjs/core'; +import { ParsePipe, RuleType } from '@midwayjs/validate'; + +@Pipe() +export class ParseCustomDataPipe extends ParsePipe { + getSchema(): RuleType.AnySchema { + // ... + } +} +``` + +`getSchema` 方法用于返回一个符合 `Joi` 格式的校验规则。 + +比如 `ParseIntPipe` 的代码如下,管道执行时会自动获取这个 schema 进行校验,并在校验成功后将值返回。 + +```typescript +import { Pipe } from '@midwayjs/core'; +import { ParsePipe, RuleType } from '@midwayjs/validate'; + +@Pipe() +export class ParseIntPipe extends ParsePipe { + getSchema() { + return RuleType.number().integer().required(); + } +} +``` + +## 校验规则 + +### 常见的校验写法 + +```typescript +RuleType.number().required(); // 数字,必填 +RuleType.string().empty(''); // 字符串非必填 +RuleType.number().max(10).min(1); // 数字,最大值和最小值 +RuleType.number().greater(10).less(50); // 数字,大于 10,小于 50 + +RuleType.string().max(10).min(5); // 字符串,长度最大 10,最小 5 +RuleType.string().length(20); // 字符串,长度 20 +RuleType.string().pattern(/^[abc]+$/); // 字符串,匹配正则格式 + +RuleType.object().length(5); // 对象,key 数量等于 5 + +RuleType.array().items(RuleType.string()); // 数组,每个元素是字符串 +RuleType.array().max(10); // 数组,最大长度为 10 +RuleType.array().min(10); // 数组,最小长度为 10 +RuleType.array().length(10); // 数组,长度为 10 + +RuleType.string().allow(''); // 非必填字段传入空字符串 + +export enum DeviceType { + iOS = 'ios', + Android = 'android', +} +RuleType.string().valid(...Object.values(DeviceType)) // 根据枚举值校验 +``` + +### 级联校验 + +Midway 支持每个校验的 Class 中的属性依旧是一个对象。 + +我们给 `UserDTO` 增加一个属性 `school` ,并且赋予一个 `SchoolDTO` 类型。 + +```typescript +import { Rule, RuleType, getSchema } from '@midwayjs/validate'; + +export class SchoolDTO { + @Rule(RuleType.string().required()) + name: string; + @Rule(RuleType.string()) + address: string; +} + +export class UserDTO { + @Rule(RuleType.number().required()) + id: number; + + @Rule(RuleType.string().required()) + firstName: string; + + @Rule(RuleType.string().max(10)) + lastName: string; + + // 复杂对象 + @Rule(getSchema(SchoolDTO).required()) + school: SchoolDTO; + + // 对象数组 + @Rule(RuleType.array().items(getSchema(SchoolDTO)).required()) + schoolList: SchoolDTO[]; +} +``` + +这个时候, `@Rule` 装饰器的参数可以为需要校验的这个类型本身。 + +### 继承校验 + +Midway 支持校验继承方式,满足开发者抽离通用的对象属性的时候做参数校验。 + +例如我们下面 `CommonUserDTO` 抽离接口的通用的一些属性,然后 `UserDTO` 作为特殊接口需要的特定参数。 + +```typescript +import { Rule, RuleType } from '@midwayjs/validate'; + +export class CommonUserDTO { + @Rule(RuleType.string().required()) + token: string; + @Rule(RuleType.string()) + workId: string; +} + +export class UserDTO extends CommonUserDTO { + @Rule(RuleType.string().required()) + name: string; +} +``` + +老版本需要在子类上面加,新版本不需要啦~ + +:::info +如果属性名相同,则取当前属性的规则进行校验,不会和父类合并。 +::: + +### 多类型校验 + +从 v3.4.5 开始,Midway 支持某个属性的不同类型的校验。 + +例如某个类型,既可以是可以普通类型,又可以是一个复杂类型。 + +```typescript +import { Rule, RuleType, getSchema } from '@midwayjs/validate'; + +export class SchoolDTO { + @Rule(RuleType.string().required()) + name: string; + @Rule(RuleType.string()) + address: string; +} + +export class UserDTO { + @Rule(RuleType.string().required()) + name: string; + + @Rule(RuleType.alternatives([RuleType.string(), getSchema(SchoolDTO)]).required()) + school: string | SchoolDTO; +} +``` + +我们可以使用 `getSchema` 方法,从某个 DTO 拿到当前的 joi schema,从而进行复杂的逻辑处理。 + +### 从原有 DTO 创建新 DTO + +有时候,我们会希望从某个 DTO 中获取一部分属性,变成一个新的 DTO 类。 + +Midway 提供了 `PickDto` 和 `OmitDto` 两个方法根据现有的的 DTO 类型创建新的 DTO。 + +`PickDto` 用于从现有的 DTO 中获取一些属性,变成新的 DTO,而 `OmitDto` 用于将其中某些属性剔除,比如: + +```typescript +// src/dto/user.ts +import { Rule, RuleType, PickDto } from '@midwayjs/validate'; + +export class UserDTO { + @Rule(RuleType.number().required()) + id: number; + + @Rule(RuleType.string().required()) + firstName: string; + + @Rule(RuleType.string().max(10)) + lastName: string; + + @Rule(RuleType.number().max(60)) + age: number; +} + +// 继承出一个新的 DTO +export class SimpleUserDTO extends PickDto(UserDTO, ['firstName', 'lastName']) {} + +// const simpleUser = new SimpleUserDTO(); +// 只包含了 firstName 和 lastName 属性 +// simpleUser.firstName = xxx + +export class NewUserDTO extends OmitDto(UserDTO, ['age']) {} + +// const newUser = new NewUserDTO(); +// newUser.age 定义和属性都不存在 + +// 使用 +async login(@Body() user: NewUserDTO) { + // ... +} + +``` + +### 复用校验规则 + +如果很多字段都是字符串必填,或者类似需求,写 `RuleType.string().required()` 有点长,可以将重复的部分赋值为新的规则对象,进行复用。 + +```typescript +// 自己在一个文件中定义一下你们部门的规范或常用的 +const requiredString = RuleType.string().required(); + +export class UserDTO { + @Rule(requiredString) // 这样就不用写上面这么长的了 + name: string; + + @Rule(requiredString) // 同上 + nickName: string; + + @Rule(requiredString) // 同上 + description: string; +} + +// 自己在一个文件中定义一下你们部门的规范或常用的 +const maxString = (length) => RuleType.string().max(length); + +export class UserDTO { + @Rule(requiredString) // 同上 + name: string; + + @Rule(requiredString) // 同上 + nickName: string; + + @Rule(requiredString) // 同上 + description: string; + + @Rule(maxString(50)) // 这样通过换个参数即可 + info: string; + + @Rule(maxString(50).required()) // 这样也行 + info2: string; +} +``` + +## 多语言 + +在 Validate 中,同时依赖了 [i18n](./i18n) 组件来实现校验消息的国际化。 + +默认情况下,提供了 `en_US` 和 `zh_CN` 两种校验的翻译文本,所以在请求失败时,会返回当前浏览器访问所指定的语言。 + + + +### 通过装饰器指定语言 + +默认情况下,会跟着 i18n 组件的 `defaultLocale` 以及浏览器访问语言的情况来返回消息,不过,我们可以在装饰器中指定当前翻译的语言,比如: + +```typescript +@Controller('/user') +export class UserController { + @Post('/') + @Validate({ + locale: 'en_US', + }) + async getUser(@Body() bodyData: UserDTO) { + // ... + } +} +``` + +### 通过参数指定语言 + +除了装饰器指定,我们也可以使用标准的 i18n 通过参数指定语言的方式。 + +比如 Query 参数。 + +``` +Get /user/get_user?locale=zh_CN +``` + +更多的参数用法请参考 [i18n](./i18n) 组件。 + +### 其他语言的翻译 + +默认情况下,Midway 提供了 `en_US` 和 `zh_CN` 两种校验的翻译文本,如果还需要额外的翻译,可以配置在 i18n 中。 + +比如: + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // 增加翻译 + zh_TW: { + validate: require('../../locales/zh_TW.json'), + }, + }, +}; +``` + +如果可以的话,我们希望你将翻译提交给 Midway 官方,让大家都能使用。 + +## 自定义错误文本 + +### 指定单个规则的文本 + +如果只想定义某个 DTO 中某个规则的错误消息,可以简单指定。 + +```typescript +export class UserDTO { + @Rule(RuleType.number().required().error(new Error('my custom message'))) + id: number; +} +``` + +这个 `id` 属性上的所有规则,只要有验证失败的,都会返回你的自定义消息。 + +### 全局指定部分文本 + +通过配置 i18n 组件的 `validate` 多语言文本表,你可以选择性的替换大部分的校验文本,所有的规则都会应用该文本。 + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // 把你的翻译文本放到这里 + localeTable: { + zh_CN: { + validate: { + 'string.max': 'hello world', + }, + }, + }, + }, +}; +``` + +这里的 `validate` 是 `@midwayjs/validate` 组件在 i18n 组件中配置的语言表关键字。 + +由于 [默认的语言表](https://github.com/midwayjs/midway/tree/main/packages/validate/locales) 也是对象形式,我们可以很方便的找到其中的字段,进行替换。 + +由于这些文本区分语言,所以需要谨慎处理,比如,替换不同的语言。 + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + // 把你的翻译文本放到这里 + localeTable: { + zh_CN: { + validate: { + 'string.max': '字符超长', + }, + }, + en_US: { + validate: { + 'string.max': 'string is too long', + }, + }, + }, + }, +}; +``` + +### 完全自定义错误文本 + +如果希望完全自定义错误文本,可以通过替换内置的语言翻译文本来解决。 + +比如: + +```typescript +// src/config/config.default.ts +export default { + // ... + i18n: { + localeTable: { + // 替换中文翻译 + zh_CN: { + validate: require('../../locales/custom.json'), + }, + }, + }, +}; +``` + +## 默认配置 + +我们可以对 validate 组件做一些配置。 + +| 配置项 | 类型 | 描述 | +| ----------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| errorStatus | number | 当校验出错时,返回的 Http 状态码,在 http 场景生效,默认 422 | +| locale | string | 校验出错文本的默认语言,当前有 `en_US` 和 `zh_CN` 两种,默认为 `en_US`,会根据 i18n 组件的规则切换 | +| validationOptions | joi 的 ValidationOptions 选项 | 常用的有 allowUnknown,stripUnknown 等选项,如果配置,那么全局的校验都允许出现没有定义的字段,更多的请查看 joi 的 [ValidationOptions 选项](https://joi.dev/api/?v=17.6.0#anyvalidatevalue-options)。 | + +## 独立的校验服务 + +组件底层提供了单例的 `ValidateService` 校验服务类,如有必要,可以在中间件或者独立的服务中使用。事实上,所有的校验装饰器,最终都会走到这个方法。 + +`ValidateService` 提供了一个 `validate` 方法,用于校验 DTO。 + +我们以上面定义的 `UserDTO` 为例。 + +```typescript +import { ValidateService } from '@midwayjs/validate'; + +export class UserService { + @Inject() + validateService: ValidateService; + + async inovke() { + // ... + const result = this.validateService.validate(UserDTO, { + name: 'harry', + nickName: 'harry', + }); + + // 失败返回 result.error + // 成功返回 result.value + } +} +``` + +`validate` 方法返回的 result 包含 `error` 和 `value` 两个属性。失败会返回 `MidwayValidationError` 错误,成功会返回格式化好的 DTO 对象。 + +## 常见问题 + +### 1、允许未定义的字段 + +由于部分用户在参数校验的时候,希望允许出现没有定义的字段,可以在全局配置,以及装饰器上分别设置,前者对全局生效,后者对单个校验生效。 + +```typescript +// src/config/config.default.ts +export default { + // ... + validate: { + validationOptions: { + allowUnknown: true, // 全局生效 + }, + }, +}; +``` + +或者在装饰器上。 + +```typescript +@Controller('/api/user') +export class HomeController { + @Post('/') + @Validate({ + validationOptions: { + allowUnknown: true, + }, + }) + async updateUser(@Body() user: UserDTO) { + // user.id + } +} +``` + +### 2、剔除参数中的未定义属性 + +也同样是 validationOptions 的属性,可以直接剔除传入的参数中的某些属性。 + +```typescript +// src/config/config.default.ts +export default { + // ... + validate: { + validationOptions: { + stripUnknown: true, // 全局生效 + }, + }, +}; +``` + +或者在装饰器上。 + +```typescript +@Controller('/api/user') +export class HomeController { + @Post('/') + @Validate({ + validationOptions: { + stripUnknown: true, + }, + }) + async updateUser(@Body() user: UserDTO) {} +} +``` + +### 3、处理校验错误 + +上面提到,Midway 会在校验失败时抛出 `MidwayValidationError` 错误,我们可以在 [异常处理器](../error_filter) 中处理。 + +比如: + +```typescript +// src/filter/validate.filter +import { Catch } from '@midwayjs/core'; +import { MidwayValidationError } from '@midwayjs/validate'; +import { Context } from '@midwayjs/koa'; + +@Catch(MidwayValidationError) +export class ValidateErrorFilter { + async catch(err: MidwayValidationError, ctx: Context) { + // ... + return { + status: 422, + message: '校验参数错误,' + err.message, + }; + } +} +``` + +### 4、临时禁用全局校验 + +开启组件后,只要参数使用了 DTO,就会自动被校验,如果某个参数临时无需验证,可以使用下面的写法。 + +```typescript +@Controller('/api/user') +export class HomeController { + @Post('/') + async updateUser(@Body() user: Partial) {} +} +``` diff --git a/site/versioned_docs/version-3.0.0/extensions/ws.md b/site/versioned_docs/version-3.0.0/extensions/ws.md new file mode 100644 index 000000000000..c8d355d89158 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/extensions/ws.md @@ -0,0 +1,476 @@ +# WebSocket + +[ws](https://www.npmjs.com/package/ws) 模块是 Node 端的一个 WebSocket 协议的实现,该协议允许客户端(一般是浏览器)持久化和服务端的连接. +这种可以持续连接的特性使得 WebSocket 特别适合用于适合用于游戏或者聊天室等使用场景。 + +Midway 提供了对 [ws](https://www.npmjs.com/package/ws) 模块的支持和封装,能够简单的创建一个 WebSocket 服务。 + +相关信息: + +**提供服务** + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ❌ | +| 可用于一体化 | ✅ | +| 包含独立主框架 | ❌ | +| 包含独立日志 | ❌ | + + + +## 安装依赖 + + +在现有项目中安装 WebSocket 的依赖。 +```bash +$ npm i @midwayjs/ws@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/ws": "^3.0.0", + // ... + }, +} +``` + +## 开启组件 + +`@midwayjs/ws` 可以作为独立主框架使用。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as ws from '@midwayjs/ws'; + +@Configuration({ + imports: [ws], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + +``` + +也可以附加在其他的主框架下,比如 `@midwayjs/koa` 。 + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as ws from '@midwayjs/ws'; + +@Configuration({ + imports: [koa, ws], + // ... +}) +export class MainConfiguration { + async onReady() { + // ... + } +} + +``` + + + +## 目录结构 + + +下面是 WebSocket 项目的基础目录结构,和传统应用类似,我们创建了 `socket` 目录,用户存放 WebSocket 业务的服务代码。 +``` +. +├── package.json +├── src +│ ├── configuration.ts ## 入口配置文件 +│ ├── interface.ts +│ └── socket ## ws 服务的文件 +│ └── hello.controller.ts +├── test +├── bootstrap.js ## 服务启动入口 +└── tsconfig.json +``` + + +## 提供 Socket 服务 + + +Midway 通过 `@WSController` 装饰器定义 WebSocket 服务。 +```typescript +import { WSController } from '@midwayjs/core'; + +@WSController() +export class HelloSocketController { + // ... +} +``` +当有客户端连接时,会触发 `connection` 事件,我们在代码中可以使用 `@OnWSConnection()` 装饰器来修饰一个方法,当每个客户端第一次连接服务时,将自动调用该方法。 +```typescript +import { WSController, OnWSConnection, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/ws'; +import * as http from 'http'; + +@WSController() +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSConnection() + async onConnectionMethod(socket: Context, request: http.IncomingMessage) { + console.log(`namespace / got a connection ${this.ctx.readyState}`); + } +} + +``` + + +:::info +这里的 ctx 等价于 WebSocket 实例。 +::: + + +## 消息和响应 + + +WebSocket 是通过事件的监听方式来获取数据。Midway 提供了 `@OnWSMessage()` 装饰器来格式化接收到的事件,每次客户端发送事件,被修饰的方法都将被执行。 +```typescript +import { WSController, OnWSMessage, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/ws'; + +@WSController() +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSMessage('message') + async gotMessage(data) { + return { name: 'harry', result: parseInt(data) + 5 }; + } +} + +``` + + +我们可以通过 `@WSBroadCast` 装饰器将消息发送到所有连接的客户端上。 +```typescript +import { WSController, OnWSConnection, Inject } from '@midwayjs/core'; +import { Context } from '@midwayjs/ws'; + +@WSController() +export class HelloSocketController { + + @Inject() + ctx: Context; + + @OnWSMessage('message') + @WSBroadCast() + async gotMyMessage(data) { + return { name: 'harry', result: parseInt(data) + 5 }; + } + + @OnWSDisConnection() + async disconnect(id: number) { + console.log('disconnect ' + id); + } +} + +``` +通过 `@OnWSDisConnection` 装饰器,在客户端断连时,做一些额外处理。 + + + +## WebSocket Server 实例 + +该组件提供的 App 即为 WebSocket Server 实例本身,我们可以如下获取。 + +```typescript +import { Controller, App } from '@midwayjs/core'; +import { Application } from '@midwayjs/ws'; + +@Controller() +export class HomeController { + + @App('webSocket') + wsApp: Application; +} +``` + +比如,我们可以在其他 Controller 或者 Service 中广播消息。 + +```typescript +import { Controller, App } from '@midwayjs/core'; +import { Application } from '@midwayjs/ws'; + +@Controller() +export class HomeController { + + @App('webSocket') + wsApp: Application; + + async invoke() { + this.wsApp.clients.forEach(ws => { + // ws.send('something'); + }); + } +} +``` + + + +## 心跳检查 + +有时服务器和客户端之间的连接可能会中断,服务器和客户端都不知道连接的断开情况。 + +可以通过启用 `enableServerHeartbeatCheck` 配置心跳检查主动断开请求。 + +```typescript +// src/config/config.default +export default { + // ... + webSocket: { + enableServerHeartbeatCheck: true, + }, +} +``` + +默认检查时间为 `30*1000` 毫秒,可以通过 `serverHeartbeatInterval` 进行修改,配置单位为毫秒。 + +```typescript +// src/config/config.default +export default { + // ... + webSocket: { + serverHeartbeatInterval: 30000, + }, +} +``` + +这一配置每隔一段时间会自动发送 `ping` 包,客户端若没有在下一个时间间隔返回消息,则会被自动 `terminate` 。 + +客户端如果希望知道服务端的状态,可以通过监听 `ping` 消息来实现。 + +```typescript +import WebSocket from 'ws'; + +function heartbeat() { + clearTimeout(this.pingTimeout); + + // 每次接收 ping 之后,延迟等待,如果下一次未拿到服务端 ping 消息,则认为出现问题 + this.pingTimeout = setTimeout(() => { + // 重连或者中止 + }, 30000 + 1000); +} + +const client = new WebSocket('wss://websocket-echo.com/'); + +// ... +client.on('ping', heartbeat); +``` + + + +## 本地测试 + +### 配置测试端口 + +由于 ws 框架可以独立启动(依附于默认的 http 服务,也可以和其他 midway 框架一起启动)。 + +当作为独立框架启动时,需要指定端口。 + +```typescript +// src/config/config.default +export default { + // ... + webSocket: { + port: 3000, + }, +} +``` + +当作为副框架启动时(比如和 http ,由于 http 在单测时未指定端口(使用 supertest 自动生成),无法很好的测试,可以仅在测试环境显式指定一个端口。 + +```typescript +// src/config/config.unittest +export default { + // ... + koa: { + port: null, + }, + webSocket: { + port: 3000, + }, +} +``` + +:::tip + +- 1、这里的端口仅为 WebSocket 服务在测试时启动的端口 +- 2、koa 中的端口为 null,即意味着在测试环境下,不配置端口,不会启动 http 服务 + +::: + +### 测试代码 + +和其他 Midway 测试方法一样,我们使用 `createApp` 启动项目。 + +```typescript +import { createApp, close } from '@midwayjs/mock' +// 这里使用的 Framework 定义,以主框架为准 +import { Framework } from '@midwayjs/koa'; + +describe('/test/index.test.ts', () => { + + it('should create app and test webSocket', async () => { + const app = await createApp(); + + //... + + await close(app); + }); + +}); +``` + + +### 测试客户端 + +你可以直接使用 `ws` 来测试。也可以使用 Midway 提供的基于 `ws` 模块封装的测试客户端。 + + +比如: +```typescript +import { createApp, close, createWebSocketClient } from '@midwayjs/mock'; +import { sleep } from '@midwayjs/core'; + +// ... 省略 describe + +it('should test create websocket app', async () => { + + // 创建一个服务 + const app = await createApp(); + + // 创建一个客户端 + const client = await createWebSocketClient(`ws://localhost:3000`); + + const result = await new Promise(resolve => { + + client.on('message', (data) => { + // xxxx + resolve(data); + }); + + // 发送事件 + client.send(1); + + }); + + // 判断结果 + expect(JSON.parse(result)).toEqual({ + name: 'harry', + result: 6, + }); + + await sleep(1000); + + // 关闭客户端 + await client.close(); + + // 关闭服务端 + await close(app); + +}); +``` + + +使用 node 自带的 `events` 模块的 `once` 方法来优化,就会变成下面的代码。 +```typescript +import { sleep } from '@midwayjs/core'; +import { once } from 'events'; +import { createApp, close, createWebSocketClient } from '@midwayjs/mock'; + +// ... 省略 describe + +it('should test create websocket app', async () => { + + // 创建一个服务 + const app = await createApp(process.cwd()); + + // 创建一个客户端 + const client = await createWebSocketClient(`ws://localhost:3000`); + + // 发送事件 + client.send(1); + + // 用事件的 promise 写法监听 + let gotEvent = once(client, 'message'); + // 等待返回 + let [data] = await gotEvent; + + // 判断结果 + expect(JSON.parse(data)).toEqual({ + name: 'harry', + result: 6, + }); + + await sleep(1000); + + // 关闭客户端 + await client.close(); + + // 关闭服务端 + await close(app); +}); + +``` +两种写法效果相同,按自己理解的写就行。 + + + +## 配置 + +## 默认配置 + +`@midwayjs/ws` 的配置样例如下: + +```typescript +// src/config/config.default +export default { + // ... + webSocket: { + port: 7001, + }, +} +``` + +当 `@midwayjs/ws` 和其他 `@midwayjs/web` , `@midwayjs/koa` , `@midwayjs/express` 同时启用时,可以复用端口。 + +```typescript +// src/config/config.default +export default { + // ... + koa: { + port: 7001, + } + webSocket: { + // 这里不配置即可 + }, +} +``` + + + +| 属性 | 类型 | 描述 | +| --- | --- | --- | +| port | number | 可选,如果传递了该端口,ws 内部会创建一个该端口的 HTTP 服务。如果希望和 midway 其他的 web 框架配合使用,请不要传递该参数。 | +| server | httpServer | 可选,当传递 port 时,可以指定一个已经存在的 webServer | + +更多的启动选项,请参考 [ws 文档](https://github.com/websockets/ws)。 diff --git a/site/versioned_docs/version-3.0.0/faq/alias_path.md b/site/versioned_docs/version-3.0.0/faq/alias_path.md new file mode 100644 index 000000000000..70d9f65e0c80 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/faq/alias_path.md @@ -0,0 +1,68 @@ +# 关于 Alias Path + +我们并不建议使用 Alias Path, Node 和 TS 原生不支持这个功能,即使有,现在也是通过各种 Hack 手段来实现(从 v18 开始,Node.js 已经有 exports 的方案,但是类型还未支持,可以等后续)。 + +如果你一定想要使用,请往下看。 + +## 本地开发的支持(dev 阶段) + +tsc 将 ts 编译成 js 的时候,并不会去转换 import 的模块路径,因此当你在 `tsconfig.json` 中配置了 paths 之后,如果你在 ts 中使用 paths 并 import 了对应模块,编译成 js 的时候就有大概率出现模块找不到的情况。 + +解决办法是,要么不用 paths ,要么使用 paths 的时候只用来 import 一些声明而非具体值,再要么就可以使用 [tsconfig-paths](https://github.com/dividab/tsconfig-paths) 来 hook 掉 node 中的模块路径解析逻辑,从而支持 `tsconfig.json` 中的 paths。 + +```bash +$ npm i tsconfig-paths --save-dev +``` + +使用 tsconfig-paths 可以在 `src/configuration.ts` 中引入。 + +```typescript +// src/configuration.ts + +import 'tsconfig-paths/register'; +// ... +``` + +:::info + +上述的方法只会对 dev 阶段( ts-node)生效。 + +::: + + + +## 测试的支持(jest test) + +在测试中,由于 Jest 的环境比较特殊,需要对 alias 再做一次处理,可以利用 Jest 的配置文件中的 `moduleNameMapper` 功能来替换加载到的模块,变相实现 alias 的功能。 + +```typescript +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1' + } +}; +``` + +注意,这里使用的 alias 前缀是 @符号,如果是其他的 alias 名,请自行修改。 + + + +## 运行时的支持 + + `tsconfig-paths` 是在 ts 运行后在内存中替换路径,编译后依旧会输出带 @符号的路径,使得部署后找不到文件,社区有一些库会在 ts 编译做一些替换支持。 + +比如: + +- https://github.com/justkey007/tsc-alias + + + +## 其他 + +老版本 CLI 中预埋了一个 mwcc 编译器,基于固定 TS 版本在构建器替换 Alias 内容,但是由于依赖 TS 私有 API 导致无法升级 TS 版本,无法享受到新版本的功能。 + +我们从 CLI 2.0 版本开始,移除了这个编译器。 \ No newline at end of file diff --git a/site/versioned_docs/version-3.0.0/faq/framework_problem.md b/site/versioned_docs/version-3.0.0/faq/framework_problem.md new file mode 100644 index 000000000000..6c84feafc870 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/faq/framework_problem.md @@ -0,0 +1,74 @@ +# 常见框架问题 + +## 多个 @midwayjs/core 警告 + + +`@midwayjs/core` 包一般来说,npm 会让相同的依赖在 node_modules 存在一份实例,其余的模块都会通过软链(link)链接到 node_modules/@midwayjs/core。 + + +我们会用到下面的命令,`npm ls` 会列出项目底下某个包的依赖树。 +```bash +$ npm ls @midwayjs/core +``` +比如下图所示。 +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01Td86gC1tQsKjRB8XU_!!6000000005897-2-tps-541-183.png) +灰色的 `deduped` 指的就是该包是被 npm 软链到同一个模块,是正常的。 + + +我们再来看下有问题的示例。 +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01gsnexD1i6lA7kM48q_!!6000000004364-2-tps-1010-308.png) + + +这是一个 lerna 项目,最下面的 demo-docs 中的 decorator 包,后面没有 **deduped** 标示,说明这个包是独立存在的,是错误的。 + + +根据这个思路,我们可以逐步排查为什么会出现这种情况。 + + +比如上图,可能是在单个模块中使用的 npm install,而不是使用 lerna 安装。 + + +我们可以按照下面的思路逐步排查: + + +- 1、包含不同版本的 decorator 包(比如,package-lock 锁包,或者依赖写死版本) +- 2、未正确使用 lerna 的 hoist 模式(比如上图,可能是在单个模块中使用的 npm install,而不是使用 lerna 安装) + + + +## xxx is not valid in current context + + +这个是当依赖注入容器中某个属性所关联的类在依赖注入容器中找不到爆出的。这个错误展示的可能会递归,比较深。 + + +比如: +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01sTvqNX1NiDcoiyS2a_!!6000000001603-2-tps-1053-141.png) +错误核心就是第一个属性,在某个类中找不到。 + + +比如上图的核心就是 `packageBuildInfoHsfService` 这个注入的类找不到。 +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01BBe4gu1KHhqnT0S75_!!6000000001139-2-tps-765-166.png) +这个时候,就需要去对应的类中去看,是否是 provide 出来的名字被自定义了。 + + +常见的问题有: + +- 1、Provide 装饰器导出的名字不对,无法和属性对应 +- 2、Provide 为空的话,大概率是大小写没写对 +- 3、注入是组件的话,可能是漏了组件名 + + +简单的解法:`@Inject` 装饰器不加参数,属性的定义写明确的类,这样 midway 可以自动找到对应的类并注入(不适用于多态的情况)。 +```typescript +@Inject() +service: PackageBuildInfoHsfService; +``` + +## TypeError: (0 ,decorator_1.Framework) is not a function + +原因为使用了错误的版本,比如低版本的框架,使用了高版本的组件(2.x 的框架使用了 3.x 的组件)。 + +![](https://img.alicdn.com/imgextra/i3/O1CN01G7gzCj1EkCpW1gaJl_!!6000000000389-2-tps-1461-491.png) + +解法:确认自己的框架大版本(@midwayjs/core 的版本即为框架版本),选择对应的文档,对应的组件使用。 diff --git a/site/versioned_docs/version-3.0.0/faq/git_problem.md b/site/versioned_docs/version-3.0.0/faq/git_problem.md new file mode 100644 index 000000000000..8b89e926162d --- /dev/null +++ b/site/versioned_docs/version-3.0.0/faq/git_problem.md @@ -0,0 +1,77 @@ +# 常见 git 问题 + +## 文件名大小写问题 + + +由于 git 默认对大小写不敏感,如果文件名从小写变成了大写之后,无法发现文件有变化导致没有提交到仓库。 + + +更可怕的是 mac 也是大小写不敏感,经常出现到本地可以运行,到服务器就执行错误的情况。 + + +为此,我们最好把 git 的默认大小写关闭。 + + +下面的命令。 + +```bash +$ git config core.ignorecase false ## 对当前项目生效 +$ git config --global --add core.ignorecase false ## 对全局生效 +``` + + +## windows 下换行问题 + + +在 Windows 上创建或者克隆代码,开发或者提交时,可能出现如下错误: + +``` +Delete `␍`eslint(prettier/prettier) +``` + +原因如下: + + +由于历史原因,windows下和linux下的文本文件的换行符不一致。 + + +- Windows在换行的时候,同时使用了回车符 CR(carriage-return character) 和换行符 LF(linefeed character) +- 而Mac和Linux系统,仅仅使用了换行符 LF +- 老版本的Mac系统使用的是回车符 CR + + + + +| Windows | Linux/Mac | Old Mac(pre-OSX | +| --- | --- | --- | +| CRLF | LF | CR | +| '\n\r' | '\n' | '\r' | + +因此,文本文件在不同系统下创建和使用时就会出现不兼容的问题。 + + +解决方案如下: + + +设置全局 git 文本换行 +```bash +$ git config --global core.autocrlf false +``` +注意:git 全局配置之后,你需要重新拉取代码。 + +如您使用的是vscode编辑器,解决方案如下: + + +在编辑器右下角将`CRLF`手动更改为`LF` + +此方法仅可修改当前文件的换行符,使用vscode新建文件换行符还为`CRLF`,可在`settings.json`中增加以下配置 + +``` +"files.eol": "\n", +``` + + +参考: + +- [Delete `␍`eslint(prettier/prettier) 错误的解决方案](https://juejin.cn/post/6844904069304156168) +- [配置 Git 处理行结束符](https://docs.github.com/cn/github/getting-started-with-github/configuring-git-to-handle-line-endings) diff --git a/site/versioned_docs/version-3.0.0/faq/npm_problem.md b/site/versioned_docs/version-3.0.0/faq/npm_problem.md new file mode 100644 index 000000000000..efcced39ae60 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/faq/npm_problem.md @@ -0,0 +1,45 @@ +# 常见 npm 问题 + +## 1、不希望生成 package-lock.json + + +在某些时候,锁版本不是特别好用,反而会出现不少奇怪的问题,我们会禁用 npm 的生成 `package-lock.json` 文件的功能。 + + +可以输入下面的命令。 +```bash +$ npm config set package-lock false +``` + +## 2、Maximum call stack size exceeded 报错 + + +一般在 npm install 之后,再 npm install 某个包导致的。 + + +解法: + + +- 1、删除 node_modules +- 2、删除 package-lock.json +- 3、重新 npm install + + + +如果还有问题,可以尝试使用 node v14/npm6 重试。 + + +## 3、Python/Canvas 报错 + + +出现在使用 node v15/npm7 安装 jest 模块。 + + +比如: +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01fctCcQ2191p8aMfDd_!!6000000006941-2-tps-1623-295.png) + + +解决方案:npm i 时添加 `--legacy-peer-deps` 参数。 + + +原因:测试框架 Jest 依赖 jsdom,npm7 会自动安装其 peerDependencies 中依赖的 canvas 包, 而 canvas 的安装编译需要有python3环境。 \ No newline at end of file diff --git a/site/versioned_docs/version-3.0.0/faq/ts_problem.md b/site/versioned_docs/version-3.0.0/faq/ts_problem.md new file mode 100644 index 000000000000..0319f0740116 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/faq/ts_problem.md @@ -0,0 +1,106 @@ +# 常见 TS 问题 + + +TS 有很多编译静态检查,比如类型不一致,对象未定义等,默认情况下是最佳的,希望用户合理考虑编码风格和习惯,谨慎开关配置,享受 TS 静态检查带来的好处。 + + +## 依赖包定义错误 + + +如果依赖包和项目本身的 TS 版本不一致,在编译时会出现错误。 + + +可以在 `tsconfig.json` 关闭依赖包的检查。 + +```typescript +{ + "compilerOptions": { + "skipLibCheck": true + }, +} +``` + + +## TS2564 初始化未赋值错误 + + +错误如下: + +```yaml +error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor. +``` +原因为开启了 TS 的初始化属性检查,如果没有初始化赋值就会报错。 + + +处理方法: + + +第一种:移除 tsconfig.json 的检查规则 + +```json +{ + "strictPropertyInitialization": false // 或者移除 +} +``` + +第二种:属性加感叹号 + +```typescript +export class HomeController { + @Inject() + userService!: UserService; +} +``` + + +## TS6133 对象声明未使用错误 + + +错误如下: +```yaml +error TS6133: 'app' is declared but its value is never read. +``` +原因为开启了 TS 的对象未使用检查,如果声明了但是没有使用就会报错。 + + +处理方法: + + +第一种:移除未定义的变量 + + +第二种:移除 tsconfig.json 的检查规则 +```json +{ + "compilerOptions": { + "noUnusedLocals": false + }, +} +``` + + +## tsconfig 中定义 typings 不生效 + + +在 tsconfig.json 中,如果定义了 typeRoots,且定义了 include,如果 include 中不包含 typeRoot 中的内容,则会在 dev/build 时报错。 + + +此为 ts/ts-node 的问题,issue 见 [#782](https://github.com/TypeStrong/ts-node/issues/782) [#22217](https://github.com/microsoft/TypeScript/issues/22217) + + +比如: +```json +"typeRoots": [ + "./node_modules/@types", + "./typings" +], +"include": [ + "src", + "typings" +], +"exclude": [ + "dist", + "node_modules" +], +``` +上述,如果 include 中不写 typings,则会在 dev/build 时找不到定义而报错。 diff --git a/site/versioned_docs/version-3.0.0/guard.md b/site/versioned_docs/version-3.0.0/guard.md new file mode 100644 index 000000000000..f504901b0cbb --- /dev/null +++ b/site/versioned_docs/version-3.0.0/guard.md @@ -0,0 +1,249 @@ +# 守卫 + +从 v3.6.0 开始,Midway 提供守卫能力。 + +守卫会根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。 + +普通的应用程序中,一般会在中间件中处理这些逻辑,但是中间件的逻辑过于通用,同时也无法很优雅的去和路由方法进行结合,为此我们在中间件之后,进入路由方法之前设计了守卫,可以方便的进行方法鉴权等处理。 + +下面的代码,我们将以 `@midwayjs/koa` 举例。 + + + +## 编写守卫 + + +一般情况下,我们会在 `src/guard` 文件夹中编写守卫。 + + +创建一个 `src/guard/auth.guard.ts` ,用于验证路由是否能被用户访问。 + +``` +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ ├── user.controller.ts +│ │ └── home.controller.ts +│ ├── interface.ts +│ ├── guard +│ │ └── auth.guard.ts +│ └── service +│ └── user.service.ts +├── test +├── package.json +└── tsconfig.json +``` + + +Midway 使用 `@Guard` 装饰器标识守卫,示例代码如下。 + + +```typescript +import { IMiddleware, Guard, IGuard } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Guard() +export class AuthGuard implements IGuard { + async canActivate(context: Context, supplierClz, methodName: string): Promise { + // ... + } +} +``` + +`canActivate` 方法用于在请求中验证是否可以访问后续的方法,当返回 true 时,后续的方法会被执行,当 `canActivate` 返回 false 时,会抛出 403 错误码。 + + + +## 使用守卫 + +守卫可以被应用到不同的框架上,在 http 下,可以应用到全局,Controller 和方法上,在其他的 Framework 实现中,仅能在方法上使用。 + + + +### 路由守卫 + +在写完守卫之后,我们需要把它应用到各个控制器路由之上。 + +使用 `UseGuard` 装饰器,我们可以应用到类和方法上。 + +```typescript +import { Controller } from '@midwayjs/core'; +import { AuthGuard } from '../guard/auth.guard'; + +@UseGuard(AuthGuard) +@Controller('/') +export class HomeController { + +} +``` + + +在方法上应用守卫。 + +```typescript +import { Controller, Get } from '@midwayjs/core'; +import { ReportMiddleware } from '../middleware/report.middlweare'; +import { AuthGuard } from '../guard/auth.guard'; + +@Controller('/') +export class HomeController { + + @UseGuard(AuthGuard) + @Get('/', { middleware: [ ReportMiddleware ]}) + async home() { + } +} +``` + +也可以传入数组。 + +```typescript +@UseGuard([AuthGuard, Auth2Guard]) +``` + + + +### 全局守卫 + + +我们需要在应用启动前,加入当前框架的守卫列表中,`useGuard` 方法,可以把守卫加入到守卫列表中。 + +```typescript +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { AuthGuard } from './guard/auth.guard'; + +@Configuration({ + imports: [koa] + // ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + this.app.useGuard(AuthGuard); + } +} + +``` + +同理可以添加多个守卫。 + +```typescript +async onReady() { + this.app.useGuard([AuthGuard, Auth2Guard]); +} +``` + + + +## 自定义错误 + +默认情况下,守卫的 `canActivate` 方法当返回 false 时,框架会抛出 403 错误(`ForbiddenError`)。 + +你也可以在守卫中自行决定需要抛出的错误。 + +```typescript +import { IMiddleware, Guard, IGuard, httpError } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Guard() +export class AuthGuard implements IGuard { + async canActivate(context: Context, supplierClz, methodName: string): Promise { + // ... + if (methodName ==='xxx') { + throw new httpError.ForbiddenError(); + } + + return true; + } +} +``` + +:::tip + +注意全局错误处理器也会拦截守卫抛出的错误。 + +::: + + + +## 和中间件的区别 + +守卫会在全局中间件 **之后**,路由方法业务逻辑 **之前** 执行。 + +中间件一般编写通用的处理逻辑,比如登录,用户识别,安全校验等,而守卫由于在路由内部,更适合做基于路由的权限控制。 + +中间件中虽然有路由信息,但是无法明确得知具体进入的是哪个实际的路由控制器(除非额外查询匹配),而守卫已经进入了路由方法,在性能方面有比较大的优势。 + + + +## 基于角色的鉴权示例 + +一般情况下,我们会把方法访问和角色关联起来,下面我们来简单实现一个基于用户角色的访问控制。 + +首先,我们定义一个 `@Role` 装饰器,用于设定方法的访问权限。 + +```typescript +// src/decorator/role.decorator.ts +import { savePropertyMetadata } from '@midwayjs/core'; + +export const ROLE_META_KEY = 'role:name' + +export function Role(roleName: string | string[]): MethodDecorator { + return (target, propertyKey, descriptor) => { + roleName = [].concat(roleName); + // 只保存元数据 + savePropertyMetadata(ROLE_META_KEY, roleName, target, propertyKey); + }; +} +``` + +编写一个守卫,用于角色鉴权。 + +```typescript +import { IMiddleware, Guard, IGuard, getPropertyMetadata } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { ROLE_META_KEY } from '../decorator/role.decorator.ts'; + +@Guard() +export class AuthGuard implements IGuard { + async canActivate(context: Context, supplierClz, methodName: string): Promise { + // 从类元数据上获取角色信息 + const roleNameList = getPropertyMetadata(ROLE_META_KEY, supplierClz, methodName); + if (roleNameList && roleNameList.length && context.user.role) { + // 假设中间件已经拿到了用户角色信息,保存到了 context.user.role 中 + // 直接判断是否包含该角色 + return roleNameList.includes(context.user.role); + } + + return false; + } +} +``` + +在路由上使用该守卫。 + +```typescript +import { Controller, Get } from '@midwayjs/core'; +import { ReportMiddleware } from '../middleware/report.middlweare'; +import { AuthGuard } from '../guard/auth.guard'; + +@UseGuard(AuthGuard) +@Controller('/user') +export class HomeController { + + // 只允许 admin 访问 + @Role(['admin']) + @Get('/getUserRoles') + async getUserRoles() { + // ... + } +} +``` + +只有当 `ctx.user.role` 返回了 `admin` 的时候,才会被允许访问 `/getUserRoles` 路由。 diff --git a/site/versioned_docs/version-3.0.0/hooks/api.md b/site/versioned_docs/version-3.0.0/hooks/api.md new file mode 100644 index 000000000000..cf876b47ad9e --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/api.md @@ -0,0 +1,506 @@ +# 接口开发 + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## 路由 + +在 Midway Hooks 中,你可以通过 `@midwayjs/hooks` 提供的 `Api()` 函数来快速创建接口。 + +Hello World 示例: + +```ts title="/src/hello.ts" +import { + Api, + Get, +} from '@midwayjs/hooks'; + +export default Api( + Get(), // Http Path: /api/hello, + async () => { + return 'Hello World!'; + } +); +``` + +一个 API 接口由以下部分组成: + +- `Api()`:定义接口函数 +- `Get(path?: string)`:指定 Http 触发器,指定请求方法为 GET,可选参数 `path` 为接口路径,不指定路径的情况下会根据`函数名 + 文件名`生成路径,默认带有 `/api` 前缀 +- `Handler: async (...args: any[]) => { ... }`:用户逻辑,处理请求并返回结果 + +你也可以指定路径,例子如下。 + +```ts title="/src/hello.ts" +import { + Api, + Get, +} from '@midwayjs/hooks'; + +export default Api( + Get('/hello'), // Http Path: /hello, + async () => { + return 'Hello World!'; + } +); +``` + +## 请求上下文(Context / Request / Response) + +你可以通过 `@midwayjs/hooks` 提供的 `useContext` 来获取请求上下文对象。 + +以使用 [Koa](https://koajs.com/) 框架为例,那么 `useContext` 将返回 Koa 的 [Context](https://koajs.com/#context) 对象。 + +基础示例: + +1. 获取请求 Method 和 Path + +```ts +import { + Api, + Get, + useContext, +} from '@midwayjs/hooks'; +import { Context } from '@midwayjs/koa'; + +export default Api(Get(), async () => { + const ctx = useContext(); + return { + method: ctx.method, + path: ctx.path, + }; +}); +``` + +2. 设置返回的 Header + +```ts +import { + Api, + Get, + useContext, +} from '@midwayjs/hooks'; + +export default Api(Get(), async () => { + const ctx = useContext(); + ctx.set('X-Powered-By', 'Midway'); + return 'Hello World!'; +}); +``` + +同时我们也可以通过 `SetHeader()` 来设置 Header。 + +## Http 触发器 + +| 触发器 | 注释 | +| ------------------------ | --------------------------- | +| `All(path?: string)` | 接受所有 Http Method 的请求 | +| `Get(path?: string)` | 接受 GET 请求 | +| `Post(path?: string)` | 接受 POST 请求 | +| `Put(path?: string)` | 接受 PUT 请求 | +| `Delete(path?: string)` | 接受 DELETE 请求 | +| `Patch(path?: string)` | 接受 PATCH 请求 | +| `Head(path?: string)` | 接受 HEAD 请求 | +| `Options(path?: string)` | 接受 OPTIONS 请求 | + +## 请求 Request + +### 传递参数 Data + +在 Midway Hooks 中,接口的入参就是声明函数的参数。 + +基础示例如下: + +```ts +import { + Api, + Post, +} from '@midwayjs/hooks'; + +export default Api( + Post(), // Http Path: /api/say, + async (name: string) => { + return `Hello ${name}!`; + } +); +``` + +你可以用两种方式来调用接口。 + +1. 全栈项目:基于零 Api,导入接口并调用 +2. 手动调用:使用 fetch 在 Http 下,`Handler(...args: any[])` 的入参,可以在手动请求时通过设置 Http Body 的 args 参数来传递参数。 + + + + +```ts +import say from './api'; + +const response = await say('Midway'); +console.log(response); // Hello Midway! +``` + + + + + +```ts +fetch('/api/say', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + args: ['Midway'], + }), +}) + .then((res) => res.text()) + .then((res) => console.log(res)); // Hello Midway! +``` + + + + +### 查询参数 Query + +查询参数可以实现在 URL 上传递参数的方式,使用该功能时,必须通过 `Query` 声明类型。 + +如果希望接口路径是 `/articles?page=0&limit=10`,可以这样写。 + +```ts +import { + Api, + Get, + Query, + useContext, +} from '@midwayjs/hooks'; + +export default Api( + Get(), + Query<{ + page: string; + limit: string; + }>(), + async () => { + const ctx = useContext(); + return { + page: ctx.query.page, + limit: ctx.query.limit, + }; + } +); +``` + +前端调用 + + + + +```ts +import getArticles from './api'; +const response = await getArticles({ + query: { page: '0', limit: '10' }, +}); +console.log(response); // { page: '0', limit: '10' } +``` + + + + + +```ts +fetch('/api/articles?page=0&limit=10') + .then((res) => res.json()) + .then((res) => console.log(res)); // { page: '0', limit: '10' } +``` + + + + +### 路径参数 Params + +路径参数可以实现动态路径和从路径中获取参数的功能。使用该功能时,必须手动设置路径,并通过 `Params` 声明类型。 + +如果希望接口路径是 `/article/100`,并获取 id 为 `100` 的值,可以这样写: + +```ts +import { + Api, + Get, + Params, + useContext, +} from '@midwayjs/hooks'; + +export default Api( + Get('/article/:id'), + Params<{ id: string }>(), + async () => { + const ctx = useContext(); + return { + article: ctx.params.id, + }; + } +); +``` + +前端调用 + + + + +```ts +import getArticle from './api/article'; +const response = await getArticle({ + params: { id: '100' }, +}); +console.log(response); // { article: '100' } +``` + + + + + +```ts +fetch('/article/100') + .then((res) => res.json()) + .then((res) => console.log(res)); // { article: '100' } +``` + + + + +### 请求头 Headers + +请求头可以实现通过 Http Headers 传递参数的功能,使用该功能时,必须通过 `Headers` 声明类型。 + +如果希望请求 `/auth`,并在 `Request Headers` 中 传递 token,可以这样写: + +```ts +import { + Api, + Get, + Headers, + useContext, +} from '@midwayjs/hooks'; + +export default Api( + Get('/auth'), + Headers<{ token: string }>(), + async () => { + const ctx = useContext(); + return { + token: ctx.headers.token, + }; + } +); +``` + +前端调用 + + + + +```ts +import getAuth from './api/auth'; +const response = await getAuth({ + headers: { token: '123456' }, +}); +console.log(response); // { token: '123456' } +``` + + + + + +```ts +fetch('/auth', { + headers: { + token: '123456', + }, +}) + .then((res) => res.json()) + .then((res) => console.log(res)); // { token: '123456' } +``` + + + + +## 响应 Response + +### 状态码 HttpCode + +支持 `HttpCode(status: number)` + + + + +```ts +import { + Api, + Get, + HttpCode, +} from '@midwayjs/hooks'; + +export default Api( + Get(), + HttpCode(201), + async () => { + return 'Hello World!'; + } +); +``` + + + + + +```ts +import { + Api, + Get, + useContext, +} from '@midwayjs/hooks'; + +export default Api(Get(), async () => { + const ctx = useContext(); + ctx.status = 201; + return 'Hello World!'; +}); +``` + + + + +### 响应头 SetHeader + +支持 `SetHeader(key: string, value: string)` + + + + +```ts +import { + Api, + Get, + SetHeader, +} from '@midwayjs/hooks'; + +export default Api( + Get(), + SetHeader('X-Powered-By', 'Midway'), + async () => { + return 'Hello World!'; + } +); +``` + + + + + +```ts +import { + Api, + Get, + useContext, +} from '@midwayjs/hooks'; + +export default Api(Get(), async () => { + const ctx = useContext(); + ctx.set('X-Powered-By', 'Midway'); + return 'Hello World!'; +}); +``` + + + + +### 重定向 Redirect + +支持: `Redirect(url: string, code?: number = 302)` + + + + +```ts +import { + Api, + Get, + Redirect, +} from '@midwayjs/hooks'; + +export default Api( + Get('/demo'), + Redirect('/hello'), + async () => {} +); +``` + + + + + +```ts +import { + Api, + Get, + useContext, +} from '@midwayjs/hooks'; + +export default Api( + Get('/demo'), + async () => { + const ctx = useContext(); + ctx.redirect('/hello'); + } +); +``` + + + + +### 返回值类型 ContentType + +支持: `ContentType(type: string)`。 + + + + +```ts +import { + Api, + Get, + ContentType, +} from '@midwayjs/hooks'; + +export default Api( + Get(), + ContentType('text/html'), + async () => { + return '

Hello World!

'; + } +); +``` + +
+ + + +```ts +import { + Api, + Get, + ContentType, +} from '@midwayjs/hooks'; + +export default Api( + Get(), + ContentType('text/html'), + async () => { + return '

Hello World!

'; + } +); +``` + +
+
diff --git a/site/versioned_docs/version-3.0.0/hooks/builtin-hooks.md b/site/versioned_docs/version-3.0.0/hooks/builtin-hooks.md new file mode 100644 index 000000000000..698dcdf2820f --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/builtin-hooks.md @@ -0,0 +1,104 @@ +# Hooks + +Midway Hooks 可以通过使用 `Hooks` 函数来获取运行时上下文。 + +## 语法 + +Hooks 需要在 Api 接口中使用。 + +有效的例子: + +```ts +import { + Api, + Get, + useContext, +} from '@midwayjs/hooks'; +import { Context } from '@midwayjs/koa'; + +export default Api(Get(), async () => { + const ctx = useContext(); + console.log(ctx.method); + // ... +}); +``` + +无效例子: + +```ts +import { useContext } from '@midwayjs/hooks'; + +const ctx = useContext(); // will throw error +``` + +## 支持的 Hooks + +### useContext + +`useContext()` 函数将返回本次请求相关的上下文,返回的 `Context` 与底层使用的框架决定。 + +以使用 [Koa](https://koajs.com/) 框架为例,那么 `useContext` 将返回 Koa 的 [Context](https://koajs.com/#context) 对象。 + +以获取请求 Method 和 Path 为例。 + +```ts +import { + Api, + Get, + useContext, +} from '@midwayjs/hooks'; +import { Context } from '@midwayjs/koa'; + +export default Api(Get(), async () => { + const ctx = useContext(); + return { + method: ctx.method, + path: ctx.path, + }; +}); +``` + +你可以通过泛型来标注当前上下文的类型。 + +```ts +// Koa +import { Context } from '@midwayjs/koa'; +const ctx = useContext(); + +// FaaS +import { Context } from '@midwayjs/faas'; +const ctx = useContext(); +``` + +## 创建可复用的 Hooks + +你可以创建可复用的 Hooks,以便在多个接口中使用。 + +```ts +import { + Api, + Get, + useContext, +} from '@midwayjs/hooks'; +import { Context } from '@midwayjs/koa'; + +function useIp() { + const ctx = useContext(); + return ctx.ip; +} + +export default Api(Get(), async () => { + const ip = useIp(); + return { + ip, + }; +}); +``` + +一体化调用: + +```ts +import getIp from './api'; +const { ip } = await getIp(); +console.log(ip); // 127.0.0.1 +``` diff --git a/site/versioned_docs/version-3.0.0/hooks/client.md b/site/versioned_docs/version-3.0.0/hooks/client.md new file mode 100644 index 000000000000..b832dc86a19a --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/client.md @@ -0,0 +1,166 @@ +# 前端请求客户端 + +在 Midway Hooks 的全栈应用中,我们使用 `@midwayjs/rpc` 作为默认的请求客户端。所有生成的接口都会通过 `@midwayjs/rpc` 来调用服务端。 + +## 配置 + +`@midwayjs/rpc` 提供了 `setupHttpClient` 方法来配置请求客户端(📢 `setupHttpClient` 应放置于前端代码的入口处。)。 + +支持的配置项如下: + +```ts +type SetupOptions = { + baseURL?: string; + withCredentials?: boolean; + fetcher?: Fetcher; + middleware?: Middleware[]; +}; + +type Fetcher = ( + req: HttpRequestOptions, + options: SetupOptions +) => Promise; + +type Middleware = ( + ctx: Context, + next: () => Promise +) => void; + +type Context = { + req: HttpRequestOptions; + res: any; +}; + +type HttpRequestOptions = { + url: string; + method: HttpMethod; + data?: { + args: any[]; + }; + + // query & headers + query?: Record; + headers?: Record; +}; +``` + +### baseURL: string + +设置请求的基础 URL,默认为 `/`。 + +```ts +import { setupHttpClient } from '@midwayjs/rpc'; + +setupHttpClient({ + baseURL: + process.env.NODE_ENV === + 'development' + ? 'http://localhost:7001' + : 'https://api.example.com', +}); +``` + +### withCredentials: boolean + +默认为 `false`。具体可参考:[MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/withCredentials) + +```ts +import { setupHttpClient } from '@midwayjs/rpc'; + +setupHttpClient({ + withCredentials: true, +}); +``` + +### fetcher: Fetcher + +`@midwayjs/rpc` 默认使用 [redaxios](https://github.com/developit/redaxios) 作为请求客户端,一个遵循 axios api 的 mini 客户端。 + +通过设置 `fetcher`,可以替换默认的请求客户端。此处以使用 `axios` 作为默认的请求客户端为例。 + +```ts +import axios from 'axios'; +import { setupHttpClient } from '@midwayjs/rpc'; +import type { Fetcher } from '@midwayjs/rpc'; + +const fetcher: Fetcher = async ( + req, + options +) => { + const response = await axios({ + method: req.method, + url: req.url, + data: req.data, + params: req.query, + headers: req.headers, + baseURL: options.baseURL, + withCredentials: + options.withCredentials, + }); + return response.data; +}; + +setupHttpClient({ fetcher }); +``` + +### middleware: Middleware[] + +在 `@midwayjs/rpc` 中,我们可以设置中间件来用于打印参数,返回值处理错误等。 + +以打印当前请求的地址与返回值为例: + +```ts +import { setupHttpClient } from '@midwayjs/rpc'; +import type { Middleware } from '@midwayjs/rpc'; + +const logger: Middleware = async ( + ctx, + next +) => { + console.log(`<-- ${ctx.req.url}`); + await next(); + console.log( + `--> ${ctx.req.url} ${ctx.res}` + ); +}; + +setupHttpClient({ + middleware: [logger], +}); +``` + +你也可以用于统一处理错误: + +使用默认 `fetcher` 的情况下,`err` 类型参考:[Axios Response Schema](https://axios-http.com/docs/res_schema)。 + +```ts +import { setupHttpClient } from '@midwayjs/rpc'; +import type { Middleware } from '@midwayjs/rpc'; + +const ErrorHandler: Middleware = async ( + ctx, + next +) => { + try { + await next(); + } catch (err) { + switch (err.status) { + case 401: + location.href = '/login'; + break; + case 500: + alert('Internal Server Error'); + break; + default: + alert( + `Unknown Error, status: ${err.status}` + ); + break; + } + } +}; + +setupHttpClient({ + middleware: [ErrorHandler], +}); +``` diff --git a/site/versioned_docs/version-3.0.0/hooks/component.md b/site/versioned_docs/version-3.0.0/hooks/component.md new file mode 100644 index 000000000000..73bded28fe25 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/component.md @@ -0,0 +1,59 @@ +# 使用 Midway 组件 + +Midway 提供了一系列的组件,包含 Cache / Http / Redis 等。 +而在 Midway Hooks 中,我们可以直接使用 Midway 组件,来快速实现功能。 + +## 引入组件 + +Midway Hooks 在 `configuration.ts` 中使用 `createConfiguration()` 来配置项目,其 Api 与 `@midwayjs/decorator` 提供的 `@Configuration()` 一致。 + +以 `@midwayjs/cache` 组件为例: + +```ts +import { + createConfiguration, + hooks, +} from '@midwayjs/hooks'; +import * as Koa from '@midwayjs/koa'; +import { join } from 'path'; +import * as cache from '@midwayjs/cache'; + +export default createConfiguration({ + imports: [cache, Koa, Hooks()], + importConfigs: [ + join(__dirname, 'config'), + ], +}); +``` + +你可以通过 `imports` 来导入组件,`importConfigs` 来导入配置文件。 + +## 使用组件 + +在 `@midwayjs/cache` 中,提供了 `CacheManager` 类来操作缓存。 + +在 Midway Hooks 中,你可以通过 `@midwayjs/hooks` 提供的 `useInject(class)` 来在运行时获取类的实例。 + +```ts +import { + Api, + Get, + useInject, +} from '@midwayjs/hooks'; +import { CacheManager } from '@midwayjs/cache'; + +export default Api(Get(), async () => { + const cache = await useInject( + CacheManager + ); + + await cache.set('name', 'Midway'); + const result = await cache.get( + `name` + ); + + return `Hello ${result}!`; +}); +``` + +这里的 `useInject(CacheManager)` 与 `@Inject() cache: CacheManager` 的功能是一致的。 diff --git a/site/versioned_docs/version-3.0.0/hooks/config.md b/site/versioned_docs/version-3.0.0/hooks/config.md new file mode 100644 index 000000000000..5bc6c02688c7 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/config.md @@ -0,0 +1,40 @@ +# 项目配置 + +我们通过项目根目录下的 `midway.config.ts` 来配置项目,具体的配置项如下。 + +> 如果是纯接口项目,因为需要在生成环境读取配置,因此请使用 JavaScript,配置文件名: `midway.config.js` + +## source: string + +配置后端根目录,纯服务接口下默认为 `./src`,全栈应用下默认为 `./src/api`。 + +## routes: RouteConfig[] + +启用文件系统路由并配置,默认为 `undefined`。具体格式参考 [简易模式 & 文件系统路由](./file-route)。 + +## dev.ignorePattern: IgnorePattern + +配置全栈应用下,本地开发的哪些请求应该忽略,不进入服务端处理。 + +## build.outDir: string + +配置全栈应用的输出目录,默认为 `./dist`。 + +## vite: ViteConfig + +仅 `import { defineConfig } from '@midwayjs/hooks-kit'` 时可用。 + +配置全栈应用下 Vite 的配置,具体配置项参考 [Vite](https://vitejs.dev/config/)。 + +例子: + +```ts +import react from '@vitejs/plugin-react'; +import { defineConfig } from '@midwayjs/hooks-kit'; + +export default defineConfig({ + vite: { + plugins: [react()], + }, +}); +``` diff --git a/site/versioned_docs/version-3.0.0/hooks/cors.md b/site/versioned_docs/version-3.0.0/hooks/cors.md new file mode 100644 index 000000000000..0397168c1017 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/cors.md @@ -0,0 +1,54 @@ +# 跨域 CORS + +在 Midway Hooks 中,可以通过 [@koa/cors](https://github.com/koajs/cors) 来配置跨域功能。 + +## 使用方法 + +安装 `@koa/cors` 依赖。 + +``` +npm install @koa/cors +``` + +在 `configuration.ts` 启用 `@koa/cors` 中间件。 + +```ts +import { + createConfiguration, + hooks, +} from '@midwayjs/hooks'; +import * as Koa from '@midwayjs/koa'; +import cors from '@koa/cors'; + +export default createConfiguration({ + imports: [ + Koa, + hooks({ + // highlight-start + middleware: [ + cors({ origin: '*' }), + ], + // highlight-end + }), + ], +}); +``` + +支持的[配置项](https://github.com/koajs/cors#corsoptions)如下: + +```javascript +/** + * CORS middleware + * + * @param {Object} [options] + * - {String|Function(ctx)} origin `Access-Control-Allow-Origin`, default is request Origin header + * - {String|Array} allowMethods `Access-Control-Allow-Methods`, default is 'GET,HEAD,PUT,POST,DELETE,PATCH' + * - {String|Array} exposeHeaders `Access-Control-Expose-Headers` + * - {String|Array} allowHeaders `Access-Control-Allow-Headers` + * - {String|Number} maxAge `Access-Control-Max-Age` in seconds + * - {Boolean|Function(ctx)} credentials `Access-Control-Allow-Credentials`, default is false. + * - {Boolean} keepHeadersOnError Add set headers to `err.header` if an error is thrown + * @return {Function} cors middleware + * @api public + */ +``` diff --git a/site/versioned_docs/version-3.0.0/hooks/debug.md b/site/versioned_docs/version-3.0.0/hooks/debug.md new file mode 100644 index 000000000000..ffd45768cb48 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/debug.md @@ -0,0 +1,29 @@ +# 调试 + +得益于编辑器的支持,我们可以快速的在本地调试应用。 + +## VSCode + +### JavaScript Debug Terminal + +在 VSCode 中创建 JavaScript Debug Terminal。 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/98602/1622789601759-d2634846-49f7-4487-be6f-0dc9e5f80082.png#clientId=u3a1b2f6d-ebe0-4&from=paste&height=192&id=p5BOe&margin=%5Bobject%20Object%5D&name=image.png&originHeight=192&originWidth=375&originalType=binary&size=31856&status=done&style=none&taskId=u7286159b-9369-4d17-8a6a-c43a6f52556&width=375) + +在命令行中运行命令(如 `npm start`),将自动启用调试模式。 + +### Debug Scripts + +打开 `package.json`,查看 `scripts` 上方的 `debug` 按钮 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/98602/1622789617835-64b2099a-6b94-41c4-81fa-4f0bb0763ebb.png#clientId=u7ee4f0d0-4c66-4&from=paste&height=225&id=u459844f5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=225&originWidth=565&originalType=binary&size=26636&status=done&style=none&taskId=u3838b111-c93e-41e0-81ce-01c1bdd6ad4&width=565) + +选择 `start` 命令,既可正常的启动调试模式 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/98602/1622789623261-57851b50-421e-45fa-9dd9-95ac7d48776e.png#clientId=u7ee4f0d0-4c66-4&from=paste&height=170&id=ue315d401&margin=%5Bobject%20Object%5D&name=image.png&originHeight=170&originWidth=427&originalType=binary&size=19905&status=done&style=none&taskId=u8b079aa2-8376-4014-b48b-ed27ef66da6&width=427) + +## Jetbrains (WebStorm/IDEA...) + +打开 `package.json`,选择你要执行的 `scripts` ,并点击 `debug` 按钮,即可启动本地调试。 + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/98602/1622789628840-eb403a2a-a864-4fd6-8f57-3f576c9b3417.png#clientId=u7ee4f0d0-4c66-4&from=paste&height=176&id=uc2a06ce8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=176&originWidth=548&originalType=binary&size=28656&status=done&style=none&taskId=ucb4c5c34-6e56-47c9-a724-4ed700dce9d&width=548) diff --git a/site/versioned_docs/version-3.0.0/hooks/deploy.md b/site/versioned_docs/version-3.0.0/hooks/deploy.md new file mode 100644 index 000000000000..1389ebdbb26c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/deploy.md @@ -0,0 +1,142 @@ +# 部署 + +Midway Hooks 支持 Api Server 与一体化两种模式。 + +## Api Server 部署 + +Api Server 部署可以参考:[启动和部署](/docs/deployment)。 + +如果使用单文件部署,可以参考示例:[hooks-api-bundle-starter](https://github.com/midwayjs/hooks/blob/main/examples/api-bundle/readme.md) + +## 一体化部署 + +一体化的构建产物中包含前后端,根据部署的难易程度,可以分为以下几类。 + +- 前后端部署在同一服务器上,由后端托管 HTML & 静态资源 & 提供接口 +- 静态资源部署至 CDN,后端托管 HTML & 提供接口 +- 静态资源部署至 CDN,HTML 由单独的服务托管(CDN / Nginx / etc.),后端仅提供接口 + +接下来我将介绍三种部署模式如何落地,优势及存在的问题。 + +### 前后端部署在同一服务器上 + +这是全栈套件默认的部署模式。 + +优势:最简单,将打包后的产物直接上传至服务器,启动后即可提供服务 +劣势: + +- 后端服务需要处理 & 发送文件 +- 静态资源不在 CDN,不同地域的访问速度不稳定 + +整体部署架构如图所示: + +![](https://img.alicdn.com/imgextra/i1/O1CN01GYtN9n1T2tbEXWOwf_!!6000000002325-2-tps-2064-648.png) + +### 静态资源部署至 CDN,后端托管 HTML & 提供接口 + +这也是当前前端主流的部署模式。 + +优势: + +- 静态资源由 CDN 托管,保证用户访问速度 +- 后端托管 HTML,确保返回的 HTML 文件是最新的 + +劣势: + +- 后端仍需要托管 HTML,仍需要处理 & 发送文件,且如果服务宕机则页面无法访问 + +整体访问架构如图所示: + +![](https://img.alicdn.com/imgextra/i4/O1CN01ue3LJg1HeernvfxgQ_!!6000000000783-55-tps-267-367.svg) + +#### 指定静态资源公共域名 + +在全栈套件项目中使用时,可以通过设置 `midway.config.ts` 中 `vite.base` 选项,来指定静态资源的公共域名。 + +```ts +import react from '@vitejs/plugin-react'; +import { defineConfig } from '@midwayjs/hooks-kit'; + +export default defineConfig({ + vite: { + plugins: [react()], + base: 'https://cdn.example.com', + }, +}); +``` + +此时访问页面时,静态资源会指向 CDN 的地址。 + +#### 部署静态文件 + +全栈套件项目中,默认的构建目录为 dist,其中 `dist/_clients` 为前端静态资源目录。 + +如下所示: + +``` +dist +├── _client +│   ├── assets +│   │   ├── index.85bb4f15.js +│   │   ├── index.b779b14d.css +│   │   └── vendor.346bc0da.js +│   ├── index.html +│   ├── logo.png +│   └── manifest.json +├── _serve +│   └── index.js +├── book.js +├── configuration.js +├── date.js +├── midway.config.js +└── star.js +``` + +你需要自行将 `_client` 目录下的文件上传至 CDN,而在部署后端时,仍然保留 `_client/index.html` 文件,以供后端托管使用。 + +### 静态资源部署至 CDN,HTML 由单独的服务托管(CDN / Nginx / etc.),后端仅提供接口 + +这也是前端目前主流的部署模式。 + +优势: + +- 后端仅提供 API 接口,不需要处理 & 发送文件 +- 静态资源由 CDN 托管,保证用户访问速度 +- HTML 由单独服务托管,保证访问是页面是最新版本,后端服务宕机不影响页面展示 +- 架构可拓展,可增加更多节点应对意外情况,如在后端前置增加网关节点,在后端服务宕机时切换至备用服务等 + +劣势: + +- 复杂,对 CI / CD 流水线及基建要求高 + +整体访问架构如图所示 + +![](https://img.alicdn.com/imgextra/i1/O1CN01i78JiC1yinvfLq84b_!!6000000006613-55-tps-323-367.svg) + +部署工作流如下: + +![](https://img.alicdn.com/imgextra/i2/O1CN018oAQf71h1QxHtRHYY_!!6000000004217-2-tps-1728-1680.png) + +#### 全栈套件部署指南 + +需要默认禁用全栈套件的 index.html 托管能力,此时全栈套件在构建时不会生成 `index.html` 的托管函数,仅提供 Api 服务。 + +```ts +import { defineConfig } from '@midwayjs/hooks-kit'; + +export default defineConfig({ + static: false, +}); +``` + +在你的 CI / CD 工作流中,需要针对以下文件做单独处理。 + +- index.html:部署至单独的托管服务,如 Nginx / CDN 等,该服务只负责静态页面渲染 +- 静态资源:部署至 CDN,如 Aliyun OSS 等,该服务可以提供静态资源的 CDN 加速 +- Api 服务:部署至你的服务器中 + +最终的域名可能如下: + +- index.html: https://example.com +- 静态资源: https://cdn.example.com +- Api 服务: https://api.example.com 或者 https://example.com/api(需要设置反向代理) diff --git a/site/versioned_docs/version-3.0.0/hooks/file-route.md b/site/versioned_docs/version-3.0.0/hooks/file-route.md new file mode 100644 index 000000000000..5151b4d2efe4 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/file-route.md @@ -0,0 +1,226 @@ +# 简易模式 & 文件系统路由 + +## 简易模式 + +在 Midway Hooks 中,我们提供了一个简易模式,可以使用纯函数来快速创建接口。 + +📢 注意: + +- 简易模式需启用文件路由系统,需要在 `midway.config.js` 中启用 `routes` 配置。 +- 纯函数自动生成的路由仅支持 `GET` 和 `POST` 方法,且全栈应用中,不支持传递 `Query / Params / Header` 参数 +- 简易模式下,仍可以使用 `Api()` 定义路由,且支持手动定义路径,拼接的路径将自动加上 `basePath` + +### Get 请求 + +```ts +import { useContext } from '@midwayjs/hooks'; + +export async function getPath() { + // Get HTTP request context by Hooks + const ctx = useContext(); + return ctx.path; +} +``` + +一体化调用: + +```ts +import { getPath } from './api/lambda'; +const path = await getPath(); +console.log(path); // /api/getPath +``` + +手动调用: + +```ts +fetcher + .get('/api/getPath') + .then((res) => { + console.log(res.data); // /api/getPath + }); +``` + +### Post 请求 + +```ts +import { useContext } from '@midwayjs/hooks'; + +export async function post( + name: string +) { + const ctx = useContext(); + + return { + message: `Hello ${name}!`, + method: ctx.method, + }; +} +``` + +一体化调用: + +```ts +import { post } from './api/lambda'; +const response = await post('Midway'); +console.log(response.data); // { message: 'Hello Midway!', method: 'POST' } +``` + +手动调用: + +```ts +fetch('/api/post', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + args: ['Midway'], + }), +}).then((res) => { + console.log(res.data); // { message: 'Hello Midway!', method: 'POST' } +}); +``` + +### 通过 `Api()` 创建路由 + +简易模式下,我们仍支持通过 `Api()` 创建路由。 + +无效的例子:`Api(Get('/specify_path'))`,简易模式下不支持手动指定路径。 + +有效的例子,导出了两个路由。 + +```ts +import { + Api, + Get, +} from '@midwayjs/hooks'; +import { useContext } from '@midwayjs/hooks'; + +export async function getPath() { + // Get HTTP request context by Hooks + const ctx = useContext(); + return ctx.path; +} + +export default Api(Get(), async () => { + return 'Hello Midway!'; +}); +``` + +## 文件系统路由 + +在 `midway.config.js` 中启用 `routes` 配置即启用文件路由系统 + 简易模式。 + +配置示例如下: + +```ts +import { defineConfig } from '@midwayjs/hooks'; + +export default defineConfig({ + source: './src/apis', + routes: [ + { + baseDir: 'lambda', + basePath: '/api', + }, + ], +}); +``` + +字段解释: + +- source: 后端目录,默认为 `./src/apis`,你也可以指定为 `./src/functions` 等自定义目录 +- routes: 路由配置,默认为数组 + - baseDir: 函数文件夹,文件夹下任意 `.ts` 文件导出的异步函数都会生成为 Api 接口 + - basePath: 生成的 Api 地址前缀 + +### Index 路由 + +我们会将目录下 `index.ts` 文件,作为根路由。 + +- `/lambda/index.ts` → `/` +- `/lambda/about/index.ts` → `/about` + +### 嵌套路由 + +嵌套的文件也将生成嵌套的路由
+ +- `/lambda/about.ts` → `/about` +- `/lambda/blog/index.ts` → `/blog` +- `/lambda/about/contact.ts` → `/about/contact` + +### 导出方法与对应路由 + +默认导出的方法则会生成为根路径,而具名方法则会在路径上拼接函数名。 + +在此以 `/lambda/about.ts` 为例 + +- `export default () => {}` → `/about` +- `export function contact ()` → `/about/contact` + +### 通配路由 + +如果需要生成通配符路由,例如:`/api/*` ,用于匹配 /api、/api/about、/api/about/a/b/c 等。文件名按 `[...file]` 命名即可。 + +📢 推荐在通配路由中,只使用 `export default` 方法导出函数,从而避免不必要的路由冲突 + +示例: + +- `/lambda/[...index].ts` → `/api/*` +- `/lambda/[...user].ts` → `/api/user/*` +- `/lambda/about/[...contact].ts` → `/api/about/contact/*` + +### 路径参数 + +如果需要生成动态路径参数,文件名按 `[file]` 格式命名即可。 + +例子: + +- `/lambda/[name]/project.ts` → `/api/about/:name/project` + - `/about/midwayjs/project` -> `{ name: 'midwayjs' }` +- `/lambda/[type]/[page].ts` → `/api/about/:type/:page` + - `/blog/1` -> `{ type: 'blog', page: '1' }` + - `/articles/3` -> `{ type: 'articles', page: '3' }` + +使用路径参数时,后端接口仅支持使用 `Api()` 开发,并使用 `Params` 标注类型。 + +以 `/lambda/[name]/project.ts` 为例: + +```ts +// lambda/[name]/project.ts +import { + Api, + Get, + Params, + useContext, +} from '@midwayjs/hooks'; + +export default Api( + Get(), + Params<{ name: string }>(), + async () => { + const ctx = useContext(); + return { + name: ctx.params.name, + }; + } +); +``` + +一体化调用: + +```ts +import getProject from './api/[name]/project'; +const response = await getProject({ + params: { name: 'midwayjs' }, +}); +console.log(response); // { name: 'midwayjs' } +``` + +手动调用: + +```ts +fetch('/api/about/midwayjs/project') + .then((res) => res.json()) + .then((res) => console.log(res)); // { name: 'midwayjs' } +``` diff --git a/site/versioned_docs/version-3.0.0/hooks/fullstack.md b/site/versioned_docs/version-3.0.0/hooks/fullstack.md new file mode 100644 index 000000000000..eb47aa20b9f8 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/fullstack.md @@ -0,0 +1,37 @@ +# 全栈套件 + +在 Midway Hooks 中,我们提供了 `@midwayjs/hooks-kit` 来快速开发全栈应用。目前我们提供了以下可直接使用的模版: + +- [react](https://github.com/midwayjs/hooks/blob/main/examples/react) +- [vue](https://github.com/midwayjs/hooks/blob/main/examples/vue) +- [prisma](https://github.com/midwayjs/hooks/blob/main/examples/prisma) + +## 命令行界面 + +在使用了 `@midwayjs/hooks-kit` 的项目中,可以在 npm scripts 中使用 hooks 可执行文件,或者通过 `npx hooks` 运行。下面是通过脚手架创建的 Midway 全栈项目中默认的 npm scripts: + +```json +{ + "scripts": { + "dev": "hooks dev", // 启动开发服务器 + "start": "hooks start", // 启动生产服务器,使用前请确保已运行 `npm run build` + "build": "hooks build" // 为生产环境构建产物 + } +} +``` + +在使用命令行时,可以通过命令行参数传入选项,具体选项可以通过 --help 参考。 + +如:`hooks build --help` + +输出: + +``` +Usage: + $ hooks build [root] + +Options: + --outDir [string] output directory (default: dist) + --clean [boolean] clean output directory before build (default: false) + -h, --help Display this message +``` diff --git a/site/versioned_docs/version-3.0.0/hooks/intro.md b/site/versioned_docs/version-3.0.0/hooks/intro.md new file mode 100644 index 000000000000..38aab81d691d --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/intro.md @@ -0,0 +1,145 @@ +# 介绍 + +:::caution + +一体化方案将逐步停止维护,已有项目可以继续使用,新建项目请谨慎选择。 + +::: + +Midway 的一体化方案,是以 Midway Hooks 为主函数式全栈框架,支持四大核心特性:"零" Api & 类型安全 & 全栈套件 & 强大后端。 + + + +## 和标准项目差异 + + + +一体化方案,基于标准项目,在其之上扩展出了一层前端适配层,在复用所有标准项目能力的同时,又可以和前端进行无缝协作开发,即在项目中,既有前端代码,又有 Node 代码。 + + + +## 特性介绍 + +### 零 Api + +在 Midway Hooks 全栈应用中开发的后端接口函数,可以直接导入并调用,无需前后端手写 Ajax 胶水层。以下是一个简单的例子: + +后端代码: + +```ts +import { + Api, + Post, +} from '@midwayjs/hooks'; + +export default Api( + Post(), // Http Path: /api/say, + async (name: string) => { + return `Hello ${name}!`; + } +); +``` + +前端调用: + +```ts +import say from './api'; + +const response = await say('Midway'); +console.log(response); // Hello Midway! +``` + +### 类型安全与运行时安全 + +使用 `@midwayjs/hooks` 提供的 [Validate](./validate.md) 校验器,可以实现从前端到后端的类型安全 + 运行时安全链路。以下是一个简单的例子: + +后端代码: + +```ts +import { + Api, + Post, + Validate, +} from '@midwayjs/hooks'; +import { z } from 'zod'; + +export default Api( + Post('/hello'), + Validate(z.string(), z.number()), + async (name: string, age: number) => { + return `Hello ${name}, you are ${age} years old.`; + } +); +``` + +一体化调用: + +```ts +import hello from './api'; + +try { + await hello(null, null); +} catch (error) { + console.log(error.message); // 'name must be a string' + console.log(error.status); // 422 +} +``` + +整个过程中。 + +- 前端:基于类型,静态校验输入参数,并获取类型提示 +- 后端:校验前端传入参数 +- 数据库等业务逻辑:使用正确的数据 + +通过这种方式,我们可以低成本的实现静态类型安全 + 运行时安全。 + +### 全栈套件 + +在 Midway Hooks 中,我们提供了 `@midwayjs/hooks-kit` 来快速开发全栈应用。 + +你可以通过 `hooks dev` 来启动全栈应用,`hooks build` 来打包全栈应用,同时在服务端你也可以使用 `hooks start` 一键启动应用。 + +解决你在使用全栈应用时的后顾之忧。 + +### 强大后端 + +Midway Hooks 基于 Midway 开发。 + +Midway 是一个有着 8 年历史的 Node.js 框架,具有强大的后端功能,包含 Cache / Redis / Mongodb / Task / Config 等 Web 下常用的组件。 + +而这些你在使用 Midway Hooks 时都可以无缝享受到。 + +## 创建应用 + +Midway Hooks 目前提供了如下模板: + +- 全栈应用 + - [react](https://github.com/midwayjs/hooks/blob/main/examples/react) + - [vue](https://github.com/midwayjs/hooks/blob/main/examples/vue) + - [prisma](https://github.com/midwayjs/hooks/blob/main/examples/prisma) +- Api Server + - [api](https://github.com/midwayjs/hooks/blob/main/examples/api) + +基于指定创建应用命令如下: + +```bash +npx degit https://github.com/midwayjs/hooks/examples/ +``` + +创建 react 模版的全栈应用命令如下: + +```bash +npx degit https://github.com/midwayjs/hooks/examples/react ./hooks-app +``` + +创建 api 模版的应用命令如下: + +```bash +npx degit https://github.com/midwayjs/hooks/examples/api ./hooks-app +``` + +## 下一步 + +- 了解如何开发接口并提供给前端调用:[接口开发](./api.md) +- 如何使用和创建可复用的 Hooks:[Hooks](./builtin-hooks.md) +- 如何在运行时校验用户参数:[校验器](./validate.md) diff --git a/site/versioned_docs/version-3.0.0/hooks/middleware.md b/site/versioned_docs/version-3.0.0/hooks/middleware.md new file mode 100644 index 000000000000..d94fae64f628 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/middleware.md @@ -0,0 +1,175 @@ +# Web 中间件 + +Midway Hooks 支持通过函数 + `useContext()` 来定义 Web 中间件。 + +## 语法 + +中间件仅有 `next` 一个参数,`ctx` 需要通过 `useContext` 获得。你也可以在中间件中使用任意的 `Hooks`。 + +### 基础示例 + +以记录请求日志为例: + +```typescript +import { Context } from '@midwayjs/koa'; +import { useContext } from '@midwayjs/hooks'; + +const logger = async (next: any) => { + const ctx = useContext(); + + console.log( + `<-- [${ctx.method}] ${ctx.url}` + ); + + const start = Date.now(); + await next(); + const cost = Date.now() - start; + + console.log( + `--> [${ctx.method}] ${ctx.url} ${cost}ms` + ); +}; +``` + +## 全局中间件 + +全局中间件在 `configuration.ts` 中定义,此处定义的中间件对所有接口生效。 + +```typescript +import { + hooks, + createConfiguration, +} from '@midwayjs/hooks'; +import logger from './logger'; + +// Global Middleware +export default createConfiguration({ + imports: [ + // highlight-start + hooks({ + middleware: [logger], + }), + // highlight-end + ], +}); +``` + +## 文件级中间件 + +文件级中间件在 Api 文件中定义,通过导出的 `config.middleware`,该中间件对文件内所有 Api 函数生效。 + +```typescript +import { + ApiConfig, + Api, + Get, +} from '@midwayjs/hooks'; +import logger from './logger'; + +// File Level Middleware +// highlight-start +export const config: ApiConfig = { + middleware: [logger], +}; +// highlight-end + +export default Api(Get(), async () => { + return 'Hello World!'; +}); +``` + +## 单函数中间件 + +通过 `Middleware(...middlewares: HooksMiddleware[])` 定义的中间件仅对单个函数生效 + +```ts +import { + Api, + Get, + Middleware, +} from '@midwayjs/hooks'; +import logger from './logger'; + +export default Api( + Get(), + // highlight-start + Middleware(logger), + // highlight-end + async () => { + return 'Hello World!'; + } +); +``` + +## 使用 Koa 中间件 + +你可以在上述的例子中直接传入 Koa 中间件。 + +以 [@koa/cors](https://www.npmjs.com/package/@koa/cors) 为例 + +全局启用: + +```ts +import { + hooks, + createConfiguration, +} from '@midwayjs/hooks'; +import logger from './logger'; +import cors from '@koa/cors'; + +// Global Middleware +export default createConfiguration({ + imports: [ + hooks({ + // highlight-start + middleware: [logger, cors()], + // highlight-end + }), + ], +}); +``` + +文件级别启用: + +```ts +import { + ApiConfig, + Api, + Get, +} from '@midwayjs/hooks'; +import logger from './logger'; +import cors from '@koa/cors'; + +// File Level Middleware +// highlight-start +export const config: ApiConfig = { + middleware: [logger, cors], +}; +// highlight-end + +export default Api(Get(), async () => { + return 'Hello World!'; +}); +``` + +函数级别启用: + +```ts +import { + Api, + Get, + Middleware, +} from '@midwayjs/hooks'; +import logger from './logger'; +import cors from '@koa/cors'; + +export default Api( + Get(), + // highlight-start + Middleware(logger, cors), + // highlight-end + async () => { + return 'Hello World!'; + } +); +``` diff --git a/site/versioned_docs/version-3.0.0/hooks/prisma.md b/site/versioned_docs/version-3.0.0/hooks/prisma.md new file mode 100644 index 000000000000..5933c06eae2b --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/prisma.md @@ -0,0 +1,149 @@ +# Prisma ORM + +在 Midway Hooks 中,我们推荐使用 [Prisma](https://prisma.io/) 来构建数据库,并实现我们静态类型安全的目标。 + +[Prsima](https://www.prisma.io/) 是面向 Node.js & TypeScript 设计的 ORM,它提供了一系列友好的功能(Schema 定义、客户端生成、完全的 TypeScript 支持),可以帮助用户快速构建应用。 + +## Example + +我们提供了一个简单的例子 [hooks-prisma-starter](https://github.com/midwayjs/hooks/blob/main/examples/prisma/README.md),来演示在 Midway Hooks 如何使用 Prisma。 + +下面我也会简单介绍,Midway Hooks 配合 Prisma 开发应用会有多么的简单。 + +### 数据库 Schema + +例子基于 sqlite,数据库 Schema 如下: + +```prisma +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + content String? + published Boolean @default(false) + viewCount Int @default(0) + author User? @relation(fields: [authorId], references: [id]) + authorId Int? +} +``` + +具体的数据库设置 & 初始数据填充工作,参考 [hooks-prisma-starter](https://github.com/midwayjs/hooks/blob/main/examples/prisma/README.md) 文档即可。 + +### 初始化 Prisma + +在项目的 src/api 下新建 prisma 文件,使用如下代码即可初始化 Client。 + +```ts +import { PrismaClient } from '@prisma/client'; + +export const prisma = + new PrismaClient(); +``` + +#### 使用代理镜像 + +Prisma 在安装时会根据平台动态下载可执行文件,如果你的网络环境不好,可以通过环境变量来设置镜像。 + +```bash +PRISMA_ENGINES_MIRROR=https://registry.npmmirror.com/-/binary/prisma/ +``` + +相关 Issue: [mirror prisma](https://github.com/cnpm/mirrors/issues/248) + +### 查询数据 + +以获取所有发布的文章为例,你可以通过生成的 Prisma Client 快速完成操作。 + +后端代码: + +```ts +import { + Api, + Get, +} from '@midwayjs/hooks'; +import { prisma } from './prisma'; + +export default Api(Get(), async () => { + const posts = + await prisma.post.findMany({ + where: { published: true }, + include: { author: true }, + }); + return posts; +}); +``` + +一体化调用: + +```ts +import fetchFeeds from '../api/feeds'; + +fetchFeeds().then((feeds) => { + console.log(feeds); +}); +``` + +### 增加数据 + +以注册登录为例,基于一体化调用 + Prisma 生成的客户端,可以在简单的几行代码中完成所有的工作。 + +包含: + +- 前端类型提示 +- 后端参数校验 +- 数据库操作 + +```ts +import { + Api, + Post, + Validate, +} from '@midwayjs/hooks'; +import { z } from 'zod'; +import { prisma } from './prisma'; + +export const signUp = Api( + Post(), + Validate( + z.string(), + z.string().email() + ), + async ( + name: string, + email: string + ) => { + const result = + await prisma.user.create({ + data: { + name, + email, + }, + }); + return result; + } +); +``` + +一体化调用: + +```ts +import { signUp } from '../api/feeds'; + +signUp('John', 'test@test.com').then( + (user) => { + console.log(user); + } +); +``` + +### 更多示例 + +关于 Prisma 的更多示例,可以参考 [Prisma 官网文档](https://www.prisma.io/)。 diff --git a/site/versioned_docs/version-3.0.0/hooks/safe.md b/site/versioned_docs/version-3.0.0/hooks/safe.md new file mode 100644 index 000000000000..3f918e541796 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/safe.md @@ -0,0 +1,65 @@ +# 静态类型安全 + 运行时安全 + +使用 [Prisma](./prisma.md) 和 `@midwayjs/hooks` 提供的 [Validate](./validate.md) 校验器,可以实现从前端到后端再到数据库的类型安全 + 运行时安全链路。 + +以 [hooks-prisma-starter](https://github.com/midwayjs/hooks/blob/main/examples/fullstack/prisma/README.md) 中的 `POST /api/post` 接口为例,代码如下: + +```ts +import { + Api, + Post, + Validate, +} from '@midwayjs/hooks'; +import { prisma } from './prisma'; +import { z } from 'zod'; + +const PostSchema = z.object({ + title: z.string().min(1), + content: z.string().min(1), + authorEmail: z.string().email(), +}); + +export const createPost = Api( + Post('/api/post'), + Validate(PostSchema), + async ( + post: z.infer + ) => { + const result = + await prisma.post.create({ + data: { + title: post.title, + content: post.content, + author: { + connect: { + email: post.authorEmail, + }, + }, + }, + }); + return result; + } +); +``` + +前端调用: + +```ts +import { createPost } from '../api/post'; + +await createPost({ + title: 'Hello Midway', + content: 'Hello Prisma', + authorEmail: 'test@test.com', +}); +``` + +此时,前端基于 Zod 的 Schema 获取类型提示,后端则使用 `Validate` 校验器进行类型检查,最终调用 `prisma.post.create` 方法来创建用户。 + +整个过程中。 + +- 前端:基于类型,静态校验输入参数,并获取类型提示 +- 后端:校验前端传入参数 +- 数据库:使用正确的数据 + +通过这种方式,我们可以低成本的实现静态类型安全 + 运行时安全。 diff --git a/site/versioned_docs/version-3.0.0/hooks/test.md b/site/versioned_docs/version-3.0.0/hooks/test.md new file mode 100644 index 000000000000..744f4aa11947 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/test.md @@ -0,0 +1,212 @@ +# 测试 + +在 Midway Hooks 中,我们可以快速的对 Http 接口进行测试。 + +## 接口测试 + +此处以 Hello World 为例,我们在 `src/hello.ts` 中,导出了一个接口,代码如下。 + +```ts +import { Api, Get } from '@midwayjs/hooks'; + +export default Api(Get('/hello'), async () => { + return 'Hello World!'; +}); +``` + +在测试中,你可以通过 `@midwayjs/mock` 去启动应用,并调用接口完成测试。 + +### 通过 `@midwayjs/hooks` 调用 + +`@midwayjs/hooks` 提供了 `getApiTrigger(api: ApiFunction)` 方法,可以用于获取触发器。 + +以上面的 `hello` 接口为例,`getApiTrigger(hello)` 将返回: + +```json +{ + "type": "HTTP", + "method": "GET", + "path": "/hello" +} +``` + +在此,我们使用 `@midwayjs/mock` 提供的 `createHttpRequest` 方法来调用接口。`createHttpRequest` 的使用文档可以参考 [supertest](https://github.com/visionmedia/supertest)。 + +```ts +// src/hello.test.ts +import { + close, + createApp, + createHttpRequest, +} from '@midwayjs/mock'; +import { + Framework, + IMidwayKoaApplication, +} from '@midwayjs/koa'; +import { getApiTrigger, HttpTriger } from '@midwayjs/hooks'; +import hello from './hello'; + +describe('test koa with api router', () => { + let app: IMidwayKoaApplication; + + beforeAll(async () => { + app = await createApp(); + }); + + afterAll(async () => { + await close(app); + }); + + test('Hello World', async () => { + const trigger = getApiTrigger(hello); + const response = await createHttpRequest(app) + .get(trigger.path) + .expect(200); + expect(response.text).toBe('Hello World!'); + }); +}); +``` + +### 手动调用 + +手动调用的情况下,需要填入 `Path` 等参数。 + +```ts +test('Hello World', async () => { + const response = await createHttpRequest(app) + .get('/hello') + .expect(200); + expect(response.text).toBe('Hello World!'); +}); +``` + +### 请求参数 Data + +后端代码: + +```ts +import { Api, Post } from '@midwayjs/hooks'; + +export default Api( + Post(), // Http Path: /api/say, + async (name: string) => { + return `Hello ${name}!`; + } +); +``` + +测试代码: + +```ts +test('Hello World', async () => { + const trigger = getApiTrigger(say); + const response = await createHttpRequest(app) + .post(trigger.path) + .send({ args: ['Midway'] }) + .expect(200); + expect(response.text).toBe('Hello Midway!'); +}); +``` + +### 查询参数 Query + +后端代码: + +```ts +import { + Api, + Get, + Query, + useContext, +} from '@midwayjs/hooks'; + +export default Api( + Get('/hello'), + Query<{ name: string }>(), + async () => { + const ctx = useContext(); + return `Hello ${ctx.query.name}!`; + } +); +``` + +测试代码: + +```ts +test('Hello World', async () => { + const trigger = getApiTrigger(hello); + const response = await createHttpRequest(app) + .get(trigger.path) + .query({ name: 'Midway' }) + .expect(200); + expect(response.text).toBe('Hello Midway!'); +}); +``` + +### 路径参数 Params + +后端代码: + +```ts +import { Api, Get, Params, useContext } from '@midwayjs/hooks' + +export default Api( + Get('/article/:id'), + Params<{ id: string }>(, + async () => { + const ctx = useContext() + return { + article: ctx.params.id + } + } +) +``` + +测试代码: + +```ts +test('Get Article', async () => { + const response = await createHttpRequest(app) + .get('/article/1') + .expect(200); + + expect(response.body).toEqual({ article: '1' }); +}); +``` + +### 请求头 Headers + +后端代码: + +```ts +import { + Api, + Get, + Headers, + useContext, +} from '@midwayjs/hooks'; + +export default Api( + Get('/auth'), + Headers<{ token: string }>(), + async () => { + const ctx = useContext(); + return { + token: ctx.headers.token, + }; + } +); +``` + +测试代码: + +```ts +test('Auth', async () => { + const response = await createHttpRequest(app) + .get('/auth') + .set('token', '123456') + .expect(200); + + expect(response.body).toEqual({ token: '123456' }); +}); +``` diff --git a/site/versioned_docs/version-3.0.0/hooks/upload.md b/site/versioned_docs/version-3.0.0/hooks/upload.md new file mode 100644 index 000000000000..24716f2d33bd --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/upload.md @@ -0,0 +1,157 @@ +# 文件上传 + +Midway Hooks 提供了 `@midwayjs/hooks-upload` 并配合 `@midwayjs/upload` 来实现纯函数 + 一体化项目中的文件上传功能。 + +## 起步 + +安装依赖: + +```bash +npm install @midwayjs/upload @midwayjs/hooks-upload +``` + +## 使用 + +### 启用 upload 组件 + +在后端目录的 `configuration.ts` 中启用 `@midwayjs/upload` 组件,具体支持的配置项可查看 [文件上传](/docs/extensions/upload) + +```diff +import { createConfiguration, hooks } from '@midwayjs/hooks'; +import * as Koa from '@midwayjs/koa'; ++ import * as upload from '@midwayjs/upload'; + +/** + * setup midway server + */ +export default createConfiguration({ + imports: [ + Koa, + hooks(), ++ upload + ], + importConfigs: [{ default: { keys: 'session_keys' } }], +}); +``` + +### 创建接口 + +在后端目录下,新建接口文件。 + +```ts +import { Api } from '@midwayjs/hooks'; +import { + Upload, + useFiles, +} from '@midwayjs/hooks-upload'; + +export default Api( + Upload('/api/upload'), + async () => { + const files = useFiles(); + return files; + } +); +``` + +> 一体化调用 + +```tsx +import upload from './api/upload'; + +function Form() { + const [file, setFile] = + React.useState(null); + + const handleSubmit = async ( + e: React.FormEvent + ) => { + e.preventDefault(); + const files = { images: file }; + const response = await upload({ + files, + }); + console.log(response); + }; + + const handleOnChange = ( + e: React.ChangeEvent + ) => { + console.log(e.target.files); + setFile(e.target.files); + }; + + return ( +
+

Hooks File Upload

+ + +
+ ); +} +``` + +> 手动调用(通过 FormData 上传) + +```ts +const input = + document.getElementById('file'); + +const formdata = new FormData(); +formdata.append('file', input.files[0]); + +fetch('/api/upload', { + method: 'POST', + body: formdata, +}) + .then((res) => res.json()) + .then((res) => console.log(res)); +``` + +## Api + +### Upload(path?: string) + +声明上传接口,可以指定路径。默认为 `POST` 接口,只支持 `multipart/form-data` 类型的请求。 + +### useFiles() + +在函数中使用 `useFiles()` 可以获取上传的文件。返回值为 Object,key 为上传时的字段名。有多个文件字段名相同时,Value 为 Array。 + +```ts +// frontend +await upload({ pdf }); + +// backend +const files = useFiles(); +{ + pdf: { + filename: 'test.pdf', // 文件原名 + data: '/var/tmp/xxx.pdf', // mode 为 file 时为服务器临时文件地址 + fieldname: 'test1', // 表单 field 名 + mimeType: 'application/pdf', // mime + } +} +``` + +### useFields() + +返回 FormData 中非文件的字段。 + +```ts +// frontend +const formdata = new FormData(); +formdata.append('name', 'test'); + +post(formdata); + +// backend +const fields = useFields(); +// { name: 'test' } +``` diff --git a/site/versioned_docs/version-3.0.0/hooks/validate.md b/site/versioned_docs/version-3.0.0/hooks/validate.md new file mode 100644 index 000000000000..247373d6cda4 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/hooks/validate.md @@ -0,0 +1,248 @@ +# 参数校验 + +## 校验 + +Midway Hooks 使用 [zod@3](https://www.npmjs.com/package/zod) 作为校验器,并提供 `Validate(...schemas: any[])` 校验用户入参,`ValidateHttp(options)` 函数来校验 Http 结构。 + +使用前请安装 [zod](https://www.npmjs.com/package/zod)。 + +``` +npm install zod +``` + +## Validate + +`Validate` 传入的 Schema 顺序与用户入参顺序匹配。 + +### 基础示例 + +```ts +import { + Api, + Post, + Validate, +} from '@midwayjs/hooks'; +import { z } from 'zod'; + +export default Api( + Post('/hello'), + Validate(z.string(), z.number()), + async (name: string, age: number) => { + return `Hello ${name}, you are ${age} years old.`; + } +); +``` + +一体化调用: + +```ts +import hello from './api'; + +try { + await hello(null, null); +} catch (error) { + console.log( + JSON.parse(error.data.message) + ); + console.log(error.status); // 422 +} +``` + +手动调用: + +```ts +fetcher + .post('/hello', { + args: [null, null], + }) + .catch((error) => { + console.log( + JSON.parse(error.data.message) + ); + console.log(error.status); // 422 + }); +``` + +### 错误处理 + +通过 Try/Catch 可以捕捉到校验失败的错误。 + +```ts +try { + // 调用接口 +} catch (error) { + console.log(error.data.code); // VALIDATION_FAILED + console.log( + JSON.parse(error.data.message) + ); +} +``` + +`error.data.message` 包含完整的[错误信息](https://zod.js.org/docs/errors/),你需要使用 `JSON.parse` 解析,解析后的示例如下: + +```ts +[ + { + code: 'invalid_type', + expected: 'string', + received: 'number', + path: [0, 'name'], + message: + 'Expected string, received number', + }, +]; +``` + +其中: + +- `message`: 错误信息 +- `path` 参数代表错误路径,如 `0` 代表第一个参数校验出错,`name` 代表是 `name` 字段校验出错。 + +你可以手动解析错误消息,并展示给用户。 + +### ValidateHttp + +ValidateHttp(options) 支持传入 `options` 参数,类型如下。 + +```ts +type ValidateHttpOption = { + query?: z.Schema; + params?: z.Schema; + headers?: z.Schema; + data?: z.Schema[]; +}; +``` + +以校验 `Query` 参数为例。 + +后端代码: + +```ts +import { + Api, + Get, + Query, + useContext, + ValidateHttp, +} from '@midwayjs/hooks'; +import { z } from 'zod'; + +const QuerySchema = z.object({ + searchString: z.string().min(5), +}); + +export const filterPosts = Api( + Get('/api/filterPosts'), + Query>(), + ValidateHttp({ query: QuerySchema }), + async () => { + const ctx = useContext(); + return ctx.query.searchString; + } +); +``` + +一体化调用: + +```ts +import filterPosts from './api'; + +try { + await filterPosts({ + query: { searchString: '' }, + }); +} catch (error) { + console.log( + JSON.parse(error.data.message) + ); + console.log(error.status); // 422 +} +``` + +手动调用: + +```ts +fetcher + .get( + '/api/filterPosts?searchString=1' + ) + .catch((error) => { + console.log( + JSON.parse(error.data.message) + ); + console.log(error.status); // 422 + }); +``` + +## TypeScript 支持 + +你可以通过 zod 内置的 TypeScript 功能,来实现复杂类型的推导与校验。 + +示例如下: + +```ts +import { + Api, + Post, + Validate, +} from '@midwayjs/hooks'; +import { z } from 'zod'; + +const Project = z.object({ + name: z.string(), + description: z.string(), + owner: z.string(), + members: z.array(z.string()), +}); + +export default Api( + Post('/project'), + Validate(Project), + async ( + // { name: string, description: string, owner: string, members: string[] } + project: z.infer + ) => { + return project; + } +); +``` + +一体化调用: + +```ts +import createProject from './api'; + +try { + await createProject({ + name: 1, + description: 'test project', + owner: 'test', + members: ['test'], + }); +} catch (error) { + console.log(error.message); + console.log(error.status); // 422 +} +``` + +手动调用: + +```ts +fetcher + .post('/project', { + args: [ + { + name: 1, + description: 'test project', + owner: 'test', + members: ['test'], + }, + ], + }) + .catch((error) => { + console.log( + JSON.parse(error.data.message) + ); + console.log(error.status); // 422 + }); +``` diff --git a/site/versioned_docs/version-3.0.0/how_to_install_nodejs.md b/site/versioned_docs/version-3.0.0/how_to_install_nodejs.md new file mode 100644 index 000000000000..80156217f8de --- /dev/null +++ b/site/versioned_docs/version-3.0.0/how_to_install_nodejs.md @@ -0,0 +1,136 @@ +# 如何安装 Node.js 环境 + +## 使用场景 + +一般来说,直接从 [Node.js 官网](https://nodejs.org/)下载对应的安装包,即可完成环境配置。 + +但在**本地开发**的时候,经常需要快速更新或切换版本。 + +社区有 [nvm](https://github.com/creationix/nvm)、[n](https://github.com/tj/n) 等方案,我们推荐跨平台的 [nvs](https://github.com/jasongin/nvs)。 + +- nvs 是跨平台的。 +- nvs 是基于 Node 编写的,我们可以参与维护。 + + + +> 友情提示:Node 12.x 和 14.x 分别于2022和2023年4月结束生命期(EOL),请尽快升级到 16 或者 18 。 +> [https://github.com/nodejs/Release](https://github.com/nodejs/Release) + + + +**PS:nvs 我们一般只用于本地开发,线上参见:**[科普文:运维不给升级 Node 版本怎么办?](https://zhuanlan.zhihu.com/p/39226941) + +--- + +## 如何安装 + +### Linux / macOS 环境 + +通过 Git Clone 对应的项目即可。 + +```bash +$ export NVS_HOME="$HOME/.nvs" +$ git clone https://github.com/jasongin/nvs --depth=1 "$NVS_HOME" +$ . "$NVS_HOME/nvs.sh" install +``` + +### Windows 环境 + +由于 Windows 环境配置比较复杂,所以还是推荐使用 `msi` 文件完成初始化工作。 +访问 [nvs/releases](https://github.com/jasongin/nvs/releases) 下载最新版本的 `nvs.msi`,然后双击安装即可。 + +--- + +## 配置镜像地址 +在国内由于大家都懂的原因,需要把对应的镜像地址修改下: +```bash +$ nvs remote node https://npmmirror.com/mirrors/node/ +$ nvs remote +default node +chakracore https://github.com/nodejs/node-chakracore/releases/ +chakracore-nightly https://nodejs.org/download/chakracore-nightly/ +nightly https://nodejs.org/download/nightly/ +node https://nodejs.org/dist/ +``` + +--- + +## 使用指南 +通过以下命令,即可非常简单的安装 Node.js 最新的 LTS 版本。 +```bash +# 安装最新的 LTS 版本 +$ nvs add lts +# 配置为默认版本 +$ nvs link lts +``` +安装其他版本: +```bash +# 安装其他版本尝尝鲜 +$ nvs add 12 +# 查看已安装的版本 +$ nvs ls +# 在当前 Shell 切换版本 +$ nvs use 12 +``` +更多指令参见 `nvs --help` 。 + +--- + +## 共用 npm 全局模块 +使用 `nvs` 时,默认的 `prefix` 是当前激活的 Node.js 版本的安装路径。 +带来一个问题是:切换版本之后,之前安装全局命令模块需要重新安装,非常不方便。 +解决方案是配置统一的全局模块安装路径到 `~/.npm-global`,如下: +```bash +$ mkdir -p ~/.npm-global +$ npm config set prefix ~/.npm-global +``` +还需配置环境变量到 `~/.bashrc` 或 `~/.zshrc` 文件里面: +```bash +$ echo "export PATH=~/.npm-global/bin:$PATH" >> ~/.zshrc +$ source ~/.zshrc +``` + +--- + + + +## Mac Silicon 芯片使用低版本 Node.js + +如果你使用的是 Apple 芯片,由于 Node.js 16 以下没有 arm64 的芯片支持构建版本,所以没法直接安装。 + +幸运的是,有一些解决方法可以使 Node.js 14与 Mac Silicon一起使用。Apple提供了Rosetta,这是一款翻译应用程序,允许为Intel芯片 (或上一代Mac) 构建的应用程序在 Apple Silicon下运行。 + +有两个步骤: + +* 1、安装 Rosetta +* 2、切换到 intel 环境,安装低版本 Node.js + + + +**安装 Rosetta** + +打开终端,执行 + +```bash +$ /usr/sbin/softwareupdate --install-rosetta --agree-to-license +``` + + + +**切换环境,安装低版本 Node.js** + +* 1、打开终端,执行 `arch` ,确认运行的是 `arm64` +* 2、执行 `arch -x86_64 zsh`,开启新的终端 +* 3、执行 `arch` ,确认运行的是 `i386` +* 4、安装低版本 Node.js,你可以使用上面提到的 nvs 或者 nvm 来安装 + + + + + +## 相关阅读 + +- [科普文:Node.js 安全攻防 - 如何伪造和获取用户真实 IP ?](https://zhuanlan.zhihu.com/p/62265144) +- [科普文:运维不给升级 Node 版本怎么办?](https://zhuanlan.zhihu.com/p/39226941) +- [科普文:为什么不能在服务器上 npm install ?](https://zhuanlan.zhihu.com/p/39209596) +- [Using NodeJs 14 with Mac Silicon (M1)](https://devzilla.io/using-nodejs-14-with-mac-silicon-m1) diff --git a/site/versioned_docs/version-3.0.0/how_to_update_midway.md b/site/versioned_docs/version-3.0.0/how_to_update_midway.md new file mode 100644 index 000000000000..c36f616cf674 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/how_to_update_midway.md @@ -0,0 +1,248 @@ +# 如何更新 Midway + + + +## 什么时候要更新 Midway + +一般来说,在下面的情况下,你可能需要更新: + +- 1、Midway 发了新版本之后,你希望用到新功能的时候 +- 2、你安装了一个新的组件且带有 lock 文件的时候 +- 3、出现方法找不到的错误的时候 +- ... 等等 + +比如出现下面错误的时候 + +1、一般是装了组件的新包,但是老的 @midwayjs/core 未包含该方法从而报错。 + +![](https://img.alicdn.com/imgextra/i3/O1CN01dDNRZr1MBPewPo7Xg_!!6000000001396-2-tps-1196-317.png) + +2、一般原因为 mock 依赖的 @midwayjs/core 版本没这个方法,说明版本不对,可能是错误引用了版本,也可能是版本太低 + +![](https://img.alicdn.com/imgextra/i3/O1CN01HVMJKP1xNuFO2Wv73_!!6000000006432-2-tps-1055-135.png) + +3、新装组件的时候,我们发现某个包的版本实例不止一个 + +![](https://img.alicdn.com/imgextra/i3/O1CN01jZxQu91YBCs0N9S9Y_!!6000000003020-2-tps-1133-43.png) + +## 更新注意事项 + +:::danger + +midway 项目的依赖使用 lerna 发布,**请不要**: + + +- 1、单独升级某个 @midwayjs/* 的包 +- 2、将 package.json 中的版本号移除 ^ 符号 + +::: + + + +## 检查包版本异常 + +你可以使用下面的命令在项目根目录执行进行检查。 + +```bash +# 社区用户 +$ npx midway-version +# 内部用户 +$ tnpx @ali/midway-version +``` + +如果项目为 pnpm 安装的依赖,请使用下面的命令。 + +```bash +# 社区用户 +$ pnpx midway-version +# 内部用户 +$ pnpx @ali/midway-version +``` + + + +## 使用工具更新版本 + +你可以使用下面的命令在项目根目录执行进行更新提示。 + +```bash +# 社区用户 +$ npx midway-version -u +# 内部用户 +$ tnpx @ali/midway-version -u +``` + +如果项目为 pnpm 安装的依赖,请使用下面的命令。 + +```bash +# 社区用户 +$ pnpx midway-version -u +# 内部用户 +$ pnpx @ali/midway-version -u +``` + +如果你希望将更新写入到 `package.json` 中,请使用下面的命令。 + +```bash +# 社区用户 +$ npx midway-version -u -w +# 内部用户 +$ tnpx @ali/midway-version -u -w +``` + +如果项目为 pnpm 安装的依赖,请使用下面的命令。 + +```bash +# 社区用户 +$ pnpx midway-version -u -w +# 内部用户 +$ pnpx @ali/midway-version -u -w +``` + +:::tip + +更新的版本会写入 `package.json` 和 `package-lock.json`,并需要重新安装依赖。 + +::: + + + +## 手动更新版本 + + +### 普通项目更新 + + +普通使用 npm/yarn 的项目,升级请按照下面的流程 + + +- 1、删除 package-lock.json 或者 yarn.lock +- 2、彻底删除 node_modules(比如 rm -rf node_modules) +- 3、重新安装依赖( npm install 或者 yarn) + + + +**我们不保证使用其他工具、cli 单独升级包的效果。** + + + + +### lerna 项目更新 + + +使用 lerna 开发项目,由于有 hoist 模式的存在,升级请按照下面的流程(以 lerna3 为例) + + + +- 1、清理子包的 node_modules,比如(lerna clean --yes) +- 2、删除主包的 node_modules(比如 rm -rf node_modules) +- 3、删除 package-lock.json 或者 yarn.lock +- 4、重新安装依赖( npm install && lerna bootstrap) + + + +**我们不保证使用其他工具、cli 单独升级包的效果。** + + + + +## 大版本更新 + + +请手动修改版本号,比如从 `^1.0.0` 修改为 `^2.0.0` 。 + + + +## 查看当前包版本 + + +Midway 包采用标准的 Semver 版本进行管理和发布,在 `package.json` 指定的版本一般为 `^` 开头,表示在大版本范围内都兼容。 + + +比如,`package.json` 中 `@midwayjs/core` 为 `^2.3.0` ,那么按照 npm 安装规则,会安装 `2.x` 这个版本下最新的 latest 版本。 + + +所以实际安装的版本高于 `package.json` 中指定的版本都是正常的。 + + +你可以使用 `npm ls 包名` 来查看具体的版本,比如 `npm ls @midwayjs/core` 来查看 `@midwayjs/core` 的版本。 + + +## 版本匹配查询 + + +由于 lerna 发包有一定的依赖性,比如修改到的包才会更新,就会出现 **midway 下的包版本不一定完全一致的情况。** + + +比如,`@midwayjs/web` 的版本高于 `@midwayjs/core`,这都是很正常的。 + + +midway 每次发布会提交一个 [@midwayjs/version ](https://www.npmjs.com/package/@midwayjs/version)的包,其中包含了我们每个版本,以及该版本的包所匹配的全部包版本,请 [访问这里](https://github.com/midwayjs/midway/tree/2.x/packages/version/versions) 查看。 + + +目录中的文件名按照 `@midwayjs/decorator版本 - @midwayjs/core版本.json` 规则创建,每个版本对应一个 JSON 文件。 + + +文件内容以包名作为 key,以可兼容匹配的版本名作为值。 + + +比如,当前文件 decorator(v2.10.18)和 core(v2.10.18) 所能兼容的 egg-layer 包版本为 v2.10.18 和 v2.10.19。 + + +如果 decorator 和 core 组合的文件名未找到,或者文件里的版本不匹配,都说明 **版本可能产生了问题**。 + + +内容示例如下: +```json +{ + "@midwayjs/egg-layer": [ + "2.10.18", + "2.10.19" + ], + "@midwayjs/express-layer": "2.10.18", + "@midwayjs/faas-typings": "2.10.7", + "@midwayjs/koa-layer": "2.10.18", + "@midwayjs/runtime-engine": "2.10.14", + "@midwayjs/runtime-mock": "2.10.14", + "@midwayjs/serverless-app": "2.10.18", + "@midwayjs/serverless-aws-starter": "2.10.14", + "@midwayjs/serverless-fc-starter": "2.10.18", + "@midwayjs/serverless-fc-trigger": "2.10.18", + "@midwayjs/serverless-http-parser": "2.10.7", + "@midwayjs/serverless-scf-starter": "2.10.14", + "@midwayjs/serverless-scf-trigger": "2.10.18", + "@midwayjs/static-layer": "2.10.18", + "@midwayjs/bootstrap": "2.10.18", + "@midwayjs/cache": "2.10.18", + "@midwayjs/consul": "2.10.18", + "@midwayjs/core": "2.10.18", + "@midwayjs/decorator": "2.10.18", + "@midwayjs/faas": "2.10.18", + "@midwayjs/grpc": "2.10.18", + "@midwayjs/logger": "2.10.18", + "midway-schedule": "2.10.18", + "midway": [ + "2.10.18", + "2.10.19" + ], + "@midwayjs/mock": "2.10.18", + "@midwayjs/prometheus": "2.10.18", + "@midwayjs/rabbitmq": "2.10.18", + "@midwayjs/socketio": "2.10.18", + "@midwayjs/task": [ + "2.10.18", + "2.10.19" + ], + "@midwayjs/typegoose": "2.10.18", + "@midwayjs/version": [ + "2.10.18", + "2.10.19" + ], + "@midwayjs/express": "2.10.18", + "@midwayjs/koa": "2.10.18", + "@midwayjs/web": [ + "2.10.18", + "2.10.19" + ] +} +``` \ No newline at end of file diff --git a/site/versioned_docs/version-3.0.0/intro.md b/site/versioned_docs/version-3.0.0/intro.md new file mode 100644 index 000000000000..ad2548a0a195 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/intro.md @@ -0,0 +1,130 @@ +# 介绍 + +Midway 是阿里巴巴 - 淘宝前端架构团队,基于渐进式理念研发的 Node.js 框架,通过自研的依赖注入容器,搭配各种上层模块,组合出适用于不同场景的解决方案。 + +Midway 基于 TypeScript 开发,结合了`面向对象(OOP + Class + IoC)`与`函数式(FP + Function + Hooks)`两种编程范式,并在此之上支持了 Web / 全栈 / 微服务 / RPC / Socket / Serverless 等多种场景,致力于为用户提供简单、易用、可靠的 Node.js 服务端研发体验。 + + + +## 为什么要有 Midway + +社区上也有很多类似的框架,那为什么还需要 Midway ? + +原因有三点: + +1. Midway 是阿里内部一直持续在研发的框架,需要有面向应用层面的框架来和集团场景对接 +2. 全量使用 TypeScript 是未来一段时间的趋势,面向未来去迭代和研发是作为架构组创新的要求 +3. 虽然社区已经有 nest 这样的框架,但是这些产品的维护、协作、修改都会受到商业化产品的制约,也无法做到需求的快速迭代和安全性保障,整体的研发理念也和我们不同,为此,我们需要有一套自研的框架体系 + + + +## 我们的优势 + +1. Midway 框架是在内部已经使用 5 年以上的 Node.js 框架,有着长期投入和持续维护的团队做后盾 +2. 已经在每年的大促场景经过考验,稳定性无须担心 +3. 丰富的组件和扩展能力,例如数据库,缓存,定时任务,进程模型,部署以及 Web,Socket 甚至 Serverless 等新场景的支持 +4. 一体化调用方案可以方便快捷和前端页面协同开发 +5. 良好的 TypeScript 定义支持 +6. 国产化文档和沟通容易简单 + + + +## 多编程范式 + +Midway 支持面向对象与函数式两种编程范式,你可以根据实际研发的需要,选择不同的编程范式来开发应用。 + + + +### 面向对象(OOP + Class + IoC) + +Midway 支持面向对象的编程范式,为应用提供更优雅的架构。 + +下面是基于面向对象,开发路由的示例。 +```typescript +// src/controller/home.ts +import { Controller, Get } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context + + @Get('/') + async home() { + return { + message: 'Hello Midwayjs!', + query: this.ctx.ip + } + } +} +``` + + + +### 函数式(FP + Function + Hooks) + +Midway 也支持函数式的编程范式,为应用提供更高的研发效率。 + + +下面是基于函数式,开发路由接口的示例。 +```typescript +// src/api/index.ts + +import { useContext } from '@midwayjs/hooks' +import { Context } from '@midwayjs/koa'; + +export default async function home () { + const ctx = useContext() + + return { + message: 'Hello Midwayjs!', + query: ctx.ip + } +} +``` + + + +## 环境准备工作 + + +Midway 运行请预先安装 Node.js 环境和 npm,在国内可以使用 cnpm。 + + +- 操作系统:支持 macOS,Linux,Windows +- 运行环境:建议选择 [LTS 版本](http://nodejs.org/),最低要求 **12.11.0**。 + +在经过不断迭代之后,Midway 的版本要求如下: + +| Midway 版本 | 开发环境 Node.js 版本要求 | 部署环境 Node.js 版本要求 | +| ------------- | ------------------------- | ------------------------- | +| >=v3.9.0 | >= v14,推荐 LTS 版本 | >= v12.11.0 | +| 3.0.0 ~ 3.9.0 | >= v12,推荐 LTS 版本 | >= v12.0.0 | +| 2.x | >= v12,推荐 LTS 版本 | >= v10.0.0 | + +如果需要帮助,请参考 [如何安装 Node.js 环境](how_to_install_nodejs)。 + + + +## 正确的提问 + +- ✅ 在 [github issue](https://github.com/midwayjs/midway/issues) 提问,可追踪,可沉淀,可 Star + - 1、描述你的问题,提供尽可能详细的复现方法,框架版本,场景(Serverless 还是应用) + - 2、尽可能提供报错截图,堆栈信息,最小复现的 repo + + + +## 答疑分享群 + +群里会有热心的朋友,也会有新版本发布推送。 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01LyI8r91S91RsKsD29_!!6000000002203-0-tps-3916-2480.jpg) + + + +## 官方宣传渠道 + +- [哔哩哔哩](https://space.bilibili.com/1746017680),会提供更新信息和教程 + diff --git a/site/versioned_docs/version-3.0.0/legacy/mongodb.md b/site/versioned_docs/version-3.0.0/legacy/mongodb.md new file mode 100644 index 000000000000..8b844178301c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/legacy/mongodb.md @@ -0,0 +1,546 @@ +# MongoDB + +:::tip +本文档从 v3.4.0 版本起废弃。 +::: + +在这一章节中,我们选择 [Typegoose](https://github.com/typegoose/typegoose) 作为基础的 MongoDB ORM 库。就如同他描述的那样 " Define Mongoose models using TypeScript classes",和 TypeScript 结合的很不错。 + +简单的来说,Typegoose 使用 TypeScript 编写 Mongoose 模型的 “包装器”,它的大部分能力还是由 [mongoose](https://www.npmjs.com/package/mongoose) 库来提供的。 + +也可以直接选择 [mongoose](https://www.npmjs.com/package/mongoose) 库来使用,我们会分别描述。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | + + +## Mongoose 版本依赖 + + +mongoose 和你服务器使用的 MongoDB Server 的版本也有着一定的关系,如下,请务必注意。 + + +- MongoDB Server 2.4.x: mongoose ^3.8 or 4.x +- MongoDB Server 2.6.x: mongoose ^3.8.8 or 4.x +- MongoDB Server 3.0.x: mongoose ^3.8.22, 4.x, or 5.x +- MongoDB Server 3.2.x: mongoose ^4.3.0 or 5.x +- MongoDB Server 3.4.x: mongoose ^4.7.3 or 5.x +- MongoDB Server 3.6.x: mongoose 5.x +- MongoDB Server 4.0.x: mongoose ^5.2.0 +- MongoDB Server 4.2.x: mongoose ^5.7.0 +- MongoDB Server 4.4.x: mongoose ^5.10.0 +- MongoDB Server 5.x: mongoose ^6.0.0 + + +**mongoose 相关的依赖比较复杂,且对应不同的版本,现阶段,我们使用的主要是 mongoose v5 和 v6。** + + +:::info +从 mongoose@v5.11.0 开始,mongoose 官方支持了定义,所以不再需要安装 @types/mongoose 依赖包。 +::: + + +安装包依赖版本如下: + +**支持 MongoDB Server 5.x** + +```json + "dependencies": { + "mongoose": "^6.0.7", + "@typegoose/typegoose": "^9.0.0", // 使用 typegoose 需要安装此依赖 + }, +``` + + +**支持 MongoDB Server 4.4.x** + + +以下版本不需要安装额外定义包。 +```json + "dependencies": { + "mongoose": "^5.13.3", + "@typegoose/typegoose": "^8.0.0", // 使用 typegoose 需要安装此依赖 + }, +``` + + +以下版本需要安装额外定义包(不推荐)。 +```json + "dependencies": { + "mongodb": "3.6.3", // mongoose 内部写死了该版本 + "mongoose": "~5.10.18", + "@typegoose/typegoose": "^7.0.0", // 使用 typegoose 需要安装此依赖 + }, + "devDependencies": { + "@types/mongodb": "3.6.3", // 只能使用此版本 + "@types/mongoose": "~5.10.3", + } +``` + + +其余的 MongoDB 安装模块类似,未测。 + + +## 使用 Typegoose + + +### 1、安装组件 + + +安装 Typegoose 组件,提供访问 MongoDB 的能力。 + + +**请务必注意,请查看第一小节提前编写/安装 mongoose 等相关依赖包。** +```bash +$ npm i @midwayjs/typegoose@3 --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + // 组件 + "@midwayjs/typegoose": "^3.0.0", + // 上一节中的 mongoose 依赖 + }, + "devDependencies": { + // 上一节中的 mongoose 依赖 + // ... + } +} +``` + + + +安装后需要手动在 `src/configuration.ts` 配置,代码如下。 + +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as typegoose from '@midwayjs/typegoose'; + +@Configuration({ + imports: [ + typegoose // 加载 typegoose 组件 + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + + +:::info +在该组件中,midway 只是做了简单的配置规则化,并将其注入到初始化流程中。 +::: + + +### 2、配置连接信息 + + +在 `src/config/config.default.ts` 中加入连接的配置。 + +```typescript +export default { + // ... + mongoose: { + client: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '***********' + } + } + }, +} +``` + + +### 3、简单的目录结构 + + +我们以一个简单的项目举例,其他结构请自行参考。 + + +``` +MyProject +├── src // TS 根目录 +│ ├── config +│ │ └── config.default.ts // 应用配置文件 +│ ├── entity // 实体(数据库 Model) 目录 +│ │ └── user.ts // 实体文件 +│ ├── configuration.ts // Midway 配置文件 +│ └── service // 其他的服务目录 +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + +在这里,我们的数据库实体主要放在 `entity` 目录(非强制),这只是一个简单的约定。 + + +### 3、创建实体文件 + + +```typescript +import { prop } from '@typegoose/typegoose'; +import { EntityModel } from '@midwayjs/typegoose'; + +@EntityModel() +export class User { + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} +``` + +等价于使用 mongoose 的下列代码 + +```typescript +const userSchema = new mongoose.Schema({ + name: String, + jobs: [{ type: String }] +}); + +const User = mongoose.model('User', userSchema); +``` + +:::info +所以说,typegoose 只是简化了 model 的创建过程。 +::: + +### 4、引用实体,调用数据库 + + +示例代码如下: + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typegoose'; +import { ReturnModelType } from '@typegoose/typegoose'; +import { User } from '../entity/user'; + +@Provide() +export class TestService { + + @InjectEntityModel(User) + userModel: ReturnModelType; + + async getTest(){ + // create data + const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an "as" assertion, to have types for all properties + + // find data + const user = await this.userModel.findById(id).exec(); + console.log(user) + } +} +``` + + +### 5、多库的情况 + + +首先配置多个连接。 + + +在 `src/config/config.default.ts` 中加入连接的配置,`default` 代表了默认的连接。 +```typescript +export default { + // ... + mongoose: { + clients: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '***********' + } + }, + db1: { + uri: 'mongodb://localhost:27017/test1', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '***********' + } + } + } + }, +} +``` + + +定义实例时使用固定的连接,比如: +```typescript +@EntityModel() // 默认使用了 default 连接 +class User { + + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} + +@EntityModel({ + connectionName: 'db1' // 这里使用了 db1连接 +}) +class User2 { + + @prop() + public name?: string; + + @prop({ type: () => [String] }) + public jobs?: string[]; +} +``` + + +在使用时,注入特定的连接 +```typescript +@Provide() +export class TestService { + + @InjectEntityModel(User) + userModel: ReturnModelType; + + @InjectEntityModel(User2) + user2Model: ReturnModelType; + + async getTest(){ + const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an "as" assertion, to have types for all properties + const user = await this.userModel.findById(id).exec(); + console.log(user) + + const { _id: id2 } = await this.user2Model.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User2); // an "as" assertion, to have types for all properties + const user2 = await this.user2Model.findById(id2).exec(); + console.log(user2) + } +} + +``` + + +## 直接使用 mongoose + +mongoose 组件是 typegoose 的基础组件,有时候我们可以直接使用它。 + + +### 1、安装组件 + + +**请务必注意,请查看第一小节提前编写/安装 mongoose 等相关依赖包。** + +```bash +$ npm i @midwayjs/mongoose --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + // 组件 + "@midwayjs/mongoose": "^3.0.0", + // 上一节中的 mongoose 依赖 + }, + "devDependencies": { + // 上一节中的 mongoose 依赖 + // ... + } +} +``` + + + +### 2、开启组件 + + +安装后需要手动在 `src/configuration.ts` 配置,代码如下。 +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as mongoose from '@midwayjs/mongoose'; + +@Configuration({ + imports: [ + mongoose // 加载 mongoose 组件 + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration { + +} +``` + + + + +### 2、配置 + + +和 typegoose 相同,或者说 typegoose 使用的就是 mongoose 的配置。 + + +单库: +```typescript +export default { + // ... + mongoose: { + client: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '**********' + } + } + }, +} +``` +多库: +```typescript +export default { + // ... + mongoose: { + clients: { + default: { + uri: 'mongodb://localhost:27017/test', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '***********' + } + }, + db1: { + uri: 'mongodb://localhost:27017/test1', + options: { + useNewUrlParser: true, + useUnifiedTopology: true, + user: '***********', + pass: '***********' + } + } + } + }, +} +``` + + +### 3、使用 + + +在只有一个默认连接或者直接使用 default 连接时,我们可以直接使用封装好的 `MongooseConnectionService` 对象来创建 model。 +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { MongooseConnectionService } from '@midwayjs/mongoose'; +import { Schema, Document } from 'mongoose'; + +interface User extends Document { + name: string; + email: string; + avatar: string; +} + +@Provide() +export class TestService { + + @Inject() + conn: MongooseConnectionService; + + async invoke(){ + const schema = new Schema({ + name: { type: String, required: true }, + email: { type: String, required: true }, + avatar: String + }); + const UserModel = this.conn.model('User', schema); + const doc = new UserModel({ + name: 'Bill', + email: 'bill@initech.com', + avatar: 'https://i.imgur.com/dM7Thhn.png' + }); + await doc.save(); + } +} + +``` + + +如果配置了多个其他连接,请从工厂方法中获取连接后再使用。 +```typescript +import { MongooseConnectionServiceFactory } from '@midwayjs/mongoose'; +import { Schema } from 'mongoose'; + +@Provide() +export class TestService { + + @Inject() + connFactory: MongooseConnectionServiceFactory; + + async invoke(){ + // get db1 connection + const conn = this.connFactory.get('db1'); + + // get default connection + const defaultConn = this.connFactory.get('default'); + + } +} + +``` + + +## 常见问题 + + +### 1、E002: You are using a NodeJS Version below 12.22.0 + + +在新版本 @typegoose/typegoose (v8, v9) 中增加了 Node 版本的校验,如果你的 Node.js 版本低于 v12.22.0,就会出现这个提示。 + + +普通情况下,请升级 Node.js 到这个版本以上即可解决。 + + +在特殊场景下,比如 Serverless 无法修改 Node.js 版本且版本低于 v12.22 的情况下,由于 v12 版本子版本其实都可以,可以通过临时修改 process.version 绕过。 + + +```typescript +// src/configuration.ts + +Object.defineProperty(process, 'version', { + value: 'v12.22.0', + writable: true, +}); + +// other code + +export class MainConfiguration {} +``` + + + diff --git a/site/versioned_docs/version-3.0.0/legacy/orm.md b/site/versioned_docs/version-3.0.0/legacy/orm.md new file mode 100644 index 000000000000..2b4c01e90951 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/legacy/orm.md @@ -0,0 +1,1524 @@ +# TypeORM + +:::tip +本文档从 v3.4.0 版本起废弃。 +::: + +[TypeORM](https://github.com/typeorm/typeorm) 是 `node.js` 现有社区最成熟的对象关系映射器(`ORM` )。Midway 和 TypeORM 搭配,使开发更简单。 + + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | + + + +## 安装组件 + + +安装 orm 组件,提供数据库 ORM 能力。 + + +```bash +$ npm i @midwayjs/orm@3 typeorm --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/orm": "^3.0.0", + "typeorm": "~0.3.0", + // ... + }, + "devDependencies": { + // ... + } +} +``` + + + +## 引入组件 + + +在 `src/configuration.ts` 引入 orm 组件,示例如下。 + +```typescript +// configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as orm from '@midwayjs/orm'; +import { join } from 'path'; + +@Configuration({ + imports: [ + // ... + orm // 加载 orm 组件 + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class ContainerConfiguratin { + +} +``` + + + +## 安装数据库 Driver + + +常用数据库驱动如下,选择你对应连接的数据库类型安装: + +```bash +# for MySQL or MariaDB,也可以使用 mysql2 替代 +npm install mysql --save +npm install mysql2 --save + +# for PostgreSQL or CockroachDB +npm install pg --save + +# for SQLite +npm install sqlite3 --save + +# for Microsoft SQL Server +npm install mssql --save + +# for sql.js +npm install sql.js --save + +# for Oracle +npm install oracledb --save + +# for MongoDB(experimental) +npm install mongodb --save +``` + +:::info +To make the** Oracle driver work**, you need to follow the installation instructions from [their](https://github.com/oracle/node-oracledb) site. +::: + + + + +## 简单的目录结构 + + +我们以一个简单的项目举例,其他结构请自行参考。 + + +``` +MyProject +├── src // TS 根目录 +│ ├── config +│ │ └── config.default.ts // 应用配置文件 +│ ├── entity // 实体(数据库 Model) 目录 +│ │ └── photo.ts // 实体文件 +│ │ └── photoMetadata.ts +│ ├── configuration.ts // Midway 配置文件 +│ └── service // 其他的服务目录 +├── .gitignore +├── package.json +├── README.md +└── tsconfig.json +``` + + + + +在这里,我们的数据库实体主要放在 `entity` 目录(非强制),这只是一个简单的约定。 + + + +## 入门 + +下面,我们将以 mysql 举例。 + + + + +### 1、创建 Model + + +我们通过模型和数据库关联,在应用中的模型就是数据库表,在 TypeORM 中,模型是和实体绑定的,每一个实体(Entity) 文件,即是 Model,也是实体(Entity)。 + + +在示例中,需要一个实体,我们这里拿 `photo` 举例。新建 entity 目录,在其中添加实体文件 `photo.ts` ,一个简单的实体如下。 + +```typescript +// entity/photo.ts +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + +要注意,这里的实体文件的每一个属性,其实是和数据库表一一对应的,基于现有的数据库表,我们往上添加内容。 + + +### 2、添加实体模型装饰器 + + +我们使用 `EntityModel` 来定义一个实体模型类。 + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; + +@EntityModel('photo') +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + +:::caution +注意,这里的 EntityModel 是 midway 做了封装的特殊装饰器,为了和 midway 更好的结合使用。请不要直接使用 typeorm 中的 Entity。 +::: + + +如果表名和当前的实体名不同,可以在参数中指定。 + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; + +@EntityModel('photo_table_name') +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + + +这些实体列也可以使用 [typeorm_generator](/docs/tool/typeorm_generator) 工具生成。 + + +### 3、添加数据库列 + + +通过 typeorm 提供的 `@Column` 装饰器来修饰属性,每一个属性对应一个列。 + + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; +import { Column } from 'typeorm'; + +@EntityModel() +export class Photo { + + @Column() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` + +现在 `id` , `name` , `description` ,`filename` , `views` , `isPublished` 列将添加到 `photo` 表中。数据库中的列类型是根据您使用的属性类型推断出来的,例如 number 将转换为整数,将字符串转换为 varchar,将布尔值转换为 bool,等等。但是您可以通过在 `@Column`装饰器中显式指定列类型来使用数据库支持的任何列类型。 + + +我们生成了带有列的数据库表,但是还剩下一件事。每个数据库表必须具有带主键的列。 + + +数据库列包括更多的列选项(ColumnOptions),比如修改列名,指定列类型,列长度等,更多的选项请参考 [官方文档](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/entities.md#%E5%88%97%E9%80%89%E9%A1%B9)。 + + + + +### 4、创建主键列 + + +每个实体必须至少具有一个主键列。要使列成为主键,您需要使用 `@PrimaryColumn` 装饰器。 + + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryColumn } from 'typeorm'; + +@EntityModel() +export class Photo { + + @PrimaryColumn() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` + +### 5、创建自增主键列 + + +现在,如果要设置自增的 id 列,需要将 `@PrimaryColumn` 装饰器更改为 `@PrimaryGeneratedColumn` 装饰器: + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryGeneratedColumn } from 'typeorm'; + +@EntityModel() +export class Photo { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + filename: string; + + @Column() + views: number; + + @Column() + isPublished: boolean; + +} +``` + + +### 6、列数据类型 + + +接下来,让我们调整数据类型。默认情况下,字符串映射到类似 `varchar(255)` 的类型(取决于数据库类型)。 Number 映射为类似整数的类型(取决于数据库类型)。但是我们不希望所有列都限制为 varchars 或整数,这个时候可以做一些修改。 + + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryGeneratedColumn } from 'typeorm'; + +@EntityModel() +export class Photo { + + @PrimaryGeneratedColumn() + id: number; + + @Column({ + length: 100 + }) + name: string; + + @Column('text') + description: string; + + @Column() + filename: string; + + @Column("double") + views: number; + + @Column() + isPublished: boolean; + +} +``` + + +示例,不同列名 + +```typescript +@Column({ + length: 100, + name: 'custom_name' +}) +name: string; +``` + + +此外还有有几种特殊的列类型可以使用: + + +- `@CreateDateColumn` 是一个特殊列,自动为实体插入日期。 +- `@UpdateDateColumn` 是一个特殊列,在每次调用实体管理器或存储库的save时,自动更新实体日期。 +- `@VersionColumn` 是一个特殊列,在每次调用实体管理器或存储库的save时自动增长实体版本(增量编号)。 +- `@DeleteDateColumn` 是一个特殊列,会在调用 soft-delete(软删除)时自动设置实体的删除时间。 + +列类型是特定于数据库的。您可以设置数据库支持的任何列类型。有关支持的列类型的更多信息,请参见[此处](https://github.com/typeorm/typeorm/blob/master/docs/entities.md#column-types)。 + +:::tip + +`CreateDateColumn` 和 `UpdateDateColumn` 是依靠第一次同步表结构时,创建列上的默认数据完成的插入日期功能,如果是自己创建的表,需要自行在列上加入默认数据。 + +::: + + + + +### 7、配置连接信息 + + +请参考 [配置](/docs/env_config) 章节,增加配置文件。 + + +然后在 `config.default.ts` 中配置数据库连接信息。 + +```typescript +// src/config/config.default.ts +export default { + // ... + orm: { + /** + * 单数据库实例 + */ + type: 'mysql', + host: '', + port: 3306, + username: '', + password: '', + database: undefined, + synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true + logging: false, + }, +} +``` + +默认存储的是 utc 时间(推荐)。 + + +也可以配置时区(不建议) + +```typescript +// src/config/config.default.ts +export default { + // ... + orm: { + // ... + timezone: '+08:00', + }, +} +``` + + +这个 `type` 字段你可以使用其他的数据库类型,包括`mysql`, `mariadb`, `postgres`, `cockroachdb`, `sqlite`, `mssql`, `oracle`, `cordova`, `nativescript`, `react-native`, `expo`, or `mongodb` + + + 比如 sqlite,则只需要以下信息。 + + +```typescript +// src/config/config.default.ts +export default { + // ... + orm: { + type: 'sqlite', + database: path.join(__dirname, '../../test.sqlite'), + synchronize: true, + logging: true, + }, +} +``` + + +:::info +注意:synchronize 字段用于同步表结构。使用 `synchronize: true` 进行生产模式同步是不安全的,在上线后,请把这个字段设置为 false。 +::: + + +### 8、使用 Model 插入数据库数据 + + +在常见的 Midway 文件中,使用 `@InjectEntityModel` 装饰器注入我们配置好的 Model。我们所需要做的只是: + + +- 1、创建实体对象 +- 2、执行 `save()` + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from '../entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // save + async savePhoto() { + // create a entity object + let photo = new Photo(); + photo.name = 'Me and Bears'; + photo.description = 'I am near polar bears'; + photo.filename = 'photo-with-bears.jpg'; + photo.views = 1; + photo.isPublished = true; + + // save entity + const photoResult = await this.photoModel.save(photo); + + // save success + console.log('photo id = ', photoResult.id); + } +} +``` + + +### 9、查询数据 + +更多的查询参数,请查询 [find文档](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/find-options.md)。 + +自 typeorm@0.3.0 起,查询 API 有所变化。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from '../entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhotos() { + + // find All + let allPhotos = await this.photoModel.find(); // v0.2.x + let allPhotos = await this.photoModel.find({}); // v0.3.x + console.log("All photos from the db: ", allPhotos); + + // find first + let firstPhoto = await this.photoModel.findOne(1); + let firstPhoto = await this.photoModel.findOne({ // v0.3.x + where: { + id: 1 + } + }); + console.log("First photo from the db: ", firstPhoto); + + // find one by name + // v0.2.x + let meAndBearsPhoto = await this.photoModel.findOne({ name: "Me and Bears" }); + // v0.3.x + let meAndBearsPhoto = await this.photoModel.findOne({ + where: { name: "Me and Bears" } + }); + console.log("Me and Bears photo from the db: ", meAndBearsPhoto); + + // find by views + // v0.2.x + let allViewedPhotos = await this.photoModel.find({ views: 1 }); + // v0.3.x + let allViewedPhotos = await this.photoModel.find({ + where: { views: 1 } + }); + console.log("All viewed photos: ", allViewedPhotos); + + // v0.2.x + let allPublishedPhotos = await this.photoModel.find({ isPublished: true }); + // v0.3.x + let allPublishedPhotos = await this.photoModel.find({ + where: { isPublished: true } + }); + console.log("All published photos: ", allPublishedPhotos); + + // find and get count + // v0.2.x + let [allPhotos, photosCount] = await this.photoModel.findAndCount(); + // v0.3.x + let [allPhotos, photosCount] = await this.photoModel.findAndCount({}); + console.log("All photos: ", allPhotos); + console.log("Photos count: ", photosCount); + + } +} + +``` + + +### 10、更新数据库 + + +现在,让我们从数据库中加载一个 Photo,对其进行更新并保存。 + + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from '../entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + + let photoToUpdate = await this.photoModel.findOne(1); + photoToUpdate.name = "Me, my friends and polar bears"; + + await this.photoModel.save(photoToUpdate); + } +} +``` + + +### 11、删除数据 + + +```typescript +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from '../entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + /*...*/ + let photoToRemove = await this.photoModel.findOne(1); // typeorm@0.2.x + await this.photoModel.remove(photoToRemove); + } +} +``` + +现在,ID = 1的 Photo 将从数据库中删除。 + + +此外还有软删除的方法。 + +```typescript +await this.photoModel.softDelete(1); +``` + + +### 12、创建一对一关联 + + +让我们与另一个类创建一对一的关系。让我们在 `entity/photoMetadata.ts` 中创建一个新类。这个类包含 photo 的其他元信息。 + + +```typescript +import { Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm'; +import { EntityModel } from '@midwayjs/orm'; +import { Photo } from "./photo"; + +@EntityModel() +export class PhotoMetadata { + + @PrimaryGeneratedColumn() + id: number; + + @Column("int") + height: number; + + @Column("int") + width: number; + + @Column() + orientation: string; + + @Column() + compressed: boolean; + + @Column() + comment: string; + + @OneToOne(type => Photo) + @JoinColumn() + photo: Photo; + +} +``` + + +在这里,我们使用一个名为 `@OneToOne` 的新装饰器。它允许我们在两个实体之间创建一对一的关系。`type => Photo`是一个函数,它返回我们要与其建立关系的实体的类。 + + +由于语言的特殊性,我们被迫使用一个返回类的函数,而不是直接使用该类。我们也可以将其写为 `() => Photo` ,但是我们使用 `type => Photo`作为惯例来提高代码的可读性。类型变量本身不包含任何内容。 + + +我们还添加了一个 `@JoinColumn`装饰器,它指示关系的这一侧将拥有该关系。关系可以是单向或双向的。关系只有一方可以拥有。关系的所有者端需要使用@JoinColumn装饰器。 如果您运行该应用程序,则会看到一个新生成的表,该表将包含一列,其中包含用于 Photo 关系的外键。 + + +``` ++-------------+--------------+----------------------------+ +| photo_metadata | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| height | int(11) | | +| width | int(11) | | +| comment | varchar(255) | | +| compressed | boolean | | +| orientation | varchar(255) | | +| photoId | int(11) | FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +接下去我们要在代码中关联他们。 + + +```typescript +import { Provide, Inject, Func } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; +import { PhotoMetadata } from './entity/photoMetadata'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + @InjectEntityModel(PhotoMetadata) + photoMetadataModel: Repository; + + async updatePhoto() { + + // create a photo + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.isPublished = true; + + // create a photo metadata + let metadata = new PhotoMetadata(); + metadata.height = 640; + metadata.width = 480; + metadata.compressed = true; + metadata.comment = "cybershoot"; + metadata.orientation = "portrait"; + metadata.photo = photo; // this way we connect them + + + // first we should save a photo + await this.photoModel.save(photo); + + // photo is saved. Now we need to save a photo metadata + await this.photoMetadataModel.save(metadata); + + // done + console.log("Metadata is saved, and relation between metadata and photo is created in the database too"); + } +} +``` + + +### 13、反向关系映射 + + +关系映射可以是单向或双向的。当在 PhotoMetadata 和 Photo之间的关系是单向的。关系的所有者是PhotoMetadata,而 Photo对 PhotoMetadata 是一无所知的。这使得从 Photo 端访问 PhotoMetadata 变得很复杂。若要解决此问题,我们添加一个反向的关系映射,使 PhotoMetadata 和 Photo之间变成双向关联。让我们修改我们的实体。 + + +```typescript +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm'; +import { Photo } from './photo'; + +@EntityModel() +export class PhotoMetadata { + + /* ... other columns */ + + @OneToOne(type => Photo, photo => photo.metadata) + @JoinColumn() + photo: Photo; +} +``` + +```typescript +import { EntityModel } from '@midwayjs/orm'; +import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from 'typeorm'; +import { PhotoMetadata } from './photoMetadata'; + +@EntityModel() +export class Photo { + + /* ... other columns */ + + @OneToOne(type => PhotoMetadata, photoMetadata => photoMetadata.photo) + metadata: PhotoMetadata; +} +``` + +`photo => photo.metadata` 是一个返回反向映射关系的函数。在这里,我们显式声明 Photo 类的 metadata 属性用于关联 PhotoMetadata。除了传递返回 photo 属性的函数外,您还可以直接将字符串传递给 `@OneToOne` 装饰器,例如 `“metadata”` 。但是我们使用了这种函数回调的方法来让我们的代码写法更简单。 + + +请注意,只会在关系映射的一侧使用 `@JoinColumn` 装饰器。无论您放置此装饰器的哪一侧,都是关系的所有者。关系的拥有方在数据库中包含带有外键的列。 + + +### 14、加载对象及其依赖关系 + + +现在,让我们尝试在单个查询中一起加载出 Photo 和 PhotoMetadata。有两种方法可以执行此操作,使用 `find *` 方法或使用 `QueryBuilder` 功能。让我们首先使用 `find *` 方法。 `find *` 方法允许您使用 `FindOneOptions` / `FindManyOptions` 接口指定对象。 + + +```typescript +import { Provide, Inject, Func } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhoto() { + /*...*/ + let photos = await this.photoModel.find({ relations: [ 'metadata' ] }); // typeorm@0.2.x + } +} + +``` + +在这里,photos 的值是一个数组,包含了整个数据库的查询结果,并且每个 photo 对象都包含其关联的 metadata 属性。在[此文档](https://github.com/typeorm/typeorm/blob/master/docs/find-options.md)中了解有关 `Find Options` 的更多信息。 + + +使用 `Find Options` 很简单,但如果需要更复杂的查询,则应改用 `QueryBuilder` 。 `QueryBuilder` 允许以优雅的方式使用更复杂的查询。 + + +```typescript +import { Provide, Inject, Func } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + // find + async findPhoto() { + /*...*/ + let photos = await this.photoModel + .createQueryBuilder('photo') + .innerJoinAndSelect('photo.metadata', 'metadata') + .getMany(); + } +} +``` + +`QueryBuilder`允许创建和执行几乎任何复杂的 SQL 查询。使用 `QueryBuilder` 时,请像创建 SQL 查询一样思考。在此示例中,“photo” 和 “metadata” 是应用于所选 photos 的别名。您可以使用别名来访问所选数据的列和属性。 + + +### 15、使用级联操作自动保存关联对象 + + +在我们希望在每次保存另一个对象时都自动保存关联的对象,这个时候可以在关系中设置级联。让我们稍微更改照片的 `@OneToOne` 装饰器。 + + +```typescript +export class Photo { + /// ... other columns + + @OneToOne(type => PhotoMetadata, metadata => metadata.photo, { + cascade: true, + }) + metadata: PhotoMetadata; +} +``` + +使用 `cascade` 允许我们现在不再单独保存 Photo 和 PhotoMetadata,由于级联选项,元数据对象将被自动保存。 + + +```typescript +import { Provide, Inject, Func } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; +import { PhotoMetadata } from './entity/photoMetadata'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + async updatePhoto() { + + // create photo object + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.isPublished = true; + + // create photo metadata object + let metadata = new PhotoMetadata(); + metadata.height = 640; + metadata.width = 480; + metadata.compressed = true; + metadata.comment = "cybershoot"; + metadata.orientation = "portrait"; + + photo.metadata = metadata; // this way we connect them + + // save a photo also save the metadata + await this.photoModel.save(photo); + + // done + console.log("Photo is saved, photo metadata is saved too"); + } +} +``` + + +注意,我们现在设置 Photo 的元数据,而不需要像之前那样设置元数据的 Photo 属性。这仅当您从 Photo 这边将 Photo 连接到 PhotoMetadata 时,级联功能才有效。如果在 PhotoMetadata 侧设置,则不会自动保存。 + + +### 16、创建多对一/一对多关联 + + +让我们创建一个多对一/一对多关系。假设一张照片有一个作者,每个作者可以有很多照片。首先,让我们创建一个 Author 类: + +```typescript +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryGeneratedColumn, OneToMany, JoinColumn } from "typeorm"; +import { Photo } from './entity/photo'; + +@EntityModel() +export class Author { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @OneToMany(type => Photo, photo => photo.author) // note: we will create author property in the Photo class below + photos: Photo[]; +} +``` + +`Author` 包含了一个反向关系。 `OneToMany` 和 `ManyToOne` 需要成对出现。 + + +现在,将关系的所有者添加到 Photo 实体中: + +```typescript +import { EntityModel } from '@midwayjs/orm'; +import { Column, PrimaryGeneratedColumn, ManyToOne } from "typeorm"; +import { PhotoMetadata } from "./photoMetadata"; +import { Author } from "./author"; + +@Entity() +export class Photo { + + /* ... other columns */ + + @ManyToOne(type => Author, author => author.photos) + author: Author; +} +``` + + +在多对一/一对多关系中,所有者方始终是多对一。这意味着使用 `@ManyToOne` 的类将存储相关对象的 ID。 + + +运行应用程序后,ORM 将创建 `author` 表: + + +``` ++-------------+--------------+----------------------------+ +| author | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| name | varchar(255) | | ++-------------+--------------+----------------------------+ +``` + +它还将修改 `photo` 表,添加新的 `author` 列并为其创建外键: + +``` ++-------------+--------------+----------------------------+ +| photo | ++-------------+--------------+----------------------------+ +| id | int(11) | PRIMARY KEY AUTO_INCREMENT | +| name | varchar(255) | | +| description | varchar(255) | | +| filename | varchar(255) | | +| isPublished | boolean | | +| authorId | int(11) | FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +### 17、创建多对多关联 + + +让我们创建一个多对一/多对多关系。假设一张照片可以在许多相册中,并且每个相册可以包含许多照片。让我们创建一个 `Album` 类。 + + +```typescript +import { EntityModel } from '@midwayjs/orm'; +import { PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from "typeorm"; + +@EntityModel() +export class Album { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @ManyToMany(type => Photo, photo => photo.albums) + @JoinTable() + photos: Photo[]; +} +``` + + +`@JoinTable` 用来指明这是关系的所有者。 + + +现在,将反向关联添加到 `Photo` 。 + + +```typescript +export class Photo { + /// ... other columns + + @ManyToMany(type => Album, album => album.photos) + albums: Album[]; +} +``` + +运行应用程序后,ORM将创建一个 album_photos_photo_albums 联结表: + + +``` ++-------------+--------------+----------------------------+ +| album_photos_photo_albums | ++-------------+--------------+----------------------------+ +| album_id | int(11) | PRIMARY KEY FOREIGN KEY | +| photo_id | int(11) | PRIMARY KEY FOREIGN KEY | ++-------------+--------------+----------------------------+ +``` + + +现在,让我们将相册和照片插入数据库: + + +```typescript +import { Provide, Inject, Func } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; +import { PhotoMetadata } from './entity/photoMetadata'; +import { Repository } from 'typeorm'; + +@Provide() +export class PhotoService { + + @InjectEntityModel(Photo) + photoModel: Repository; + + @InjectEntityModel(Album) + albumModel: Repository + + async updatePhoto() { + + // create a few albums + let album1 = new Album(); + album1.name = "Bears"; + await this.albumModel.save(album1); + + let album2 = new Album(); + album2.name = "Me"; + await this.albumModel.save(album2); + + // create a few photos + let photo = new Photo(); + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.albums = [album1, album2]; + await this.photoModel.save(photo); + + + // now our photo is saved and albums are attached to it + // now lets load them: + const loadedPhoto = await this.photoModel.findOne(1, { relations: ["albums"] }); // typeorm@0.2.x + } +} +``` + +`loadedPhoto` 的值为: + +```json +{ + id: 1, + name: "Me and Bears", + description: "I am near polar bears", + filename: "photo-with-bears.jpg", + albums: [{ + id: 1, + name: "Bears" + }, { + id: 2, + name: "Me" + }] +} +``` + +### 18、使用 QueryBuilder + + +您可以使用QueryBuilder来构建几乎任何复杂的SQL查询。例如,您可以这样做: + + +```typescript +let photos = await this.photoModel + .createQueryBuilder("photo") // first argument is an alias. Alias is what you are selecting - photos. You must specify it. + .innerJoinAndSelect("photo.metadata", "metadata") + .leftJoinAndSelect("photo.albums", "album") + .where("photo.isPublished = true") + .andWhere("(photo.name = :photoName OR photo.name = :bearName)") + .orderBy("photo.id", "DESC") + .skip(5) + .take(10) + .setParameters({ photoName: "My", bearName: "Mishka" }) + .getMany(); +``` + +该查询选择所有带有 “My” 或 “Mishka” 名称的已发布照片。它将从位置 5 开始返回结果(分页偏移),并且将仅选择 10 个结果(分页限制)。选择结果将按 ID 降序排列。该照片的相册将 left-Joined,元数据将自动关联。 + + +您将在应用程序中大量使用查询生成器。在 [此处](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/select-query-builder.md) 了解有关QueryBuilder的更多信息。 + + +### 19、Event Subscriber + + +typeorm 提供了一个事件订阅机制,方便在做一些数据库操作时的日志输出,为此 midway 提供了一个 `EventSubscriberModel` 装饰器,用来标注事件订阅类,代码如下。 + + +```typescript +import { Provide } from '@midwayjs/core'; +import { EventSubscriberModel } from '@midwayjs/orm'; +import { EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm'; + +@Provide() +@EventSubscriberModel() +export class EverythingSubscriber implements EntitySubscriberInterface { + + /** + * Called before entity insertion. + */ + beforeInsert(event: InsertEvent) { + console.log(`BEFORE ENTITY INSERTED: `, event.entity); + } + + /** + * Called before entity insertion. + */ + beforeUpdate(event: UpdateEvent) { + console.log(`BEFORE ENTITY UPDATED: `, event.entity); + } + + /** + * Called before entity insertion. + */ + beforeRemove(event: RemoveEvent) { + console.log(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); + } + + /** + * Called after entity insertion. + */ + afterInsert(event: InsertEvent) { + console.log(`AFTER ENTITY INSERTED: `, event.entity); + } + + /** + * Called after entity insertion. + */ + afterUpdate(event: UpdateEvent) { + console.log(`AFTER ENTITY UPDATED: `, event.entity); + } + + /** + * Called after entity insertion. + */ + afterRemove(event: RemoveEvent) { + console.log(`AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); + } + + /** + * Called after entity is loaded. + */ + afterLoad(entity: any) { + console.log(`AFTER ENTITY LOADED: `, entity); + } + +} +``` + + +这个订阅类提供了一些常用的接口,用来在数据库操作时执行一些事情。 + +### 20、OrmConnectionHook + +在 3.4.0(不包含) 之前的版本中, Midway 封装提供了一种 Hook 机制,用于监听数据库连接与断连事件;代码如下。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { OrmConnectionHook, OrmHook } from '@midwayjs/orm'; +import { Connection, ConnectionOptions } from 'typeorm'; + +@Provide() +@OrmHook() +export class OrmConnectionListener implements OrmConnectionHook { + /** + * Called before connection create + * @param opts + * @returns + */ + async beforeCreate(opts?: ConnectionOptions): Promise { + console.log('BEFORE CONNECTION CREATE'); + return opts; + } + + /** + * Called after connection create + * @param conn + * @param opts + * @returns + */ + async afterCreate(conn?: Connection, opts?: ConnectionOptions): Promise { + console.log('AFTER CONNECTION CREATE'); + return conn; + } + + /** + * Called before connection close + * @param conn + * @param connectionName + * @returns + */ + async beforeClose(conn?: Connection, connectionName?: string): Promise { + console.log('BEFORE CONNECTION CLOSE'); + return conn; + } + + /** + * Called after connection close + * @param conn + * @returns + */ + async afterClose(conn?: Connection): Promise { + console.log('AFTER CONNECTION CLOSE'); + return conn; + } +} +``` + + +## 高级功能 + +### 多数据库支持 + + +有时候,我们一个应用中会有多个数据库连接(Connection)的情况,这个时候会有多个配置。我们使用**对象的形式**来定义配置。 + + +比如下面定义了 `default` 和 `test` 两个数据库连接(Connection)。 + + +```typescript +import {join} from 'path'; + +export default { + orm: { + default: { + type: 'sqlite', + database: join(__dirname, '../../default.sqlite'), + logging: true, + }, + test: { + type: 'mysql', + host: '127.0.0.1', + port: 3306, + username: '*********', + password: '*********', + database: undefined, + synchronize: true, + logging: false, + } + } +} +``` + + +在使用时,需要指定模型归属于哪个连接(Connection)。 + +```typescript +// entity/photo.ts +import { InjectEntityModel } from '@midwayjs/orm'; +import { User } from './model/user'; + +export class XXX { + + @InjectEntityModel(User, 'test') + testUserModel: Repository; + + //... +} +``` + + +同样的,在使用注入 Model 时,需要指定连接。 + + +```typescript +// entity/photo.ts +import { EntityModel } from '@midwayjs/orm'; + +@EntityModel('photo', { + connectionName: 'test' +}) +export class Photo { + id: number; + name: string; + description: string; + filename: string; + views: number; + isPublished: boolean; +} +``` + + + + +### 获取连接池 + +```typescript +import { Configuration } from '@midwayjs/core'; +import { getConnection } from 'typeorm'; + +@Configuration() +export class MainConfiguration { + async onReady() { + const conn = getConnection('default'); + console.log(conn.isConnected); + } +} +``` + + +### Hooks 场景支持 + + +针对函数式编程的场景,我们提供了简化的函数式写法。 + + +```typescript +import { useEntityModel } from '@midwayjs/orm'; +import { Photo } from './entity/photo'; + +export async function getPhoto() { + // get model + const photoModel = useEntityModel(Photo); + + const photo = new Photo(); + // create entity + photo.name = "Me and Bears"; + photo.description = "I am near polar bears"; + photo.filename = "photo-with-bears.jpg"; + photo.views = 1; + photo.isPublished = true; + + // find + const newPhoto = await photoModel.save(photo); + + return 'hello world'; +} +``` + + +### 关于表结构同步 + + +- 如果你已有表结构,想自动创建 Entity,使用 [生成器](../tool/typeorm_generator) +- 如果已经有 Entity 代码,想创建表结构请使用配置中的 `synchronize: true` 。 + +## 常见问题 + + +### Handshake inactivity timeout + + +一般是网络原因,如果本地出现,可以 ping 但是telnet不通,可以尝试执行如下命令: + +```bash +$ sudo sysctl -w net.inet.tcp.sack=0 +``` + +### 关于 mysql 时间列的当前时区展示 + + +如果使用 `@UpdateDateColumn` 和 `@CreateDateColumn` 列,一般情况下,数据库中保存的是 UTC 时间,如果你希望返回当前时区的时间,可以使用下面的方式。 + + +在配置时,开启时间转字符串的选项。 + +```typescript +// src/config/config.default.ts +export default { + // ... + orm: { + //... + dateStrings: true, + }, +} +``` + + +实体中的时间列需要列类型。 + +```typescript +@EntityModel() +export class Photo { + //... + @UpdateDateColumn({ + name: "gmt_modified", + type: 'timestamp' + }) + gmtModified: Date; + + @CreateDateColumn({ + name: "gmt_create", + type: 'timestamp' + }) + gmtCreate: Date; +} +``` + +这样,输出的时间字段就是当前的时区了。 + + +效果如下: + + +**配置前:** + +```typescript +gmtModified: 2021-12-13T03:49:43.000Z, +gmtCreate: 2021-12-13T03:49:43.000Z +``` + +**配置后:** + +```typescript +gmtModified: '2021-12-13 11:49:43', +gmtCreate: '2021-12-13 11:49:43' +``` + + + + +### 关于时间列的默认值 + + +如果使用 `@UpdateDateColumn` 和 `@CreateDateColumn` 列,那么注意,typeorm 是在建表语句中自动添加了默认值,如果表是用户自建的,该字段会由于没有默认值而写入 00:00:00 的时间。 + + +解决方案有两个 **1、修改表的默认值** 或者 **2、修改代码中列的默认值** + + +**如果不想修改表,而想修改代码,请参考下面的代码。** + +```typescript +@Column({ + default: () => "NOW()", + type: 'timestamp' +}) +createdOn: Date; + +@Column({ + default: () => "NOW()", + type: 'timestamp' +}) +modifiedOn: Date; +``` + + + +### 同时安装 mysql 和 mysql2 + +在 node_modules 中同时有 mysql 和 mysql2 时,typeorm 会自动加载 mysql,而不是 mysql2。 + +这个时候如需使用 mysql2,请指定 driver。 + +```typescript +// src/config/config.default.ts +export default { + // ... + orm: { + //... + type: 'mysql', + driver: require('mysql2'), + }, +} +``` + diff --git a/site/versioned_docs/version-3.0.0/legacy/sequelize.md b/site/versioned_docs/version-3.0.0/legacy/sequelize.md new file mode 100644 index 000000000000..c3e8d26faee4 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/legacy/sequelize.md @@ -0,0 +1,283 @@ +# Sequelize + +:::tip +本文档从 v3.4.0 版本起废弃。 +::: + +本文档介绍如何在 Midway 中使用 Sequelize 模块。 + +相关信息: + +| 描述 | | +| ----------------- | --- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ✅ | +| 可用于一体化 | ✅ | + +## 使用方法: + +```bash +$ npm i @midwayjs/sequelize@3 sequelize --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/sequelize": "^3.0.0", + "sequelize": "^6.13.0" + // ... + }, + "devDependencies": { + // ... + } +} +``` + +## 安装数据库 Driver + +常用数据库驱动如下,选择你对应连接的数据库类型安装: + +```bash +# for MySQL or MariaDB,也可以使用 mysql2 替代 +npm install mysql --save +npm install mysql2 --save + +# for PostgreSQL or CockroachDB +npm install pg --save + +# for SQLite +npm install sqlite3 --save + +# for Microsoft SQL Server +npm install mssql --save + +# for sql.js +npm install sql.js --save + +# for Oracle +npm install oracledb --save + +# for MongoDB(experimental) +npm install mongodb --save +``` + +## 引入模块 + +在 configuration.ts 文件中 + +```typescript +import { App, Configuration, ILifeCycle } from '@midwayjs/core'; +import { Application } from '@midwayjs/web'; +import { join } from 'path'; +import * as sequelize from '@midwayjs/sequelize'; + +@Configuration({ + imports: [sequelize], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration implements ILifeCycle { + @App() + app: Application; + + async onReady() {} +} +``` + +## 配置 + +在 config.default.ts 中配置: + +```typescript +// src/config/config.default.ts +export default { + // ... + sequelize: { + dataSource: { + default: { + database: 'test4', + username: 'root', + password: '123456', + host: '127.0.0.1', // 此处支持idb上面vipserver key的那种方式,也支持aliyun的地址。 + port: 3306, + encrypt: false, + dialect: 'mysql', + define: { charset: 'utf8' }, + timezone: '+08:00', + logging: console.log, + }, + }, + sync: false, // 本地的时候,可以通过sync: true直接createTable + }, +}; +``` + +## 业务层 + +### 定义 Entity + +```typescript +import { Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript'; +import { BaseTable } from '@midwayjs/sequelize'; +import { User } from './User'; + +@BaseTable +export class Photo extends Model { + @ForeignKey(() => User) + @Column({ + comment: '用户Id', + }) + userId: number; + @BelongsTo(() => User) user: User; + + @Column({ + comment: '名字', + }) + name: string; +} +``` + +```typescript +import { Model, Column, HasMany } from 'sequelize-typescript'; +import { BaseTable } from '@midwayjs/sequelize'; +import { Photo } from './Photo'; + +@BaseTable +export class User extends Model { + @Column name!: string; + @HasMany(() => Photo) Photo: Photo[]; +} +``` + +### 使用 Entity: + +#### 查询列表 + +```typescript +import { Config, Controller, Get, Provide } from '@midwayjs/core'; +import { Photo } from '../entity/Photo'; + +@Provide() +@Controller('/') +export class HomeController { + @Get('/') + async home() { + let result = await Photo.findAll(); + console.log(result); + return 'hello world'; + } +} +``` + +增加数据: + +```typescript +import { Controller, Post, Provide } from '@midwayjs/core'; +import { Photo } from '../entity/Photo'; + +@Provide() +@Controller('/') +export class HomeController { + @Post('/add') + async home() { + let result = await Photo.create({ + name: '123', + }); + console.log(result); + return 'hello world'; + } +} +``` + +#### 删除: + +```typescript +import { Controller, Post, Provide } from '@midwayjs/core'; +import { Photo } from '../entity/Photo'; + +@Provide() +@Controller('/') +export class HomeController { + @Post('/delete') + async home() { + await Photo.destroy({ + where: { + name: '123', + }, + }); + return 'hello world'; + } +} +``` + +#### 查找单个: + +```typescript +import { Controller, Post, Provide } from '@midwayjs/core'; +import { Photo } from '../entity/Photo'; + +@Provide() +@Controller('/') +export class HomeController { + @Post('/delete') + async home() { + let result = await Photo.findOne({ + where: { + name: '123', + }, + }); + return 'hello world'; + } +} +``` + +#### 联合查询: + +```typescript +import { Controller, Get, Provide } from '@midwayjs/core'; +import { Photo } from '../entity/Photo'; +import { Op } from 'sequelize'; + +@Provide() +@Controller('/') +export class HomeController { + @Get('/') + async home() { + // SELECT * FROM photo WHERE name = "23" OR name = "34"; + let result = await Photo.findAll({ + where: { + [Op.or]: [{ name: '23' }, { name: '34' }], + }, + }); + console.log(result); + return 'hello world'; + } +} +``` + +#### 连表查询 + +```typescript +import { Controller, Get, Provide } from '@midwayjs/core'; +import { User } from '../entity/User'; +import { Photo } from '../entity/Photo'; + +@Provide() +@Controller('/users') +export class HomeController { + @Get('/') + async home() { + let result = await User.findAll({ include: [Photo] }); + console.log(result); + return 'hello world'; + } +} +``` + +关于 OP 的更多用法:[https://sequelize.org/v5/manual/querying.html](https://sequelize.org/v5/manual/querying.html) + +midway + sequelize 完整使用案例 [https://github.com/ddzyan/midway-practice](https://github.com/ddzyan/midway-practice) + +如果遇到比较复杂的,可以使用 raw query 方法: +[https://sequelize.org/v5/manual/raw-queries.html](https://sequelize.org/v5/manual/raw-queries.html) diff --git a/site/versioned_docs/version-3.0.0/legacy/task.md b/site/versioned_docs/version-3.0.0/legacy/task.md new file mode 100644 index 000000000000..96dd1d35f272 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/legacy/task.md @@ -0,0 +1,580 @@ +# 任务调度 + +:::tip +本文档从 v3.6.0 版本起废弃。 +::: + +@midwayjs/task 是为了解决任务系列的模块,例如分布式定时任务、延迟任务调度。例如每日定时报表邮件发送、订单2小时后失效等工作。 + +分布式定时任务依赖 bull,其通过 redis 进行实现,所以配置中,需要配置额外的 Redis,本地定时任务基于 Cron 模块,不需要额外配置。 + +相关信息: + +| 描述 | | +| ----------------- | ---- | +| 可用于标准项目 | ✅ | +| 可用于 Serverless | ❌ | +| 可用于一体化 | ✅ | + +**其他** + +| 描述 | | +| -------------------- | ---- | +| 可作为主框架独立使用 | ✅ | +| 包含自定义日志 | ✅ | +| 可独立添加中间件 | ❌ | + + + +## 安装依赖 + +首先安装 Midway 提供的任务组件: + +```bash +$ npm install @midwayjs/task@3 @types/bull --save +``` + +或者在 `package.json` 中增加如下依赖后,重新安装。 + +```json +{ + "dependencies": { + "@midwayjs/task": "^3.0.0", + // ... + }, + "devDependencies": { + "@types/bull": "^3.15.8", + // ... + } +} +``` + + + +## 引入组件 + +在 `configuration.ts` 中,引入这个组件: + +```typescript +// src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as task from '@midwayjs/task'; // 导入模块 +import { join } from 'path'; + +@Configuration({ + imports: [task], + importConfigs: [join(__dirname, 'config')] +}) +export class MainConfiguration { +} +``` + + + +## 分布式定时任务 + +这是我们最常用的定时任务方式。 + +分布式定时任务,可以做到分布在多个进程,多台机器去执行单一定时任务方式。 + +分布式定义任务依赖 Redis 服务,需要提前申请。 + + + +### 配置 + +在 `config.default.ts` 文件中配置对应的模块信息: + +```typescript +// src/config/config.default.ts +export default { + // ... + task: { + redis: `redis://127.0.0.1:32768`, // 任务依赖redis,所以此处需要加一个redis + prefix: 'midway-task', // 这些任务存储的key,都是midway-task开头,以便区分用户原有redis里面的配置。 + defaultJobOptions: { + repeat: { + tz: "Asia/Shanghai" // Task等参数里面设置的比如(0 0 0 * * *)本来是为了0点执行,但是由于时区不对,所以国内用户时区设置一下。 + }, + }, + }, +} +``` + +有账号密码情况: + +```typescript +// src/config/config.default.ts +export default { + // ... + task: { + // ioredis的配置 https://www.npmjs.com/package/ioredis + redis: { + port: 6379, + host: '127.0.0.1', + password: 'foobared', + }, + prefix: 'midway-task', // 这些任务存储的 key,都是 midway-task 开头,以便区分用户原有redis 里面的配置。 + defaultJobOptions: { + repeat: { + tz: "Asia/Shanghai" // Task 等参数里面设置的比如(0 0 0 * * *)本来是为了0点执行,但是由于时区不对,所以国内用户时区设置一下。 + }, + }, + }, +} +``` + + + +### 代码使用 + +```typescript +import { Provide, Inject, Task, FORMAT } from '@midwayjs/core'; + +@Provide() +export class UserService { + @Inject() + helloService: HelloService; + + // 例如下面是每分钟执行一次,并且是分布式任务 + @Task({ + repeat: { cron: FORMAT.CRONTAB.EVERY_MINUTE} + }) + async test() { + console.log(this.helloService.getName()) + } +} +``` + +### 设置进度 + +例如我们在做音视频或者发布这种比较耗时的任务的时候,我们希望能设置进度。 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01WPYaAz21NgV3VNzjV_!!6000000006973-2-tps-576-454.png) + +相当于第二个参数,将 bull 的 job 传递给了用户。用户可以通过 `job.progress` 来设置进度。 + + +然后查询进度: + +```typescript +import { QueueService } from '@midwayjs/task'; +import { Provide, Controller, Get } from '@midwayjs/core'; + +@Controller() +export class HelloController{ + @Inject() + queueService: QueueService; + + @Get("/get-queue") + async getQueue(@Query() id: string){ + return await this.queueService.getClassQueue(TestJob).getJob(id); + } +} +``` + +### 任务的相关内容 + +```typescript +let job = await this.queueService.getClassQueue(TestJob).getJob(id) +``` + +然后 job 上面有类似停止的方法,或者查看进度的方法。 + + + +### 启动就触发 + + +有朋友由于只有一台机器,希望重启后立马能执行一下对应的定时任务。 + +```typescript +import { Configuration, Context, ILifeCycle, IMidwayBaseApplication, IMidwayContainer } from '@midwayjs/core'; +import { Queue } from 'bull'; +import { join } from 'path'; +import * as task from '@midwayjs/task'; +import { QueueService } from '@midwayjs/task'; + +@Configuration({ + imports: [ + task + ], + importConfigs: [ + join(__dirname, './config') + ] +}) +export class MainConfiguration implements ILifeCycle { + + async onServerReady(container: IMidwayContainer, app?: IMidwayBaseApplication): Promise { + + // Task这块的启动后立马执行 + let result: QueueService = await container.getAsync(QueueService); + // 此处第一个是你任务的类名,第二个任务的名字也就是装饰器Task的函数名 + let job: Queue = result.getQueueTask(`HelloTask`, 'task') + // 表示立即执行。 + job.add({}, {delay: 0, repeat: null}) + + // LocalTask的启动后立马执行 + const result = await container.getAsync(QueueService); + let job = result.getLocalTask(`HelloTask`, 'task'); // 参数1:类名 参数2: 装饰器TaskLocal的函数名 + job(); // 表示立即执行 + } +} + +``` + + + +## 常用 Cron 表达式 + +关于 Task 任务的配置: + +```typescript +* * * * * * +┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ | +│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) +│ │ │ │ └───── month (1 - 12) +│ │ │ └────────── day of month (1 - 31) +│ │ └─────────────── hour (0 - 23) +│ └──────────────────── minute (0 - 59) +└───────────────────────── second (0 - 59, optional) +``` + +常见表达式: + + +- 每隔5秒执行一次:`*/5 * * * * *` +- 每隔1分钟执行一次:`0 */1 * * * *` +- 每小时的20分执行一次:`0 20 * * * *` +- 每天 0 点执行一次:`0 0 0 * * *` +- 每天的两点35分执行一次:`0 35 2 * * *` + +可以使用 [在线工具](https://cron.qqe2.com/) 执行确认下一次执行的时间。 + + + +Midway 在框架侧提供了一些常用的表达式,放在 `@midwayjs/core` 中供大家使用。 + +```typescript +import { FORMAT } from '@midwayjs/core'; + +// 每分钟执行的 cron 表达式 +FORMAT.CRONTAB.EVERY_MINUTE +``` + +内置的还有一些其他的表达式。 + +| 表达式 | 对应时间 | +| ------------------------------ | --------------- | +| CRONTAB.EVERY_SECOND | 每秒钟 | +| CRONTAB.EVERY_MINUTE | 每分钟 | +| CRONTAB.EVERY_HOUR | 每小时整点 | +| CRONTAB.EVERY_DAY | 每天 0 点 | +| CRONTAB.EVERY_DAY_ZERO_FIFTEEN | 每天 0 点 15 分 | +| CRONTAB.EVERY_DAY_ONE_FIFTEEN | 每天 1 点 15 分 | +| CRONTAB.EVERY_PER_5_SECOND | 每隔 5 秒 | +| CRONTAB.EVERY_PER_10_SECOND | 每隔 10 秒 | +| CRONTAB.EVERY_PER_30_SECOND | 每隔 30 秒 | +| CRONTAB.EVERY_PER_5_MINUTE | 每隔 5 分钟 | +| CRONTAB.EVERY_PER_10_MINUTE | 每隔 10 分钟 | +| CRONTAB.EVERY_PER_30_MINUTE | 每隔 30 分钟 | + + + +## 手动触发任务 + +任务的定义,通过 `@Queue` 装饰器,定义一个任务类,必须含有一个 `async execute()` 方法。 +```typescript +import { Provide, Inject, Queue } from '@midwayjs/core'; + +@Queue() +export class HelloTask{ + async execute(params){ + console.log(params); + } +} +``` + + +触发: +```typescript +import { QueueService } from '@midwayjs/task'; +import { Provide, Inject } from '@midwayjs/core'; + +@Provide() +export class UserTask{ + @Inject() + queueService: QueueService; + + async execute(params = {}){ + // 3秒后触发分布式任务调度。 + const xxx = await this.queueService.execute(HelloTask, params, {delay: 3000}); + } +} +``` + 3 秒后,会触发 HelloTask 这个任务。 + +:::tip + +注意,如果没触发,请检查上面的 params,保证其不为空。 + +::: + + + +## 运维 + +### 日志 +在Midway Task Component上面,增加了两个日志: + +- midway-task.log +- midway-task-error.log + + +分别在task、localTask、queue触发开始和结束的时候会打印对应的日志。 + +task日志基本配置: +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; +export default { + midwayLogger: { + default: { + // ... + }, + clients: { + coreLogger: { + // ... + }, + appLogger: { + // ... + }, + taskLog: { + disableConsole: false, // 是否禁用打印到控制台,默认禁用 + level: 'warn', // 服务器默认warn + consoleLevel: 'warn', + }, + } + }, +} as MidwayConfig; +``` +分布式的Task触发日志: +```typescript +logger.info(`task start.`) + +// 异常情况: +logger.error(err.stack) + +logger.info(`task end.`) +``` +非分布式的LocalTask触发日志: +```typescript +logger.info(`local task start.`) + +// 异常情况: +// logger.error(`${e.stack}`) + +logger.info(`local task end.`) +``` + + +任务队列的触发日志: +```typescript +logger.info(`queue process start.`) + +// 异常情况: +// logger.error(`${e.stack}`) + +logger.info(`queue process end.`) +``` + + +### 排查问题链路: +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01xL1mQE25kMZnB5ygb_!!6000000007564-2-tps-1614-847.png) +用户可以搜索这个相同的id,找到同一次请求的日志。 +为了方便用户在自己的业务代码中串联对应的日志,我在ctx上面挂了traceId变量。 + +例如异常情况:当异常的时候,**本地可以在控制台和 midway-task.log 栏内看到这个错误相关的情况:** + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01WYBjbL1lGKHmsdSnH_!!6000000004791-2-tps-1964-324.png) + + + +### traceId + +localTask 则是自己生成了一个 uuid 的 id 作为 traceId。 + + +task 和 queue 则采用 job 的 id 作为 traceId。 + + + +### 业务内部的代码 + +在 service 内可以通过 inject 注入 logger,或者注入 ctx 拿 logger 变量 +```typescript +import { App, Inject, Provide, Queue } from '@midwayjs/core'; +import { Application } from "@midwayjs/koa"; + +@Queue() +export class QueueTask{ + + @App() + app: Application; + + @Inject() + logger; + + async execute(params){ + this.logger.info(`====>QueueTask execute`) + this.app.getApplicationContext().registerObject(`queueConfig`, JSON.stringify(params)); + } +} + +``` +或者 +```typescript +import { App, Inject, Provide, Queue } from '@midwayjs/core'; +import { Application } from "@midwayjs/koa"; + +@Queue() +export class QueueTask{ + + @App() + app: Application; + + @Inject() + ctx; + + async execute(params){ + this.ctx.logger.info(`====>QueueTask execute`) + this.app.getApplicationContext().registerObject(`queueConfig`, JSON.stringify(params)); + } +} + +``` + + +打印的日志 +```typescript +2021-07-30 13:00:13,101 INFO 5577 [Queue][12][QueueTask] queue process start. +2021-07-30 13:00:13,102 INFO 5577 [Queue][12][QueueTask] ====>QueueTask execute +2021-07-30 13:00:13,102 INFO 5577 [Queue][12][QueueTask] queue process end. +``` + + + +## 本地定时任务 + +本地定时任务和分布式任务不同,无需依赖和配置 Redis,只能做到单进程的事情,即每台机器的每个进程都会被执行。 + +```typescript +import { Provide, Inject, TaskLocal, FORMAT } from '@midwayjs/core'; + +@Provide() +export class UserService { + @Inject() + helloService: HelloService; + + // 例如下面是每分钟执行一次 + @TaskLocal(FORMAT.CRONTAB.EVERY_MINUTE) + async test(){ + console.log(this.helloService.getName()) + } +} +``` + + + + + +## 常见问题 + + + +### 1、EVALSHA错误 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01KfjCKT1yypmNPDkIL_!!6000000006648-2-tps-3540-102.png) + +这个问题基本明确,问题会出现在 redis 的集群版本上。原因是 redis 会对 key 做 hash 来确定存储的 slot,集群下这一步 @midwayjs/task 的 key 命中了不同的 slot。临时的解决办法是 task 里的 prefix 配置用 {} 包括,强制 redis 只计算 {} 里的hash,例如 `prefix: '{midway-task}'`。 + + + +### 2、历史日志删除 + +当每次redis执行完他会有日志,那么如何让其在完成后删除: +```typescript +import { Provide, Task } from '@midwayjs/core'; +import { IUserOptions } from '../interface'; + +@Provide() +export class UserService { + async getUser(options: IUserOptions) { + return { + uid: options.uid, + username: 'mockedName', + phone: '12345678901', + email: 'xxx.xxx@xxx.com', + }; + } + + @Task({ + repeat: { cron: '* * * * * *'}, + removeOnComplete: true // 加了一行这个 + }) + async test(){ + console.log(`====`) + } +} + +``` +目前是否默认删除,需要跟用户沟通。 + + + +### 3、配置 Redis 集群 + +你可以使用 bull 提供的 `createClient` 方式来接入自定义的 redis 实例,这样你可以接入 Redis 集群。 + +比如: + +```typescript +// src/config/config.default +import Redis from 'ioredis'; + +const clusterOptions = { + enableReadyCheck: false, // 一定要是false + retryDelayOnClusterDown: 300, + retryDelayOnFailover: 1000, + retryDelayOnTryAgain: 3000, + slotsRefreshTimeout: 10000, + maxRetriesPerRequest: null // 一定要是null +} + +const redisClientInstance = new Redis.Cluster([ + { + port: 7000, + host: '127.0.0.1' + }, + { + port: 7002, + host: '127.0.0.1' + }, +], clusterOptions); + +export default { + task: { + createClient: (type, opts) => { + return redisClientInstance; + }, + prefix: '{midway-task}', // 这些任务存储的key,都是相同开头,以便区分用户原有redis里面的配置。 + defaultJobOptions: { + repeat: { + tz: "Asia/Shanghai" // Task等参数里面设置的比如(0 0 0 * * *)本来是为了0点执行,但是由于时区不对,所以国内用户时区设置一下。 + } + } + } +} +``` + diff --git a/site/versioned_docs/version-3.0.0/lifecycle.md b/site/versioned_docs/version-3.0.0/lifecycle.md new file mode 100644 index 000000000000..c9c22711fb92 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/lifecycle.md @@ -0,0 +1,448 @@ +# 生命周期 + +在通常情况下,我们希望在应用启动的时候做一些初始化、或者其他一些预处理的事情,比如创建数据库连接、预生成一些配置,而不是在请求响应时去处理。 + + + +## 项目生命周期 + +框架提供了这些生命周期函数供开发人员处理: + +- 配置文件加载,我们可以在这里去修改配置(`onConfigLoad`) +- 依赖注入容器准备完毕,可以在这个阶段做大部分的事情(`onReady`) +- 服务启动完成,可以拿到 server(`onServerReady`) +- 应用即将关闭,在这里清理资源(`onStop`) + + +Midway 的生命周期是通过 `src/configuration.ts` 文件,实现 ILifeCycle 接口,就可以在项目启动时候自动加载。 + + +接口定义如下。 + + +```typescript +interface ILifeCycle { + /** + * 在应用配置加载后执行 + */ + onConfigLoad?(container: IMidwayContainer, app: IMidwayApplication): Promise; + + /** + * 在依赖注入容器 ready 的时候执行 + */ + onReady(container: IMidwayContainer, app: IMidwayApplication): Promise; + + /** + * 在应用服务启动后执行 + */ + onServerReady?(container: IMidwayContainer, app: IMidwayApplication): Promise; + + /** + * 在应用停止的时候执行 + */ + onStop?(container: IMidwayContainer, app: IMidwayApplication): Promise; + + /** + * 在健康检查时执行 + */ + onHealthCheck?(container: IMidwayContainer): Promise; +} +``` + + + +### onConfigLoad + +一般用于修改项目的配置文件。 + +举个例子。 + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onConfigLoad(): Promise { + // 直接返回数据,会自动合并到配置中 + return { + test: 1 + } + } +} +``` + +这个时候,`@Config` 拿到的配置就包含了返回的数据,具体可以参考 [异步初始化配置](./env_config#异步初始化配置) 章节。 + + + +### onReady + +onReady 是一个大部分场景下都会使用到的生命周期。 + +:::info +注意,这里的 ready 指的是依赖注入容器 ready,并不是应用 ready,所以你可以对应用做任意扩展,比如添加中间件,连接数据库等等。 +::: + + +我们需要在初始化时提前连接一个数据库,由于在类中,所以也可以通过 `@Inject` 装饰器注入 db 这样一个数据库的连接工具类,这个实例包含 connect 和 close 两个函数: + + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + @Inject() + db: any; + + async onReady(container: IMidwayContainer): Promise { + // 建立数据库连接 + await this.db.connect(); + } + + async onStop(): Promise { + // 关闭数据库连接 + await this.db.close(); + } +} +``` + + +这样,我们就能够在应用启动时建立数据库连接,而不是在请求响应时再去创建。同时,在应用停止时,也可以优雅的关闭数据库连接。 + + +除此之外,通过这个方式,可以对默认注入的对象做扩充。 + + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer } from '@midwayjs/core'; +import * as sequelize from 'sequelize'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onReady(container: IMidwayContainer): Promise { + // 三方包对象 + container.registerObject('sequelize', sequelize); + } +} +``` + + +在其他的类中可以直接注入使用。 + + +```typescript +export class IndexHandler { + + @Inject() + sequelize; + + async handler() { + console.log(this.sequelize); + } +} +``` + + + +### onServerReady + +当要获取框架的服务对象,端口等信息时,就需要用到这个生命周期。 + +我们以 `@midwayjs/koa` 为例,在启动时获取它的 Server。 + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + imports: [koa] +}) +export class MainConfiguration implements ILifeCycle { + + async onServerReady(container: IMidwayContainer): Promise { + // 获取到 koa 中暴露的 Framework + const framework = await container.getAsync(koa.Framework); + const server = framework.getServer(); + // ... + + } +} +``` + + + +### onStop + +我们可以在这个阶段清理一些资源,比如关闭连接等。 + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; + +@Configuration({ + imports: [koa] +}) +export class MainConfiguration implements ILifeCycle { + @Inject() + db: any; + + async onReady(container: IMidwayContainer): Promise { + // 建立数据库连接 + await this.db.connect(); + } + + async onStop(): Promise { + // 关闭数据库连接 + await this.db.close(); + } +} +``` + + + +### onHealthCheck + +当内置的健康检查服务调用状态获取 API 时,所有组件的该方法都被自动执行。 + +下面模拟了一个 db 健康检查的方法。 + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, HealthResult } from '@midwayjs/core'; + +@Configuration({ + namespace: 'db' +}) +export class MainConfiguration implements ILifeCycle { + @Inject() + db: any; + + async onReady(container: IMidwayContainer): Promise { + await this.db.connect(); + } + + async onHealthCheck(): Promise { + try { + const result = await this.db.isConnect(); + if (result) { + return { + status: true, + }; + } else { + return { + status: false, + reason: 'db is disconnect', + }; + } + } catch (err) { + return { + status: false, + reason: err.message, + }; + } + } +} +``` + +上述 `onHealthCheck` 中,调用了一个 `isConnect` 的状态检查,根据结果返回了固定的 `HealthResult` 类型格式。 + +注意,外部调用 `onHealthCheck` 可能会非常频繁,请尽可能保持检查逻辑的可靠性和效率,确保不会对检查依赖有较大的压力。同时请自行处理检查超时后资源释放的逻辑,避免资源频繁请求却未返回结果,导致内存泄露的风险。 + + + +## 全局对象生命周期 + +所谓对象生命周期,指的是每个对象,在依赖注入容器中创建,销毁的事件。我们通过这些生命周期,可以在对象创建后,销毁时做一些操作。 + +```typescript +export interface IObjectLifeCycle { + onBeforeObjectCreated(/**...**/); + onObjectCreated(/**...**/); + onObjectInit(/**...**/); + onBeforeObjectDestroy(/**...**/); +} +``` + +`ILifeCycle` 定义中已经包含了这些阶段。 + +:::caution + +注意,对象生命周期 API 会影响整个依赖注入容器以及业务的使用,请谨慎操作。 + +::: + +### onBeforeObjectCreated + +在业务对象实例创建前执行,框架内部的某些对象由于已经初始化,无法被拦截。 + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectBeforeCreatedOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onBeforeObjectCreated(Clzz: new (...args), options: ObjectBeforeCreatedOptions): Promise { + // ... + } +} +``` + +这里入参有两个参数: + +- `Clzz` 当前待创建对象的原型类 +- `options` 一些参数 + +参数如下: + +| 属性 | 类型 | 描述 | +| ----------------------- | ----------------- | ---------------- | +| options.context | IMidwayContainer | 依赖注入容器本身 | +| options.definition | IObjectDefinition | 对象定义 | +| options.constructorArgs | any[] | 构造器入参 | + + + +### onObjectCreated + +在对象实例创建后执行,这个阶段可以替换创建的对象。 + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectCreatedOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onObjectCreated(ins: any, options: ObjectCreatedOptions): Promise { + // ... + } +} +``` + +这里入参有两个参数: + +- `ins` 当前通过构建器创出来的对象 +- `options` 一些参数 + +参数如下: + +| 属性 | 类型 | 描述 | +| ----------------------- | ------------------ | ------------------ | +| options.context | IMidwayContainer | 依赖注入容器本身 | +| options.definition | IObjectDefinition | 对象定义 | +| options.replaceCallback | (ins: any) => void | 对象替换的回调方法 | + +**示例:动态添加属性** + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectInitOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onObjectCreated(ins: any, options: ObjectInitOptions): Promise { + // 每个创建的对象都会添加一个 _name 的属性 + ins._name = 'xxxx'; + // ... + } +} +``` + +**示例:替换对象** + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectInitOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onObjectCreated(ins: any, options: ObjectInitOptions): Promise { + // 之后每个创建的对象都会被替换为 { bbb: 'aaa' } + options.replaceCallback({ + bbb: 'aaa' + }); + + // ... + } +} +``` + + + +### onObjectInit + +在对象实例创建后执行异步初始化方法后执行。 + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectInitOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onObjectInit(ins: any, options: ObjectInitOptions): Promise { + // ... + } +} +``` + +这里入参有两个参数: + +- `ins` 当前通过构建器创出来的对象 +- `options` 一些参数 + +参数如下: + +| 属性 | 类型 | 描述 | +| ------------------ | ----------------- | ---------------- | +| options.context | IMidwayContainer | 依赖注入容器本身 | +| options.definition | IObjectDefinition | 对象定义 | + +:::info + +在这个阶段也可以动态给对象附加属性,方法等,和 `onObjectCreated` 的区别是,这个阶段是在初始化方法执行之后。 + +::: + + + +### onBeforeObjectDestroy + +在对象实例销毁前执行。 + +```typescript +// src/configuration.ts +import { Configuration, ILifeCycle, IMidwayContainer, ObjectBeforeDestroyOptions } from '@midwayjs/core'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + async onBeforeObjectDestroy(ins: any, options: ObjectBeforeDestroyOptions): Promise { + // ... + } +} +``` + +这里入参有两个参数: + +- `ins` 当前通过构建器创出来的对象 +- `options` 一些参数 + +参数如下: + +| 属性 | 类型 | 描述 | +| ------------------ | ----------------- | ---------------- | +| options.context | IMidwayContainer | 依赖注入容器本身 | +| options.definition | IObjectDefinition | 对象定义 | + diff --git a/site/versioned_docs/version-3.0.0/logger.md b/site/versioned_docs/version-3.0.0/logger.md new file mode 100644 index 000000000000..b3954dbf5ee0 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/logger.md @@ -0,0 +1,894 @@ +# 日志(v2) + +:::tip + +本文档为 `@midwayjs/logger` v2.0 版本的文档。 + +::: + +Midway 为不同场景提供了一套统一的日志接入方式。通过 `@midwayjs/logger` 包导出的方法,可以方便的接入不同场景的日志系统。 + +Midway 的日志系统基于社区的 [winston](https://github.com/winstonjs/winston),是现在社区非常受欢迎的日志库。 + +实现的功能有: + +- 日志分级 +- 按大小和时间自动切割 +- 自定义输出格式 +- 统一错误日志 + + + +## 日志路径和文件 + +Midway 会在日志根目录创建一些默认的文件。 + + +- `midway-core.log` 框架、组件打印信息的日志,对应 `coreLogger` 。 +- `midway-app.log` 应用打印信息的日志,对应 `appLogger` +- `common-error.log` 所有错误的日志(所有 Midway 创建出来的日志,都会将错误重复打印一份到该文件中) + +本地开发和服务器部署时的 **日志路径** 和 **日志等级** 不同,具体请参考 [配置日志根目录](#配置日志根目录) 和 [框架的默认等级](#框架的默认等级)。 + + + +## 默认日志对象 + +Midway 默认在框架提供了三种不同的日志,对应三种不同的行为。 + +| 日志 | 释义 | 描述 | 常见使用 | +| ----------------------------------- | -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| coreLogger | 框架,组件层面的日志 | 默认会输出控制台日志和文本日志 `midway-core.log` ,并且默认会将错误日志发送到 `common-error.log` 。 | 框架和组件的错误,一般会打印到其中。 | +| appLogger | 业务层面的日志 | 默认会输出控制台日志和文本日志 `midway-app.log` ,并且默认会将错误日志发送到 `common-error.log` 。 | 业务使用的日志,一般业务日志会打印到其中。 | +| 上下文日志(复用 appLogger 的配置) | 请求链路的日志 | 默认使用 `appLogger` 进行输出,除了会将错误日志发送到 `common-error.log` 之外,还增加了上下文信息。 | 修改日志输出的标记(Label),不同的框架有不同的请求标记,比如 HTTP 下就会输出路由信息。 | + + + +## 使用日志 + +Midway 的常用日志使用方法。 + +### 上下文日志 + +上下文日志是关联框架上下文对象(Context) 的日志。 + +我们可以通过 [获取到 ctx 对象](./req_res_app) 后,使用 `ctx.logger` 对象进行日志打印输出。 + +比如: + +```typescript +ctx.logger.info("hello world"); +ctx.logger.debug('debug info'); +ctx.logger.warn('WARNNING!!!!'); + +// 错误日志记录,直接会将错误日志完整堆栈信息记录下来,并且输出到 errorLog 中 +// 为了保证异常可追踪,必须保证所有抛出的异常都是 Error 类型,因为只有 Error 类型才会带上堆栈信息,定位到问题。 +ctx.logger.error(new Error('custom error')); +``` + +在执行后,我们能在两个地方看到日志输出: + + +- 控制台看到输出。 +- 日志目录的 midway-app.log 文件中 + + +输出结果: + +```text +2021-07-22 14:50:59,388 INFO 7739 [-/::ffff:127.0.0.1/-/0ms GET /api/get_user] hello world +``` + +在注入的形式中,我们也可以直接使用 `@Inject() logger` 的形式来注入 `ctx.logger` ,和直接调用 `ctx.logger` 等价。 + +比如: + +```typescript +import { Get, Inject, Controller, Provide } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Controller() +export class HelloController { + + @Inject() + logger: ILogger; + + @Inject() + ctx; + + @Get("/") + async hello(){ + // ... + + // this.logger === ctx.logger + } +} +``` + + + +### 应用日志(App Logger) + +如果我们想做一些应用级别的日志记录,如记录启动阶段的一些数据信息,可以通过 App Logger 来完成。 + +```typescript +import { Configuration, Logger } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + @Logger() + logger: ILogger; + + async onReady(container: IMidwayContainer): Promise { + this.logger.debug('debug info'); + this.logger.info('启动耗时 %d ms', Date.now() - start); + this.logger.warn('warning!'); + + this.logger.error(someErrorObj); + } + +} +``` + +注意,这里使用的是 `@Logger()` 装饰器。 + + + +### CoreLogger + +在组件或者框架层面的研发中,我们会使用 coreLogger 来记录日志。 + +```typescript +@Configuration() +export class MainConfiguration implements ILifeCycle { + + @Logger('coreLogger') + logger: ILogger; + + async onReady(container: IMidwayContainer): Promise { + this.logger.debug('debug info'); + this.logger.info('启动耗时 %d ms', Date.now() - start); + this.logger.warn('warning!'); + + this.logger.error(someErrorObj); + } + +} +``` + + + + + + +## 输出方法和格式 + + +Midway 的日志对象继承与 winston 的日志对象,一般情况下,只提供 `error()` , `warn()` , `info()` , `debug` 四种方法。 + + +示例如下。 + +```typescript +logger.debug('debug info'); +logger.info('启动耗时 %d ms', Date.now() - start); +logger.warn('warning!'); +logger.error(new Error('my error')); +``` + + +### 默认的输出行为 + + +在大部分的普通类型下,日志库都能工作的很好。 + + +比如: + +```typescript +logger.info('hello world'); // 输出字符串 +logger.info(123); // 输出数字 +logger.info(['b', 'c']); // 输出数组 +logger.info(new Set([2, 3, 4])); // 输出 Set +logger.info(new Map([['key1', 'value1'], ['key2', 'value2']])); // 输出 Map +``` + +> Midway 针对 winston 无法输出的 `Array` , `Set` , `Map` 类型,做了特殊定制,使其也能够正常的输出。 + + +不过需要注意的是,日志对象在一般情况下,只能传入一个参数,它的第二个参数有其他作用。 + +```typescript +logger.info('plain error message', 321); // 会忽略 321 +``` + + +### 错误输出 + + +针对错误对象,Midway 也对 winston 做了定制,使其能够方便的和普通文本结合到一起输出。 + +```typescript +// 输出错误对象 +logger.error(new Error('error instance')); + +// 输出自定义的错误对象 +const error = new Error('named error instance'); +error.name = 'NamedError'; +logger.error(error); + +// 文本在前,加上 error 实例 +logger.info('text before error', new Error('error instance after text')); +``` + +:::caution +注意,错误对象只能放在最后,且有且只有一个,其后面的所有参数都会被忽略。 +::: + + + + +### 格式化内容 + +基于 `util.format` 的格式化方式。 + +```typescript +logger.info('%s %d', 'aaa', 222); +``` + +常用的有 + + +- `%s` 字符串占位 +- `%d` 数字占位 +- `%j` json 占位 + +更多的占位和详细信息,请参考 node.js 的 [util.format](https://nodejs.org/dist/latest-v14.x/docs/api/util.html#util_util_format_format_args) 方法。 + + + +### 输出自定义对象或者复杂类型 + + +基于性能考虑,Midway(winston)大部分时间只会输出基本类型,所以当输出的参数为高级对象时,**需要用户手动转换为需要打印的字符串**。 + + +如下示例,将不会得到希望的结果。 + +```typescript +const obj = {a: 1}; +logger.info(obj); // 默认情况下,输出 [object Object] +``` + +需要手动输出希望打印的内容。 + +```typescript +const obj = {a: 1}; +logger.info(JSON.stringify(obj)); // 可以输出格式化文本 +logger.info(obj.a); // 直接输出属性值 +logger.info('%j', a); // 直接占位符输出整个 json +``` + + + +### 纯输出内容 + + +特殊场景下,我们需要单纯的输出内容,不希望输出时间戳,label 等和格式相关的信息。这种需求我们可以使用 `write` 方法。 + +`write` 方法是个非常底层的方法,并且不管什么级别的日志,它都会写入到文件中。 + + +虽然 `write` 方法在每个 logger 上都有,但是我们只在 `IMidwayLogger` 定义中提供它,我们希望你能明确的知道自己希望调用它。 + +```typescript +(logger as IMidwayLogger).write('hello world'); // 文件中只会有 hello world +``` + + + +## 日志类型定义 + + +默认的情况,用户应该使用最简单的 `ILogger` 定义。 + +```typescript +import { Provide, Logger } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Provide() +export class UserService { + + @Inject() + logger: ILogger; // 获取上下文日志 + + async getUser() { + this.logger.info('hello user'); + } + +} +``` + + +`ILogger` 定义只提供最简单的 `debug` , `info` , `warn` 以及 `error` 方法。 + + +在某些场景下,我们需要更为复杂的定义,比如修改日志属性或者动态调节,这个时候需要使用更为复杂的 `IMidwayLogger` 定义。 + + +```typescript +import { Provide, Logger } from '@midwayjs/core'; +import { IMidwayLogger } from '@midwayjs/logger'; + +@Provide() +export class UserService { + + @Inject() + logger: IMidwayLogger; // 获取上下文日志 + + async getUser() { + this.logger.disableConsole(); // 禁止控制台输出 + this.logger.info('hello user'); // 这句话在控制台看不到 + this.logger.enableConsole(); // 开启控制台输出 + this.logger.info('hello user'); // 这句话在控制台可以看到 + } + +} +``` + +`IMidwayLogger` 的定义可以参考 interface 中的描述,或者查看 [代码](https://github.com/midwayjs/logger/blob/main/src/interface.ts)。 + + + +## 日志基本配置 + +我们可以在配置文件中配置日志的各种行为。 + +Midway 中的的日志配置包含 **全局配置** 和 **单个日志配置** 两个部分,两者配置会合并和覆盖。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + // ... + }, + clients: { + coreLogger: { + // ... + }, + appLogger: { + // ... + } + } + }, +} as MidwayConfig; +``` + +如上所述,`clients` 配置段中的每个对象都是一个独立的日志配置项,其配置会和 `default` 段落合并后创建 logger 实例。 + +如果你发现没有定义,请将 `@midawyjs/logger` 在 `src/interface.ts` 中显式声明一次。 + +```typescript +// ... +import type {} from '@midwayjs/logger'; +``` + + + + + +## 配置日志等级 + + +winston 的日志等级分为下面几类,日志等级依次降低(数字越大,等级越低): + +```typescript +const levels = { + none: 0, + error: 1, + trace: 2, + warn: 3, + info: 4, + verbose: 5, + debug: 6, + silly: 7, + all: 8, +} +``` + +在 Midway 中,为了简化,一般情况下,我们只会使用 `error` , `warn` , `info` , `debug` 这四种等级。 + +日志等级表示当前可输出日志的最低等级。比如当你的日志 level 设置为 `warn` 时,仅 `warn` 以及更高的 `error` 等级的日志能被输出。 + +在 Midway 中,针对不同的输出行为,可以配置不同的日志等级。 + +- `level` 写入文本的日志等级 +- `consoleLevel` 控制台输出的日志等级 + + + +### 框架的默认等级 + + +在 Midway 中,有着自己的默认日志等级。 + + +- 在开发环境下(local,test,unittest),文本和控制台日志等级统一为 `info` 。 +- 在服务器环境(除开发环境外),为减少日志数量,`coreLogger` 日志等级为 `warn` ,而其他日志为 `info`。 + + + +### 调整日志等级 + +一般情况下,我们不建议调整全局默认的日志等级,而是调整特定的 logger 的日志等级,比如: + +调整 `coreLogger` 或者 `appLogger` 。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + clients: { + coreLogger: { + level: 'warn', + consoleLevel: 'warn' + // ... + }, + appLogger: { + level: 'warn', + consoleLevel: 'warn' + // ... + } + } + }, +} as MidwayConfig; +``` + +特殊场景,也可以临时调整全局的日志等级。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + level: 'info', + consoleLevel: 'warn' + }, + // ... + }, +} as MidwayConfig; +``` + + + +## 配置日志根目录 + +默认情况下,Midway 会在本地开发和服务器部署时输出日志到 **日志根目录**。 + + +- 本地的日志根目录为 `${app.appDir}/logs/项目名` 目录下 +- 服务器的日志根目录为用户目录 `${process.env.HOME}/logs/项目名` (Linux/Mac)以及 `${process.env.USERPROFILE}/logs/项目名` (Windows)下,例如 `/home/admin/logs/example-app`。 + +我们可以配置日志所在的根目录。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + dir: '/home/admin/logs', + }, + // ... + }, +} as MidwayConfig; +``` + + + +## 配置日志切割(轮转) + + +默认行为下,同一个日志对象 **会生成两个文件**。 + +以 `midway-core.log` 为例,应用启动时会生成一个带当日时间戳 `midway-core.YYYY-MM-DD` 格式的文件,以及一个不带时间戳的 `midway-core.log` 的软链文件。 + +> windows 下不会生成软链 + + +为方便配置日志采集和查看,该软链文件永远指向最新的日志文件。 + + +当凌晨 `00:00` 时,会生成一个以当天日志结尾 `midway-core.log.YYYY-MM-DD` 的形式的新文件。 + +同时,当单个日志文件超过 200M 时,也会自动切割,产生新的日志文件。 + +可以通过配置调整按大小的切割行为。 + +```typescript +export default { + midwayLogger: { + default: { + maxSize: '100m', + }, + // ... + }, +} as MidwayConfig; +``` + + + +## 配置日志清理 + +默认情况下,日志会存在 31 天。 + +可以通过配置调整该行为,比如改为保存 3 天。 + +```typescript +export default { + midwayLogger: { + default: { + maxFiles: '3d', + }, + // ... + }, +} as MidwayConfig; +``` + + + + + + +## 高级配置 + +如果用户不满足于默认的日志对象,也可以自行创建和修改。 + + + +### 增加自定义日志 + +可以如下配置: + +```typescript +export default { + midwayLogger: { + clients: { + abcLogger: { + fileLogName: 'abc.log' + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +自定义的日志可以通过 `@Logger('abcLogger')` 获取。 + +更多的日志选项可以参考 interface 中 [LoggerOptions 描述](https://github.com/midwayjs/logger/blob/main/src/interface.ts)。 + + + +### 配置日志输出格式 + + +显示格式指的是日志输出时单行文本的字符串结构。Midway 对 Winston 的日志做了定制,提供了一些默认对象。 + +每个 logger 对象,都可以配置一个输出格式,显示格式是一个返回字符串结构的方法,参数为 Winston 的 [info 对象](https://github.com/winstonjs/logform#info-objects)。 + +```typescript +export default { + midwayLogger: { + clients: { + appLogger: { + format: info => { + return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.labelText}${info.message}`; + } + // ... + }, + customOtherLogger: { + format: info => { + return 'xxxx'; + } + } + } + // ... + }, +} as MidwayConfig; +``` + +info 对象的默认属性如下: + +| **属性名** | **描述** | **示例** | +| ----------- | ------------------------------------------------ | ------------------------------------------------------------ | +| timestamp | 时间戳,默认为 `'YYYY-MM-DD HH:mm:ss,SSS` 格式。 | 2020-12-30 07:50:10,453 | +| level | 小写的日志等级 | info | +| LEVEL | 大写的日志等级 | INFO | +| pid | 当前进程 pid | 3847 | +| labelText | 标签的聚合文本 | [abcde] | +| message | 普通消息 + 错误消息 + 错误堆栈的组合 | 1、普通文本,如 `123456` , `hello world`
2、错误文本(错误名+堆栈)Error: another test error at Object.anonymous (/home/runner/work/midway/midway/packages/logger/test/index.test.ts:224:18)
3、普通文本+错误文本 hello world Error: another test error at Object.anonymous (/home/runner/work/midway/midway/packages/logger/test/index.test.ts:224:18) | +| stack | 错误堆栈 | | +| originError | 原始错误对象 | 错误实例本身 | +| originArgs | 原始的用户入参 | [ 'a', 'b', 'c' ] | + + + +### 获取自定义上下文日志 + +上下文日志是基于 **原始日志对象** 来打日志的,会复用原始日志的所有格式,他们的关系如下。 + +```typescript +// 伪代码 +const contextLogger = customLogger.createContextLogger(ctx); +``` + +`@Inject` 只能注入默认的上下文日志,我们可以通过 `ctx.getLogger` 方法获取其他 **自定义日志** 对应的 **上下文日志**。上下文日志和 ctx 关联,同一个上下文会相同的 key 会获取到同一个日志对象,当 ctx 被销毁,日志对象也会被回收。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { IMidwayLogger } from '@midwayjs/logger'; +import { Context } from '@midwayjs/koa'; + +@Provide() +export class UserService { + + @Inject() + ctx: Context; + + async getUser() { + // 这里获取的是 customLogger 对应的上下文日志对象 + const customLogger = this.ctx.getLogger('customLogger'); + customLogger.info('hello world'); + } + +} +``` + + + + +### 配置上下文日志输出格式 + +上下文日志是基于 **原始日志对象** 来打日志的,会复用原始日志的所有格式,但是我们可以单独配置日志对象的对应的上下文日志格式。 + +上下文日志的 info 对象中多了 ctx 对象,我们以修改 `customLogger` 的上下文日志为例。 + +```typescript +export default { + midwayLogger: { + clients: { + customLogger: { + contextFormat: info => { + const ctx = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} [${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`; + } + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +则你在使用上下文日志输出时,会默认变成你 format 的样子。 + +```typescript +ctx.getLogger('customLogger').info('hello world'); +// 2021-01-28 11:10:19,334 INFO 9223 [2ms POST] hello world +``` + +注意,由于 `App Logger` 是所有框架默认的日志对象,较为特殊,现有部分框架默认配置了其上下文格式,导致在 `midwayLogger` 字段中配置无效。 + +为此你需要单独修改某一框架的上下文日志格式配置,请跳转到不同的框架查看。 + +- [修改 koa 的上下文日志格式](./extensions/koa#修改上下文日志) +- [修改 egg 的上下文日志格式](./extensions/egg#修改上下文日志) +- [修改 express 的上下文日志格式](./extensions/express#修改上下文日志) + + + +### 日志默认 Transport + +每个日志包含几个默认的 Transport。 + +| 名称 | 默认行为 | 描述 | +| ----------------- | -------- | ------------------------------ | +| Console Transport | 开启 | 用于输出到控制台 | +| File Transport | 开启 | 用于输出到文本文件 | +| Error Transport | 开启 | 用于将错误输出到特定的错误日志 | +| JSON Transport | 关闭 | 用于输出 JSON 格式的文本 | + +可以通过配置进行修改。 + +**示例:只开启控制台输出** + +```typescript +export default { + midwayLogger: { + clients: { + abcLogger: { + enableFile: false, + enableError: false, + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +**示例:关闭控制台输出** + +```typescript +export default { + midwayLogger: { + clients: { + abcLogger: { + enableConsole: false, + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +**示例:开启文本和 JSON 同步输出,关闭错误输出** + +```typescript +export default { + midwayLogger: { + clients: { + abcLogger: { + enableConsole: false, + enableFile: true, + enableError: false, + enableJSON: true, + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + + + +### 自定义 Transport + +框架提供了扩展 Transport 的功能,比如,你可以写一个 Transport 来做日志的中转,上传到别的日志库等能力。 + +比如下面的示例,我们就将日志中转到另一个本地文件中。 + +```typescript +import { EmptyTransport } from '@midwayjs/logger'; + +class CustomTransport extends EmptyTransport { + log(info, callback) { + const levelLowerCase = info.level; + if (levelLowerCase === 'error' || levelLowerCase === 'warn') { + writeFileSync(join(logsDir, 'test.log'), info.message); + } + callback(); + } +} +``` + +我们可以初始化,加到 logger 中,也可以单独对 Transport 设置 level。 + +```typescript +const customTransport = new CustomTransport({ + level: 'warn', +}); + +logger.add(customTransport); +``` + +这样,原有的 logger 打印日志时,会自动执行该 Transport。 + +所有的 Transport 是附加在原有的 logger 实例之上(非 context logger),如需 ctx 数据,可以从 info 获取,注意判空。 + + +```typescript +class CustomTransport extends EmptyTransport { + log(info, callback) { + if (info.ctx) { + // ... + } else { + // ... + } + callback(); + } +} +``` + + +我们也可以使用依赖注入的方式来定义 Transport。 + +```typescript +import { EmptyTransport, IMidwayLogger } from '@midwayjs/logger'; +import { MidwayLoggerService, Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum) +export class CustomTransport extends EmptyTransport { + log(info, callback) { + // ... + callback(); + } +} + +// src/configuration.ts +@Configuration(/*...*/) +export class MainConfiguration { + + @Inject() + loggerService: MidwayLoggerService; + + @Inject() + customTransport: CustomTransport; + + async onReady() { + const appLogger = this.loggerService.getLogger('customLogger') as IMidwayLogger; + appLogger.add(this.customTransport); + } +} +``` + + + +### 延迟初始化 + +可以使用 `lazyLoad` 配置让日志延迟初始化。 + +比如: + +```typescript +export default { + midwayLogger: { + clients: { + customLoggerA: { + level: 'DEBUG', + }, + customLoggerB: { + lazyLoad: true, + }, + } + // ... + }, +} as MidwayConfig; +``` + +`customLoggerA` 会在框架启动时立即初始化,而 `customLoggerB` 会在业务实际第一次使用 `getLogger` 或者 `@Logger` 注入时才被初始化。 + +这个功能非常适合动态化创建日志,但是配置却希望合并到一起的场景。 + + + +## 常见问题 + + + +### 1、服务器环境日志不输出 + +服务器环境,默认日志等级为 warn,即 `logger.warn` 才会打印输出,请查看 ”日志等级“ 部分。 + +我们不推荐在服务器环境打印太多的日志,只打印必须的内容,过多的日志输出影响性能,也影响快速定位问题。 + + + +### 2、服务器没有控制台日志 + +一般来说,服务器控制台日志(console)是关闭的,只会输出到文件中,如有特殊需求,可以单独调整。 + diff --git a/site/versioned_docs/version-3.0.0/logger_v3.md b/site/versioned_docs/version-3.0.0/logger_v3.md new file mode 100644 index 000000000000..6f1e9ffa1798 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/logger_v3.md @@ -0,0 +1,1047 @@ +# 日志 + +Midway 为不同场景提供了一套统一的日志接入方式。通过 `@midwayjs/logger` 包导出的方法,可以方便的接入不同场景的日志系统。 + +实现的功能有: + +- 日志分级 +- 按大小和时间自动切割 +- 自定义输出格式 +- 统一错误日志 + +:::tip + +当前版本为 3.0 的日志 SDK 文档,如需 2.0 版本,请查看 [这个文档](/docs/logger)。 + +::: + + + +## 从 2.0 升级到 3.0 + +从 midway v3.13.0 开始,支持使用 3.0 版本的 `@midwayjs/logger`。 + +将 `package.json` 中的依赖版本升级,注意是 `dependencies` 依赖。 + +```diff +{ + "dependencies": { +- "@midwayjs/logger": "2.0.0", ++ "@midwayjs/logger": "^3.0.0" + } +} +``` + +如果在配置中没有了 midwayLogger 的类型提示,你需要在 `src/interface.ts` 中加入日志库的引用。 + +```diff +// src/interface.ts ++ import type {} from '@midwayjs/logger'; +``` + +在大部分场景下,两个版本是兼容的,但是由于是大版本升级,肯定会有一定的差异性,完整的 Breaking Change 变化,请查看 [变更文档](https://github.com/midwayjs/logger/blob/main/BREAKING-3.md)。 + + + +## 日志路径和文件 + +Midway 会在日志根目录创建一些默认的文件。 + + +- `midway-core.log` 框架、组件打印信息的日志,对应 `coreLogger` 。 +- `midway-app.log` 应用打印信息的日志,对应 `appLogger`,在 `@midawyjs/web` 中,该文件是 `midway-web.log` +- `common-error.log` 所有错误的日志(所有 Midway 创建出来的日志,都会将错误重复打印一份到该文件中) + +本地开发和服务器部署时的 **日志路径** 和 **日志等级** 不同,具体请参考 [配置日志根目录](#配置日志根目录) 和 [框架的默认等级](#框架的默认等级)。 + + + +## 默认日志对象 + +Midway 默认在框架提供了三种不同的日志,对应三种不同的行为。 + +| 日志 | 释义 | 描述 | 常见使用 | +| ----------------------------------- | -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| coreLogger | 框架,组件层面的日志 | 默认会输出控制台日志和文本日志 `midway-core.log` ,并且默认会将错误日志发送到 `common-error.log` 。 | 框架和组件的错误,一般会打印到其中。 | +| appLogger | 业务层面的日志 | 默认会输出控制台日志和文本日志 `midway-app.log` ,并且默认会将错误日志发送到 `common-error.log` ,在 `@midawyjs/web` 中,该文件是 `midway-web.log`。 | 业务使用的日志,一般业务日志会打印到其中。 | +| 上下文日志(复用 appLogger 的配置) | 请求链路的日志 | 默认使用 `appLogger` 进行输出,除了会将错误日志发送到 `common-error.log` 之外,还增加了上下文信息。 | 不同的协议有不同的请求日志格式,比如 HTTP 下就会输出路由信息。 | + + + +## 使用日志 + +Midway 的常用日志使用方法。 + +### 上下文日志 + +上下文日志是关联框架上下文对象(Context) 的日志。 + +我们可以通过 [获取到 ctx 对象](./req_res_app) 后,使用 `ctx.logger` 对象进行日志打印输出。 + +比如: + +```typescript +ctx.logger.info("hello world"); +ctx.logger.debug('debug info'); +ctx.logger.warn('WARNNING!!!!'); + +// 错误日志记录,直接会将错误日志完整堆栈信息记录下来,并且输出到 errorLog 中 +// 为了保证异常可追踪,必须保证所有抛出的异常都是 Error 类型,因为只有 Error 类型才会带上堆栈信息,定位到问题。 +ctx.logger.error(new Error('custom error')); +``` + +在执行后,我们能在两个地方看到日志输出: + + +- 控制台看到输出。 +- 日志目录的 midway-app.log 文件中 + + +输出结果: +```text +2021-07-22 14:50:59,388 INFO 7739 [-/::ffff:127.0.0.1/-/0ms GET /api/get_user] hello world +``` + +在注入的形式中,我们也可以直接使用 `@Inject() logger` 的形式来注入 `ctx.logger` ,和直接调用 `ctx.logger` 等价。 + +比如: + +```typescript +import { Get, Inject, Controller, Provide } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Controller() +export class HelloController { + + @Inject() + logger: ILogger; + + @Inject() + ctx; + + @Get("/") + async hello(){ + // ... + + // this.logger === ctx.logger + } +} +``` + + + +### 应用日志(App Logger) + +如果我们想做一些应用级别的日志记录,如记录启动阶段的一些数据信息,可以通过 App Logger 来完成。 + +```typescript +import { Configuration, Logger } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + @Logger() + logger: ILogger; + + async onReady(container: IMidwayContainer): Promise { + this.logger.debug('debug info'); + this.logger.info('启动耗时 %d ms', Date.now() - start); + this.logger.warn('warning!'); + + this.logger.error(someErrorObj); + } + +} +``` + +注意,这里使用的是 `@Logger()` 装饰器。 + + + +### CoreLogger + +在组件或者框架层面的研发中,我们会使用 coreLogger 来记录日志。 + +```typescript + +@Configuration() +export class MainConfiguration implements ILifeCycle { + + @Logger('coreLogger') + logger: ILogger; + + async onReady(container: IMidwayContainer): Promise { + this.logger.debug('debug info'); + this.logger.info('启动耗时 %d ms', Date.now() - start); + this.logger.warn('warning!'); + + this.logger.error(someErrorObj); + } + +} +``` + + + + +## 输出方法和格式 + + +Midway 的日志对象提供 `error()` , `warn()` , `info()` , `debug()`,`write()` 五种方法。 + + +示例如下。 +```typescript +logger.debug('debug info'); +logger.info('启动耗时 %d ms', Date.now() - start); +logger.warn('warning!'); +logger.error(new Error('my error')); +logger.write('abcdef'); +``` + +:::tip + +`write` 方法用于输出用户的原始格式日志。 + +::: + + + +基于 `util.format` 的格式化方式。 +```typescript +logger.info('%s %d', 'aaa', 222); +``` +常用的有 + + +- `%s` 字符串占位 +- `%d` 数字占位 +- `%j` json 占位 + +更多的占位和详细信息,请参考 node.js 的 [util.format](https://nodejs.org/dist/latest-v14.x/docs/api/util.html#util_util_format_format_args) 方法。 + + + +## 日志类型定义 + + +大部分情况下,用户应该使用 `@midwayjs/core` 中最简单的 `ILogger` 定义。 +```typescript +import { Provide, Logger, ILogger } from '@midwayjs/core'; + +@Provide() +export class UserService { + + @Inject() + logger: ILogger; + + async getUser() { + this.logger.info('hello user'); + } +} +``` + +`ILogger` 定义只提供最简单的 `debug` , `info` , `warn` 以及 `error` 方法。 + + +在某些场景下,我们需要更为复杂的定义,这个时候需要使用 `@midwayjs/logger` 提供的 `ILogger` 定义。 + + +```typescript +import { Provide, Logger } from '@midwayjs/core'; +import { ILogger } from '@midwayjs/logger'; + +@Provide() +export class UserService { + + @Inject() + logger: ILogger; + + async getUser() { + // ... + } + +} +``` +`ILogger` 的定义可以参考 interface 中的描述,或者查看 [代码](https://github.com/midwayjs/logger/blob/main/src/interface.ts)。 + + + +## 日志配置 + + + +### 基本配置结构 + +我们可以在配置文件中配置日志的各种行为。 + +Midway 中的的日志配置包含 **全局配置** 和 **单个日志配置** 两个部分,两者配置会合并和覆盖。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + // ... + }, + clients: { + coreLogger: { + // ... + }, + appLogger: { + // ... + } + } + }, +} as MidwayConfig; +``` + +如上所述,`clients` 配置段中的每个对象都是一个独立的日志配置项,其配置会和 `default` 段落合并后创建 logger 实例。 + + + +### 默认 Transport + +在日志模块中,默认内置了 `console`,`file`,`error` ,`json` 四个 Transport,其中 Midway 默认启用了 `console`,`file`,`error` ,更多信息可以通过配置进行修改。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + transports: { + console: { + // console transport 配置 + }, + file: { + // file transport 配置 + }, + error: { + // error transport 配置 + }, + } + }, + // ... + }, +} as MidwayConfig; +``` + +如果不需要某个 transport,可以设置为 `false`。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + transports: { + console: false, + } + }, + // ... + }, +} as MidwayConfig; +``` + + + +### 配置日志等级 + +在 Midway 中,一般情况下,我们只会使用 `error` , `warn` , `info` , `debug` 这四种等级。 + +日志等级表示当前可输出日志的最低等级。比如当你的日志 level 设置为 `warn` 时,仅 `warn` 以及更高的 `error` 等级的日志能被输出。 + + +在 Midway 中,有着自己的默认日志等级。 + + +- 在开发环境下(local,test,unittest),文本和控制台日志等级统一为 `info` 。 +- 在服务器环境,为减少日志数量,`coreLogger` 日志等级为 `warn` ,而其他日志为 `info`。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + level: 'info', + }, + // ... + }, +} as MidwayConfig; +``` + + + +logger 的 level 和 Transport 的 level 可以分开设置,Tranport 的 level 优先级高于 logger 的 level。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + // logger 的 level + level: 'info', + transports: { + file: { + // file transport 的 level + level: 'warn' + } + } + }, + // ... + }, +} as MidwayConfig; +``` + + + +我们也可以调整特定的 logger 的日志等级,比如: + +调整 `coreLogger` 或者 `appLogger` 。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + clients: { + coreLogger: { + level: 'warn', + // ... + }, + appLogger: { + level: 'warn', + // ... + } + } + }, +} as MidwayConfig; +``` + +特殊场景,也可以临时调整全局的日志等级。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + level: 'info', + transports: { + console: { + level: 'warn' + } + } + }, + // ... + }, +} as MidwayConfig; +``` + + + +### 配置日志根目录 + +默认情况下,Midway 会在本地开发和服务器部署时输出日志到 **日志根目录**。 + + +- 本地的日志根目录为 `${app.appDir}/logs/项目名` 目录下 +- 服务器的日志根目录为用户目录 `${process.env.HOME}/logs/项目名` (Linux/Mac)以及 `${process.env.USERPROFILE}/logs/项目名` (Windows)下,例如 `/home/admin/logs/example-app`。 + +我们可以配置日志所在的根目录,注意,要将所有 Transport 的路径都修改。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + transports: { + file: { + dir: '/home/admin/logs', + }, + error: { + dir: '/home/admin/logs', + }, + } + }, + // ... + }, +} as MidwayConfig; +``` + + + +### 配置日志切割(轮转) + + +默认行为下,同一个日志对象 **会生成两个文件**。 + +以 `midway-core.log` 为例,应用启动时会生成一个带当日时间戳 `midway-core.YYYY-MM-DD` 格式的文件,以及一个不带时间戳的 `midway-core.log` 的软链文件。 + +> windows 下不会生成软链 + + +为方便配置日志采集和查看,该软链文件永远指向最新的日志文件。 + + +当凌晨 `00:00` 时,会生成一个以当天日志结尾 `midway-core.log.YYYY-MM-DD` 的形式的新文件。 + +同时,当单个日志文件超过 200M 时,也会自动切割,产生新的日志文件。 + +可以通过配置调整按大小的切割行为。 + +```typescript +export default { + midwayLogger: { + default: { + transports: { + file: { + maxSize: '100m', + }, + error: { + maxSize: '100m', + }, + } + }, + // ... + }, +} as MidwayConfig; +``` + + + +### 配置日志清理 + +默认情况下,日志会存在 7 天。 + +可以通过配置调整该行为,比如改为保存 3 天。 + +```typescript +export default { + midwayLogger: { + default: { + transports: { + file: { + maxFiles: '3d', + }, + error: { + maxFiles: '3d', + }, + } + }, + // ... + }, +} as MidwayConfig; +``` + +也可以配置数字,表示最多保留日志文件的个数。 + +```typescript +export default { + midwayLogger: { + default: { + transports: { + file: { + maxFiles: '3', + }, + error: { + maxFiles: '3d', + }, + } + }, + // ... + }, +} as MidwayConfig; +``` + + + +### 配置自定义日志 + +可以如下配置: + +```typescript +export default { + midwayLogger: { + clients: { + abcLogger: { + fileLogName: 'abc.log' + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +自定义的日志可以通过 `@Logger('abcLogger')` 获取。 + +更多的日志选项可以参考 interface 中 [LoggerOptions 描述](https://github.com/midwayjs/logger/blob/main/src/interface.ts)。 + + + +### 配置日志输出格式 + + +显示格式指的是日志输出时单行文本的字符串结构。 + +每个 logger 对象,都可以配置一个输出格式,显示格式是一个返回字符串结构的方法,参数为一个 info 对象。 + +```typescript +import { LoggerInfo } from '@midwayjs/logger'; + +export default { + midwayLogger: { + clients: { + appLogger: { + format: (info: LoggerInfo) => { + return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.labelText}${info.message}`; + } + // ... + }, + customOtherLogger: { + format: (info: LoggerInfo) => { + return 'xxxx'; + } + } + } + // ... + }, +} as MidwayConfig; +``` + +info 对象的默认属性如下: + +| **属性名** | **描述** | **示例** | +| ----------- | ------------------------------------------------ | ----------------------- | +| timestamp | 时间戳,默认为 `'YYYY-MM-DD HH:mm:ss,SSS` 格式。 | 2020-12-30 07:50:10,453 | +| level | 小写的日志等级 | info | +| LEVEL | 大写的日志等级 | INFO | +| pid | 当前进程 pid | 3847 | +| message | util.format 的结果 | | +| args | 原始的用户入参 | [ 'a', 'b', 'c' ] | +| ctx | 使用 ContextLogger 时关联的上下文对象 | | +| originError | 原始错误对象,遍历参数后获取,性能较差 | 错误实例本身 | +| originArgs | 同 args,仅做兼容老版本使用 | | + + + +### 获取自定义上下文日志 + +上下文日志是基于 **原始日志对象** 来打日志的,会复用原始日志的所有格式,他们的关系如下。 + +```typescript +// 伪代码 +const contextLogger = customLogger.createContextLogger(ctx); +``` + +`@Inject` 只能注入默认的上下文日志,我们可以通过 `ctx.getLogger` 方法获取其他 **自定义日志** 对应的 **上下文日志**。上下文日志和 ctx 关联,同一个上下文会相同的 key 会获取到同一个日志对象,当 ctx 被销毁,日志对象也会被回收。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Provide() +export class UserService { + + @Inject() + ctx: Context; + + async getUser() { + // 这里获取的是 customLogger 对应的上下文日志对象 + const customLogger = this.ctx.getLogger('customLogger'); + customLogger.info('hello world'); + } + +} +``` + + + + +### 配置上下文日志输出格式 + +上下文日志是基于 **原始日志对象** 来打日志的,会复用原始日志的所有格式,但是我们可以单独配置日志对象的对应的上下文日志格式。 + +上下文日志的 info 对象中多了 ctx 对象,我们以修改 `customLogger` 的上下文日志为例。 + +```typescript +export default { + midwayLogger: { + clients: { + customLogger: { + contextFormat: info => { + const ctx = info.ctx; + return `${info.timestamp} ${info.LEVEL} ${info.pid} [${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`; + } + // ... + } + } + // ... + }, +} as MidwayConfig; +``` + +则你在使用上下文日志输出时,会默认变成你 format 的样子。 + +```typescript +ctx.getLogger('customLogger').info('hello world'); +// 2021-01-28 11:10:19,334 INFO 9223 [2ms POST] hello world +``` + +注意,由于 `App Logger` 是所有框架默认的日志对象,较为特殊,现有部分框架默认配置了其上下文格式,导致在 `midwayLogger` 字段中配置无效。 + +为此你需要单独修改某一框架的上下文日志格式配置,请跳转到不同的框架查看。 + +- [修改 koa 的上下文日志格式](./extensions/koa#修改上下文日志) +- [修改 egg 的上下文日志格式](./extensions/egg#修改上下文日志) +- [修改 express 的上下文日志格式](./extensions/express#修改上下文日志) + + + +### 配置延迟初始化 + +可以使用 `lazyLoad` 配置让日志延迟初始化。 + +比如: + +```typescript +export default { + midwayLogger: { + clients: { + customLoggerA: { + // .. + }, + customLoggerB: { + lazyLoad: true, + }, + } + // ... + }, +} as MidwayConfig; +``` + +`customLoggerA` 会在框架启动时立即初始化,而 `customLoggerB` 会在业务实际第一次使用 `getLogger` 或者 `@Logger` 注入时才被初始化。 + +这个功能非常适合动态化创建日志,但是配置却希望合并到一起的场景。 + + + +### 配置关联日志 + +日志对象可以配置一个关联的日志对象名。 + +比如: + +```typescript +export default { + midwayLogger: { + clients: { + customLoggerA: { + aliasName: 'customLoggerB', + // ... + }, + } + // ... + }, +} as MidwayConfig; +``` + +当使用 API 获取时,不同的名字将取到同样的日志对象。 + +```typescript +app.getLogger('customLoggerA') => customLoggerA +app.getLogger('customLoggerB') => customLoggerA +``` + + + +### 配置控制台输出颜色 + +控制台输出时,在命令行支持颜色输出的情况下,针对不同的的日志等级会输出不同的颜色,如果不支持颜色,则不会显示。 + +你可以通过配置直接关闭颜色输出。 + +```typescript +export default { + midwayLogger: { + default: { + transports: { + console: { + autoColors: false, + } + } + } + // ... + }, +} as MidwayConfig; +``` + + + +### 配置 JSON 输出 + +通过开启 `json` Transport,可以将日志输出为 JSON 格式。 + +比如所有 logger 开启。 + +```typescript +export default { + midwayLogger: { + default: { + transports: { + file: false, + json: { + fileLogName: 'midway-app.json.log' + } + } + } + // ... + }, +} as MidwayConfig; +``` + +或者单个 logger 开启。 + +```typescript +export default { + midwayLogger: { + default: { + // ... + }, + clients: { + appLogger: { + transports: { + json: { + // ... + } + } + } + } + }, +} as MidwayConfig; +``` + +`json` Transport 的配置格式和 `file` 相同,输出略有不同。 + +比如我们可以修改 `format` 中输出的内容,默认情况下,输出至少会包含 `level` 和 `pid` 字段 + +```typescript +export default { + midwayLogger: { + default: { + transports: { + json: { + format: (info: LoggerInfo & {data: string}) => { + info.data = 'custom data'; + return info; + } + } + } + } + // ... + }, +} as MidwayConfig; +``` + +输出为: + +```text +{"data":"custom data","level":"info","pid":89925} +{"data":"custom data","level":"debug","pid":89925} +``` + + + +## 自定义 Transport + +框架提供了扩展 Transport 的功能,比如,你可以写一个 Transport 来做日志的中转,上传到别的日志库等能力。 + + + +### 继承现有 Transport + +如果是写入到新的文件,可以通过使用 `FileTransport` 来实现。 + +```typescript +import { FileTransport, isEnableLevel, LoggerLevel, LogMeta } from '@midwayjs/logger'; + +// Transport 的配置 +interface CustomOptions { + // ... +} + +class CustomTransport extends FileTransport { + log(level: LoggerLevel | false, meta: LogMeta, ...args) { + // 判断 level 是否满足当前 Transport + if (!isEnableLevel(level, this.options.level)) { + return; + } + + // 使用内置的格式化方法格式化消息 + let buf = this.format(level, meta, args) as string; + // 加上换行符 + buf += this.options.eol; + + // 写入自己想写的日志 + if (this.options.bufferWrite) { + this.bufSize += buf.length; + this.buf.push(buf); + if (this.buf.length > this.options.bufferMaxLength) { + this.flush(); + } + } else { + // 没启用缓存,则直接写入 + this.logStream.write(buf); + } + } +} +``` + +在使用前,需要注册到日志库中。 + +```typescript +import { TransportManager } from '@midwayjs/logger'; + +TransportManager.set('custom', CustomTransport); +``` + +之后就可以在配置中使用这个 Transport 了。 + +```typescript +// src/config/config.default.ts +import { MidwayConfig } from '@midwayjs/core'; + +export default { + midwayLogger: { + default: { + transports: { + custom: { + dir: 'xxxx', + fileLogName: 'xxx', + // ... + } + } + } + }, +} as MidwayConfig; +``` + +这样,原有的 logger 打印日志时,会自动执行该 Transport。 + + + +### 完全自定义 Transport + +除了写入文件之外,也可以将日志投递到远端服务,比如下面的示例,将日志中转到另一个服务。 + +注意,Transport 是一个可异步执行的操作,但是 logger 本身不会等待 Transport 执行返回。 + +```typescript +import { Transport, ITransport, LoggerLevel, LogMeta } from '@midwayjs/logger'; + + +// Transport 的配置 +interface CustomOptions { + // ... +} + +class CustomTransport extends Transport implements ITransport { + log(level: LoggerLevel | false, meta: LogMeta, ...args) { + // 使用内置的格式化方法格式化消息 + let msg = this.format(level, meta, args) as string; + + // 异步写入日志库 + remoteSdk.send(msg).catch(err => { + // 记录下错误或者忽略 + console.error(err); + }); + } +} +``` + + + +## 动态 API + +通过 `getLogger` 方法动态获取日志对象。 + +```typescript +// 获取 coreLogger +const coreLogger = app.getLogger('coreLogger'); +// 获取默认的 contextLogger +const contextLogger = ctx.getLogger(); +// 获取特定 logger 创建出来的 contextLogger,等价于 customALogger.createContextLogger(ctx) +const customAContextLogger = ctx.getLogger('customA'); +``` + +框架内置的 `MidwayLoggerService` 也拥有上述的 API。 + +```typescript +import { MidwayLoggerService } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Provide() +export class MainConfiguration { + + @Inject() + loggerService: MidwayLoggerService; + + @Inject() + ctx: Context; + + async getUser() { + // get custom logger + const customLogger = this.loggerService.getLogger('customLogger'); + + // 创建 context logger + const customContextLogger = this.loggerService.createContextLogger(this.ctx, customLogger); + } +} +``` + + + +## 常见问题 + + + +### 1、服务器环境日志不输出 + +我们不推荐在服务器环境打印太多的日志,只打印必须的内容,过多的日志输出影响性能,也影响快速定位问题。 + +如需调整日志等级,请查看 ”配置日志等级“ 部分。 + + + +### 2、服务器没有控制台日志 + +一般来说,服务器控制台日志(console)是关闭的,只会输出到文件中,如有特殊需求,可以单独调整。 + + + +### 3、部分 Docker 环境启动失败 + +检查日志写入的目录当前应用启动的用户是否有权限。 + + + +### 4、如果有老的配置如何转换 + +新版本日志库已经兼容老配置,一般情况下无需额外处理,老配置和新配置在合并时有优先级关系,请查看 [变更文档](https://github.com/midwayjs/logger/blob/main/BREAKING-3.md)。 + +为了减少排查问题,在使用新版本日志库时请尽可能使用新配置格式。 diff --git a/site/versioned_docs/version-3.0.0/middleware.md b/site/versioned_docs/version-3.0.0/middleware.md new file mode 100644 index 000000000000..3ea19d986d3b --- /dev/null +++ b/site/versioned_docs/version-3.0.0/middleware.md @@ -0,0 +1,721 @@ +# Web 中间件 + +Web 中间件是在控制器调用 **之前** 和 **之后(部分)**调用的函数。 中间件函数可以访问请求和响应对象。 +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01h6hYvW1ogNexjJ3Nl_!!6000000005254-2-tps-2196-438.png) + + +不同的上层 Web 框架中间件形式不同,Midway 标准的中间件基于 [洋葱圈模型](https://eggjs.org/zh-cn/intro/egg-and-koa.html#midlleware)。而 Express 则是传统的队列模型。 + + +Koa 和 EggJs 可以在 **控制器前后都被执行**,在 Express 中,中间件 **只能在控制器之前** 调用,将在 Express 章节单独介绍。 + +下面的代码,我们将以 `@midwayjs/koa` 举例。 + + + + +## 编写中间件 + + +一般情况下,我们会在 `src/middleware` 文件夹中编写 Web 中间件。 + + +创建一个 `src/middleware/report.middleware.ts` 。我们在这个 Web 中间件中打印了控制器(Controller)执行的时间。 +``` +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ ├── user.controller.ts +│ │ └── home.controller.ts +│ ├── interface.ts +│ ├── middleware ## 中间件目录 +│ │ └── report.middleware.ts +│ └── service +│ └── user.service.ts +├── test +├── package.json +└── tsconfig.json +``` + + +Midway 使用 `@Middleware` 装饰器标识中间件,完整的中间件示例代码如下。 + + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + // 控制器前执行的逻辑 + const startTime = Date.now(); + // 执行下一个 Web 中间件,最后执行到控制器 + // 这里可以拿到下一个中间件或者控制器的返回值 + const result = await next(); + // 控制器之后执行的逻辑 + console.log(Date.now() - startTime); + // 返回给上一个中间件的结果 + return result; + }; + } + + static getName(): string { + return 'report'; + } +} +``` + + +简单来说, `await next()` 则代表了下一个要执行的逻辑,这里一般代表控制器执行,在执行的前后,我们可以进行一些打印和赋值操作,这也是洋葱圈模型最大的优势。 + +注意,Midway 对传统的洋葱模型做了一些微调,使得其可以获取到下一个中间件的返回值,同时,你也可以将这个中间件的结果,通过 `return` 方法返回给上一个中间件。 + +这里的静态 `getName` 方法,用来指定中间件的名字,方便排查问题。 + + + +## 使用中间件 + + +Web 中间件在写完之后,需要应用到请求流程之中。 + + +根据应用到的位置,分为两种: + + +- 1、全局中间件,所有的路由都会执行的中间件,比如 cookie、session 等等 +- 2、路由中间件,单个/部分路由会执行的中间件,比如某个路由的前置校验,数据处理等等 + + + +他们之间的关系一般为: + + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01oQZ5Rk1jReqck6YMn_!!6000000004545-2-tps-2350-584.png) + + + +### 路由中间件 + + +在写完中间件之后,我们需要把它应用到各个控制器路由之上。 `@Controller` 装饰器的第二个参数,可以让我们方便的在某个路由分组之上添加中间件。 +```typescript +import { Controller } from '@midwayjs/core'; +import { ReportMiddleware } from '../middleware/report.middlweare'; + +@Controller('/', { middleware: [ ReportMiddleware ] }) +export class HomeController { + +} +``` + + +Midway 同时也在 `@Get` 、 `@Post` 等路由装饰器上都提供了 middleware 参数,方便对单个路由做中间件拦截。 +```typescript +import { Controller, Get } from '@midwayjs/core'; +import { ReportMiddleware } from '../middleware/report.middlweare'; + +@Controller('/') +export class HomeController { + + @Get('/', { middleware: [ ReportMiddleware ]}) + async home() { + } +} +``` + + + +### 全局中间件 + + +所谓的全局中间件,就是对所有的路由生效的 Web 中间件。 + + +我们需要在应用启动前,加入当前框架的中间件列表中,`useMiddleware` 方法,可以把中间件加入到中间件列表中。 + +```typescript +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { ReportMiddleware } from './middleware/user.middleware'; + +@Configuration({ + imports: [koa] + // ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + this.app.useMiddleware(ReportMiddleware); + } +} + +``` +你可以同时添加多个中间件。 + +```typescript +async onReady() { + this.app.useMiddleware([ReportMiddleware1, ReportMiddleware2]); +} +``` + + + +## 忽略和匹配路由 + +在中间件执行时,我们可以添加路由忽略的逻辑。 + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + // ... + }; + } + + ignore(ctx: Context): boolean { + // 下面的路由将忽略此中间件 + return ctx.path === '/' + || ctx.path === '/api/auth' + || ctx.path === '/api/login'; + } + + static getName(): string { + return 'report'; + } +} +``` + +同理,也可以添加匹配的路由,只有匹配到的路由才会执行该中间件。`ignore` 和 `match` 同时只有一个会生效。 + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + // ... + }; + } + + match(ctx: Context): boolean { + // 下面的匹配到的路由会执行此中间件 + if (ctx.path === '/api/index') { + return true; + } + } + + static getName(): string { + return 'report'; + } +} +``` + +除此之外,`match` 和 `ignore` 还可以是普通字符串或者正则,以及他们的数组形式。 + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + // 字符串 + match = '/api/index'; + + // 正则 + match = /^\/api/; + + // 数组 + match = ['/api/index', '/api/user', /^\/openapi/, ctx => { + if (ctx.path === '/api/index') { + return true; + } + }]; +} +``` + +我们也可以在初始化阶段对属性进行修改,比如: + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + // 某个中间件的配置 + @Config('report') + reportConfig; + + @Init() + async init() { + // 动态合并一些规则 + if (this.reportConfig.match) { + this.match = ['/api/index', '/api/user'].concat(this.reportConfig.match); + } else if (this.reportConfig.ignore) { + this.match = [].concat(this.reportConfig.ignore); + } + } +} +``` + + + +## 复用中间件 + +中间件的本质是函数,函数可以传递不同的配置来复用中间件,但是在 class 场景下较难实现。Midway 提供了 `createMiddleware` 方法辅助 class 场景下创建不同的中间件函数。 + +可以在 `useMiddleware` 阶段使用 `createMiddleare` 复用。 + +```typescript +// src/configuration.ts +import { App, Configuration, createMiddleare } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { ReportMiddleware } from './middleware/user.middleware'; + +@Configuration({ + imports: [koa] + // ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + // 添加 ReportMiddleware 中间件 + this.app.useMiddleware(ReportMiddleware); + // 添加一个不同参数的 ReportMiddleware + this.app.useMiddleware(createMiddleare(ReportMiddleware, { + text: 'abc' + }, 'anotherReportMiddleare')); + } +} + +``` + +我们可以在中间件中获取到这个参数,从而执行不同的逻辑,。 + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + initData = 'text1'; + + resolve(_, options?: { + text: string; + }) { + return async (ctx: Context, next: NextFunction) => { + this.ctx.setAttr('data', options?.text || this.initData); + return await next(); + }; + } +} +``` + +`createMiddleare` 方法定义如下,包含三个参数。 + +```typescript +function createMiddleware(middlewareClass: new (...args) => IMiddleware, options, name?: string); +``` + +| 参数 | 描述 | +| --------------- | ---------------- | +| middlewareClass | 中间件类 | +| options | 传递的自定义参数 | +| name | 可选,中间件名称 | + +`options` 可以传递中间件的自定义函数,在逻辑中可以自行进行处理。 + +`name` 字段用于中间件的排序和展示,一般会选择一个和原中间件名不同的字符串。 + +`createMiddleare` 方法还可以在路由中间件使用。 + +```typescript +import { Controller, Get, createMiddleware } from '@midwayjs/core'; +import { ReportMiddleware } from '../middleware/report.middlweare'; + +const anotherMiddleware = createMiddleware(ReportMiddleware, { + // ... +}); + +@Controller('/') +export class HomeController { + @Get('/', { + middleware: [anotherMiddleware], + }) + async home() {} +} +``` + +注意,装饰器会在框架启动前加载,这个时候 `createMiddleware` 的参数无法从框架配置中获取,一般为固定的对象值。 + + + +## 函数中间件 + +Midway 依旧支持函数中间件的形式,并且可以使用 `useMiddleware` 来加入到中间件列表。 + +```typescript +// src/middleware/another.middleware.ts +export async function fnMiddleware(ctx, next) { + // ... + await next(); + // ... +} + + +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { ReportMiddleware } from './middleware/user.middleware'; +import { fnMiddleware } from './middleware/another.middleware'; + +@Configuration({ + imports: [koa] + // ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + // add middleware + this.app.useMiddleware([ReportMiddleware, fnMiddleware]); + } +} + + +``` + +这样的话,社区很多 koa 三方中间件都可以比较方便的接入。 + + + +## 使用社区中间件 + + +我们以 `koa-static` 举例。 + + +在 `koa-static` 文档中,是这样写的。 + +```typescript +const Koa = require('koa'); +const app = new Koa(); +app.use(require('koa-static')(root, opts)); +``` + +那么, `require('koa-static')(root, opts)` 这个,其实就是返回的中间件方法,我们直接导出,并且调用 `useMiddleware` 即可。 + +```typescript +async onReady() { + // add middleware + this.app.useMiddleware(require('koa-static')(root, opts)); +} +``` + +如果中间件支持在路由上引入,比如: + +```typescript +const Koa = require('koa'); +const app = new Koa(); +app.get('/controller', require('koa-static')(root, opts)); +``` + +我们也可以将中间件看成普通函数,放在装饰器参数中。 + +```typescript +const staticMiddleware = require('koa-static')(root, opts); + +// ... +class HomeController { + @Get('/controller', {middleware: [staticMiddleware]}) + async getMethod() { + // ... + } +} +``` + +也可以作为作为路由方法体使用。 + +```typescript +const staticMiddleware = require('koa-static')(root, opts); + +// ... +class HomeController { + @Get('/controller') + async getMethod(ctx, next) { + // ... + return staticMiddleware(ctx, next); + } +} +``` + +:::tip + +三方中间件写法有很多种,上面只是列出最基本的使用方式。 + +::: + + + +## 获取中间件名 + +每个中间件应当有一个名字,默认情况下,类中间件的名字将依照下面的规则获取: + +- 1、当 `getName()` 静态方法存在时,以其返回值作为名字 +- 2、如果不存在 `getName()` 静态方法,将使用类名作为中间件名 + +一个好认的中间件名在手动排序或者调试代码时有很大的作用。 + +```typescript +@Middleware() +export class ReportMiddleware implements IMiddleware { + + // ... + + static getName(): string { + return 'report'; // 中间件名 + } +} +``` + +函数中间件也是类似,定义的方法名就是中间件的名字,比如下面的 `fnMiddleware` 。 + +```typescript +export async function fnMiddleware(ctx, next) { + // ... + await next(); + // ... +} +``` + +假如三方中间件导出了一个匿名的中间件函数,那么你可以使用 `_name` 来添加一个名字。 + +```typescript +const fn = async (ctx, next) => { + // ... + await next(); + // ... +}; + +fn._name = 'fnMiddleware'; + +``` + +我们可以使用 `getMiddleware().getNames()` 来获取当前中间件列表中的所有中间件名。 + +```typescript +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { ReportMiddleware } from './middleware/user.middleware'; +import { fnMiddleware } from './middleware/another.middleware'; + +@Configuration({ + imports: [koa] + // ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + // add middleware + this.app.useMiddleware([ReportMiddleware, fnMiddleware]); + + // output + console.log(this.app.getMiddleware().getNames()); + // => report, fnMiddleware + } +} + + + +``` + + + +## 中间件顺序 + +有时候,我们需要在组件或者应用中修改中间件的顺序。 + +Midway 提供了 `insert` 系列的 API,方便用户快速调整中间件。 + +我们需要先使用 `getMiddleware()` 方法获取中间件列表,然后对其进行操作。 + +```typescript +// src/configuration.ts +import { App, Configuration } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { ReportMiddleware } from './middleware/user.middleware'; + +@Configuration({ + imports: [koa] + // ... +}) +export class MainConfiguration { + + @App() + app: koa.Application; + + async onReady() { + // 把中间件添加到最前面 + this.app.getMiddleware().insertFirst(ReportMiddleware); + // 把中间件添加到最后面,等价于 useMiddleware + this.app.getMiddleware().insertLast(ReportMiddleware); + + // 把中间件添加到名为 session 的中间件之后 + this.app.getMiddleware().insertAfter(ReportMiddleware, 'session'); + // 把中间件添加到名为 session 的中间件之前 + this.app.getMiddleware().insertBefore(ReportMiddleware, 'session'); + } +} + +``` + + + + +## 常见示例 + + + +### 中间件中获取请求作用域实例 + + +由于 Web 中间件在生命周期的特殊性,会在应用请求前就被加载(绑定)到路由上,所以无法和请求关联。中间件类的作用域 **固定为单例(Singleton)**。 + + +由于 **中间件实例为单例**,所以中间件中注入的实例和请求不绑定,**无法获取到 ctx**,无法使用 `@Inject()` 注入请求作用域的实例,只能获取 Singleton 的实例。 + + +比如,**下面的代码是错误的。** + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + @Inject() + userService; // 这里注入的实例和上下文不绑定,无法获取到 ctx + + resolve() { + return async (ctx: Context, next: NextFunction) => { + // TODO + await next(); + }; + } + +} +``` + + +如果要获取请求作用域的实例,可以使用从请求作用域容器 `ctx.requestContext` 中获取,如下面的方法。 + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class ReportMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + const userService = await ctx.requestContext.getAsync(UserService); + // TODO userService.xxxx + await next(); + }; + } + +} +``` + +### 统一返回数据结构 + +比如在 `/api` 返回的所有数据都是用统一的结构,减少 Controller 中的重复代码。 + +我们可以增加一个类似下面的中间件代码。 + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class FormatMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + const result = await next(); + return { + code: 0, + msg: 'OK', + data: result, + } + }; + } + + match(ctx) { + return ctx.path.indexOf('/api') !== -1; + } +} +``` + +上面的仅是正确逻辑返回的代码,如需错误的返回包裹,可以使用 [过滤器](./error_filter)。 + + + +### 关于中间件返回 null 的情况 + +在 koa/egg 下,如果中间件中返回 null 值,会使得状态码变为 204,如果需要返回其他状态码(如 200),需要在中间件中显式额外赋值状态码。 + +```typescript +import { Middleware, IMiddleware } from '@midwayjs/core'; +import { NextFunction, Context } from '@midwayjs/koa'; + +@Middleware() +export class FormatMiddleware implements IMiddleware { + + resolve() { + return async (ctx: Context, next: NextFunction) => { + const result = await next(); + if (result === null) { + ctx.status = 200; + } + return { + code: 0, + msg: 'OK', + data: result, + } + }; + } + + match(ctx) { + return ctx.path.indexOf('/api') !== -1; + } +} +``` + diff --git a/site/versioned_docs/version-3.0.0/midway_component.md b/site/versioned_docs/version-3.0.0/midway_component.md new file mode 100644 index 000000000000..d6e51ac23f34 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/midway_component.md @@ -0,0 +1,66 @@ +# 使用组件 + +组件是 Midway 的扩展机制,我们会将复用的业务代码,或者逻辑,抽象的公共的能力开发成组件,使得这些代码能够在所有的 Midway 场景下复用。 + + + +## 启用组件 + +组件一般以 npm 包形式进行复用。每个组件都是一个可以被直接 `require` 的代码包。我们以 `@midwayjs/validate` 组件为例。 + +首先,在应用中加入依赖。 + +```json +// package.json +{ + "dependencies": { + "@midwayjs/validate": "^3.0.0" + } +} +``` + +我们需要在代码中启用这个组件,Midway 的组件加载能力设计在 `src/configuration.ts` 文件中。 + +```typescript +// 应用或者函数的 src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as validate from '@midwayjs/validate'; + +@Configuration({ + imports: [validate], +}) +export class MainConfiguration {} +``` + + + +## 不同环境启用组件 + +有时候,我们需要在特殊环境下才使用组件,比如本地开发时。 `imports` 属性可以传入对象数组,我们可以在对象中针对组件启用的环境进行配置。 + +比如常用的 `info` 组件,为了安全考虑,我们就可以只让他在本地环境启用。 + +```typescript +// 应用或者函数的 src/configuration.ts +import { Configuration } from '@midwayjs/core'; +import * as info from '@midwayjs/info'; + +@Configuration({ + imports: [ + { + component: info, + enabledEnvironment: ['local'], + }, + ], +}) +export class MainConfiguration {} +``` + +- `component` 用于指定组件对象,组件对象必须包含一个 `Configuration` 导出的属性 +- `enabledEnvironment` 组件启用的环境数组 + + + +## 开发组件 + +参见文档:[组件开发](component_development)。 diff --git a/site/versioned_docs/version-3.0.0/midway_slow_problem.md b/site/versioned_docs/version-3.0.0/midway_slow_problem.md new file mode 100644 index 000000000000..bbc2719587e0 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/midway_slow_problem.md @@ -0,0 +1,65 @@ +# 关于 Midway 启动慢的问题 + +Midway 在本地开发时会使用 ts-node 实时扫描并 require 模块,如果 ts 文件太多(比如 200+)个,启动时可能会导致比较慢,在 Windows 下非 SSD 硬盘的情况下特别明显,导致 ts-node 的类型检查的 Server 频繁 fullGC,每个文件加载可能会达到 1-2s。 + +一般 Mac 都是 SSD,所以基本没有问题,而 Windows 会有出现,构建后执行无此问题。 + +如下图所示。 + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1601523014939-40121f9c-bc19-4f9e-a7e6-e744d409a9ea.png) + +## 如何判断 + +1、先清理下 ts-node 缓存。 + +在临时目录中有一个 `ts-node-*` 的目录,删除即可(不知道临时目录的可以在命令行执行 `require('os').tmpdir()` 输出查看)。 + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1601523402032-7e9c162a-762e-4cba-82b4-8ae63fe37280.png) + +删了下面类似的这个目录。 + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1601523340452-7924affe-96b5-4544-85b7-e41ace4206e8.png) + +2、用 ts-node 启动 Midway + +执行下面的启动命令。 + +```bash +// midway v1 +cross-env DEBUG=midway* NODE_ENV=local midway-bin dev --ts + +// midway v2 +cross-env NODE_DEBUG=midway* NODE_ENV=local midway-bin dev --ts +``` + +会出现每个文件的 require 时长,如果时间比较久一般就是了。 + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1601523470970-1812326a-39d9-4b39-af57-7723f80f6e17.png) + +## 解决问题 + +由于 `TS_NODE_TYPE_CHECK` 内部会启动一个 Server,在文件特别的多的情况下,每次 require 都会做类型检查,如果造成严重启动影响,建议关闭。**代价是启动运行时不会做类型校验,由于一般在编辑器里已经有提示,运行时不再做检查也可以。** + +在执行命令前增加下面两个环境变量。 + +```bash +TS_NODE_TYPE_CHECK=false TS_NODE_TRANSPILE_ONLY=true +``` + +比如: + +```json +cross-env TS_NODE_TYPE_CHECK=false TS_NODE_TRANSPILE_ONLY=true NODE_DEBUG=midway* NODE_ENV=local midway-bin dev --ts +``` + +下面是使用相同的项目的对比效果。 + +| | 第一次执行(无缓存) | 第二次执行(有缓存) | +| ------------ | -------------------- | -------------------- | +| 不加优化参数 | 约 258s | 约 5.6s | +| 加优化参数 | 约 15s | 约 4.7s | +| | | | + +## 其他 + +如果任有问题,请提交你的仓库 + node_modules 给我们。 diff --git a/site/versioned_docs/version-3.0.0/mock.md b/site/versioned_docs/version-3.0.0/mock.md new file mode 100644 index 000000000000..df4f4726231b --- /dev/null +++ b/site/versioned_docs/version-3.0.0/mock.md @@ -0,0 +1,432 @@ +# 数据模拟 + +Midway 提供了内置的在开发和测试时模拟数据的能力。 + + + +## 测试时 Mock + +`@midwayjs/mock` 提供了一些更为通用的 API,用于在测试时进行模拟。 + +### 模拟上下文 + +使用 `mockContext` 方法来模拟上下文。 + +```typescript +import { mockContext } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + const app = await createApp(); + + // 模拟上下文 + mockContext(app, 'user', 'midway'); + + const result1 = await createHttpRequest(app).get('/'); + // ctx.user => midway + // ... +}); +``` + +如果你的数据比较复杂,或者带有逻辑,也可以使用回调形式。 + +```typescript +import { mockContext } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + const app = await createApp(); + + // 模拟上下文 + mockContext(app, (ctx) => { + ctx.user = 'midway'; + }); +}); +``` + +注意,这个 mock 行为是在所有中间件之前执行。 + + + +### 模拟 Session + +使用 `mockSession` 方法来模拟 Session。 + +```typescript +import { mockSession } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + const app = await createApp(); + + mockSession(app, 'user', 'midway'); + + const result1 = await createHttpRequest(app).get('/'); + // ctx.session.user => midway + // ... +}); +``` + +### 模拟 Header + +使用 `mockHeader` 方法来模拟 Header。 + +```typescript +import { mockHeader } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + const app = await createApp(); + + mockHeader(app, 'x-abc', 'bbb'); + + const result1 = await createHttpRequest(app).get('/'); + // ctx.headers['x-abc'] => bbb + // ... +}); +``` + +### 模拟类属性 + +使用 `mockClassProperty` 方法来模拟类的属性。 + +假如有下面的服务类。 + +```typescript +@Provide() +export class UserService { + data; + + async getUser() { + return 'hello'; + } +} +``` + +我们可以在使用时进行模拟。 + +```typescript +import { mockClassProperty } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + + mockClassProperty(UserService, 'data', { + bbb: 1 + }); + // userService.data => {bbb: 1} + + // ... +}); +``` + +也可以模拟方法。 + +```typescript +import { mockClassProperty } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + + mockClassProperty(UserService, 'getUser', async () => { + return 'midway'; + }); + + // userService.getUser() => 'midway' + + // ... +}); +``` + + + +### 模拟普通对象属性 + +使用 `mockProperty` 方法来模拟对象的属性。 + +```typescript +import { mockProperty } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + + const a = {}; + mockProperty(a, 'name', 'hello'); + + // a['name'] => 'hello' + + // ... +}); +``` + +也可以模拟方法。 + +```typescript +import { mockProperty } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + + const a = {}; + mockProperty(a, 'getUser', async () => { + return 'midway'; + }); + + // a.getUser() => 'midway' + + // ... +}); +``` + +### 分组 + +从 `3.19.0` 开始,Midway 的 mock 功能支持通过分组来管理不同的 mock 数据。你可以在创建 mock 时指定一个分组名称,这样可以在需要时单独恢复或清理某个分组的 mock 数据。 + + +```typescript +import { mockContext, restoreMocks } from '@midwayjs/mock'; + +it('should test mock with groups', async () => { + const app = await createApp(); + + // 创建普通对象的 mock + const a = {}; + mockProperty(a, 'getUser', async () => { + return 'midway'; + }, 'group1'); + + // 创建上下文的 mock + mockContext(app, 'user', 'midway', 'group1'); + mockContext(app, 'role', 'admin', 'group2'); + + // 恢复单个分组 + restoreMocks('group1'); + + // 恢复所有分组 + restoreAllMocks(); +}); +``` + +通过分组,你可以更灵活地管理和控制 mock 数据,特别是在复杂的测试场景中。 + + + +### 清理 mock + +在每次调用 `close` 方法时,会自动清理所有的 mock 数据。 + +如果希望手动清理,也可以执行方法 `restoreAllMocks` 。 + +```typescript +import { restoreAllMocks } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + restoreAllMocks(); + // ... +}); +``` + +从 `3.19.0` 开始,支持指定 group 清理。 + +```typescript +import { restoreMocks } from '@midwayjs/mock'; + +it('should test create koa app with new mode with mock', async () => { + restoreMocks('group1');(); + // ... +}); +``` + + +## 标准 Mock 服务 + +Midway 提供了标准的 MidwayMockService 服务,用于在代码中进行模拟数据。 + + `@midwayjs/mock` 中的各种模拟方法,底层皆调用了此服务。 + +具体 API 请参考 [内置服务](./built_in_service#midwaymockservice) + + + +## 开发期 Mock + +每当后端服务没有上线,或者在开发阶段未准备好数据的时候,就需要用到开发期模拟的能力。 + + + +### 编写模拟类 + +一般情况下,我们会在 `src/mock` 文件夹中编写开发期使用的模拟数据,我们的模拟行为实际是一段逻辑代码。 + +:::tip + +不要对模拟数据放在代码中不习惯,事实上它是逻辑的一部分。 + +::: + +我们举个例子,假如现在有一个获取 Index 数据的服务,但是服务还未开发完毕,我们只能编写模拟代码。 + +```typescript +// src/service/indexData.service.ts +import { Singleton, makeHttpRequest, Singleton } from '@midwayjs/core'; + +@Singleton() +export class IndexDataService { + + @Config('index') + indexConfig: {indexUrl: string}; + + private indexData; + + async load() { + // 从远端获取数据 + this.indexData = await this.fetchIndex(this.indexConfig.indexUrl); + } + + public getData() { + if (!this.indexData) { + // 数据不存在,就加载一次 + this.load(); + } + return this.indexData; + } + + async fetchIndex(url) { + return makeHttpRequest>(url, { + method: 'GET', + dataType: 'json', + }); + } +} +``` + +:::tip + +上面的代码,故意抽离了 `fetchIndex` 方法,用来方便后续的模拟行为。 + +::: + +当接口未开发完毕的时候,我们本地开发就很困难,常见的做法是定义一份 JSON 数据, + +比如,创建一个 `src/mock/indexData.mock.ts` ,用于初始化的服务接口模拟。 + +```typescript +// src/mock/indexData.mock.ts +import { Mock, ISimulation } from '@midwayjs/core'; + +@Mock() +export class IndexDataMock implements ISimulation { +} +``` + +`@Mock` 用于代表它是一个模拟类,用于模拟一些业务行为,`ISimulation` 是需要业务实现的一些接口。 + +比如,我们要模拟接口的数据。 + +```typescript +// src/mock/indexData.mock.ts +import { App, IMidwayApplication, Inject, Mock, ISimulation, MidwayMockService } from '@midwayjs/core'; +import { IndexDataService } from '../service/indexData.service'; + +@Mock() +export class IndexDataMock implements ISimulation { + + @App() + app: IMidwayApplication; + + @Inject() + mockService: MidwayMockService; + + async setup(): Promise { + // 使用 MidwayMockService API 模拟属性 + this.mockService.mockClassProperty(IndexDataService, 'fetchIndex', async (url) => { + // 根据逻辑返回不同的数据 + if (/current/.test(url)) { + return { + data: require('./resource/current.json'), + }; + } else if (/v7/.test(url)) { + return { + data: require('./resource/v7.json'), + }; + } else if (/v6/.test(url)) { + return { + data: require('./resource/v6.json'), + }; + } + }); + } + + enableCondition(): boolean | Promise { + // 模拟类启用的条件 + return ['local', 'test', 'unittest'].includes(this.app.getEnv()); + } +} +``` + +上面的代码中,`enableCondition` 是必须实现的方法,代表当前模拟类的启用条件,比如上面的代码仅在 `local` ,`test` 和 `unittest` 环境下生效。 + + + +### 模拟时机 + +模拟类包含一些模拟的时机,都已经定义在 `ISimulation` 接口中,比如: + +```typescript +export interface ISimulation { + /** + * 最开始的模拟时机,在生命周期 onConfigLoad 之后执行 + */ + setup?(): Promise; + /** + * 在生命周期关闭时执行,一般用于数据清理 + */ + tearDown?(): Promise; + /** + * 在每种框架初始化时执行,会传递当前框架的 app + */ + appSetup?(app: IMidwayApplication): Promise; + /** + * 在每种框架的请求开始时执行,会传递当前框架的 app 和 ctx + */ + contextSetup?(ctx: IMidwayContext, app: IMidwayApplication): Promise; + /** + * 每种框架的请求结束时执行,在错误处理之后 + */ + contextTearDown?(ctx: IMidwayContext, app: IMidwayApplication): Promise; + /** + * 每种框架的停止时执行 + */ + appTearDown?(app: IMidwayApplication): Promise; + /** + * 模拟的执行条件,一般是特定环境,或者特定框架下 + */ + enableCondition(): boolean | Promise; +} +``` + +基于上面的接口,我们实现非常自由的模拟逻辑。 + +比如,在不同的框架上添加不同的中间件。 + +```typescript +import { App, IMidwayApplication, Mock, ISimulation } from '@midwayjs/core'; + +@Mock() +export class InitDataMock implements ISimulation { + + @App() + app: IMidwayApplication; + + async appSetup(app: IMidwayApplication): Promise { + // 针对不同的框架类型,添加不同的测试中间件 + if (app.getNamespace() === 'koa') { + app.useMiddleware(/*...*/); + app.useFilter(/*...*/); + } + + if (app.getNamespace() === 'bull') { + app.useMiddleware(/*...*/); + app.useFilter(/*...*/); + } + } + + enableCondition(): boolean | Promise { + return ['local', 'test', 'unittest'].includes(this.app.getEnv()); + } +} +``` + + diff --git a/site/versioned_docs/version-3.0.0/ops/ecs_start_err.md b/site/versioned_docs/version-3.0.0/ops/ecs_start_err.md new file mode 100644 index 000000000000..993a00e096a8 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/ops/ecs_start_err.md @@ -0,0 +1,49 @@ +# 服务器启动失败排查 + +应用启动失败是非常常见的现象,逻辑错误,编译错误,配置错误,环境问题,都有可能导致你的项目无法启动。 + + +## 快速定位代码问题 + +大多数情况下,我们说的启动失败一般都是服务器环境启动失败,下面以 Linux 为例。 + +1、使用 `ps aux | grep node` 检查进程是否存在,进程数量是否正确 + +2、打开 [项目的日志目录](/docs/logger_v3#配置日志根目录),查看 `common-error.log` 文件的内容,根据最新的堆栈来排查原因 + +3、启动的控制台日志,比如 `pm2 logs` 相关的信息 + + +大多数问题都会在日志中发现,请尽可能养成登录机器查看日志的习惯,这是开发者必备的技能。 + + + +## 可能的环境问题 + +除了代码本身的问题之外,环境可能也会带来一些问题,这些问题查起来就比较困难,往往和系统,权限,环境变量,启动参数,网络环境,甚至内核本身有关系。 + +下面列举一些可能的情况。 + +### 1、文件不完整或不是最新 + +确保的你的项目在部署前执行过以下的流程 + +- 1、使用 `npm run dev` 或者类似命令本地启动并运行成功 +- 2、使用 `npm run build` 已经将 ts 文件编译为 js 文件,在根目录生成出 `dist` 目录 +- 3、使用 `npm run start` 在本地使用 js 文件执行成功 + +检查服务器上的文件,目录结构是否完整,比如: + +- 1、`node_modules` 目录是否存在 +- 2、`dist` 目录以及其中的 js 文件是否存在,或者为最新 + +### 2、启动用户的权限问题 + +我们一般会使用一个普通账户来执行,比如 admin 账户,而不是使用 sudo 去部署。 + +- 1、检查用户是否用创建目录,启动 node 的权限 +- 2、检查项目的服务端日志目录是否有写入的权限 + +### 3、启动端口冲突 + +如果你启动了多个 Node.js 项目,如果使用了相同的端口,那么就会抛出端口重复使用的错误。 diff --git a/site/versioned_docs/version-3.0.0/pipe.md b/site/versioned_docs/version-3.0.0/pipe.md new file mode 100644 index 000000000000..46ab7df398db --- /dev/null +++ b/site/versioned_docs/version-3.0.0/pipe.md @@ -0,0 +1,191 @@ +# 管道 + +管道是参数装饰器的内部机制,可以在参数装饰器逻辑之后执行一些自定义代码,一般用于以下的场景: + +- 1、数据的校验 +- 2、参数的转换 + + + +## 组件提供的管道 + +`@midwayjs/validate` 默认提供了验证管道,只需要启用组件即可使用。 + +例如: + +```typescript +@Controller('/api/user') +export class HomeController { + + @Post('/') + async updateUser(@Body() user: UserDTO ) { + // ... + } +} +``` + +`@Body` 装饰器已经被自动注册了 `ValidatePipe` ,如果 `UserDTO` 是一个已经经过 `@Rule` 装饰器修饰的 DTO,会自动校验并转换。 + +如果使用了基础类型,则也可以通过数据转换管道进行校验和转换。 + +例如: + +```typescript +import { ParseIntPipe } from '@midwayjs/validate'; + +@Controller('/api/user') +export class HomeController { + + @Post('/update_age') + async updateAge(@Body('age', [ParseIntPipe]) age: number ) { + // ... + } +} +``` + +`ParseIntPipe` 管道可以将字符串,数字数据转换为数字,这样从请求参数获取到的 `age` 字段则会通过管道的校验并转换为数字格式。 + +除此之外,还提供了 `ParseBoolPipe` ,`ParseFloatPipe` 等更多数据转换管道,具体请查看 [Validate 组件](./extensions/validate)。 + + + +## 自定义管道 + +管道可以是一个实现 `PipeTransform` 接口的类或者方法,我们一般将管道放在 `src/pipe` 目录。 + +比如: + +```typescript +// src/pipe/validate.pipe.ts +import { Pipe, PipeTransform, TransformOptions } from '@midwayjs/core'; + +@Pipe() +export class ValidatePipe implements PipeTransform { + transform(value: T, options: TransformOptions): R { + return value; + } +} +``` + +`PipeTransform` 是每个管道必须要实现的泛型接口。泛型 `T` 表明输入的 `value` 的类型,`R` 表明 `transfrom()` 方法的返回类型。 + +为实现 `PipeTransfrom`,每个管道必须声明 `transfrom()` 方法。该方法有两个参数: + +- `value` +- `options` + +`value` 是当前处理的参数值,`options` 是当前处理的选项,包含以下属性。 + +```typescript +export TransformOptions { + metaType: TSDesignType; + metadata: Record; + target: any; + methodName: string; +} +``` + +| 参数 | 描述 | +| :--------- | :----------------------------------------------------------- | +| metaType | 一个 ts 元数据类型的解析对象,包含 `name` 、`originDesign`、`isBaseType` 三个属性。 | +| metadata | 参数装饰器的元数据对象 | +| target | 当前装饰的实例本身 | +| methodName | 当前参数装饰器装饰器的方法名 | + + + +## 绑定管道 + +管道必须依附在参数装饰器上使用。 + +在自定义装饰器的选项中,我们可以透传管道参数达到应用管道的目的。 + +例如我们自定义一个 `RegValid` 参数装饰器,用于传入正则和另一个管道参数: + +```typescript +import { PipeUnionTransform, createCustomParamDecorator } from '@midwayjs/core'; + +function RegValid(reg: RegExp, pipe: PipeUnionTransform) { + return createCustomParamDecorator('reg-valid', { + reg, + }, { + // ... + pipes: [pipe] + }); +} +``` + +`createCustomParamDecorator` 的第三个参数支持传入一个 `pipes` 属性,我们需要将管道传入其中,这样管道就会和装饰器绑定,在后续的运行中自动执行。 + +具体可以查询 [自定义装饰器](./custom_decorator) 中的参数装饰器章节。 + +`RegValid` 装饰器用于正则的校验,实现部分我们暂时忽略。 + +另外,我们再定义一个管道用于截取数据。 + +```typescript +@Pipe() +export class CutPipe implements PipeTransform { + transform(value: number, options: TransformOptions): string { + return String(value).slice(5); + } +} +``` + +现在我们可以使用他们了。 + +```typescript +class UserService { + async invoke(@RegValid(/\d{11}/, CutPipe) phoneNumber: string) { + return phoneNumber; + } +} + +invoke(13712345678) => '345678' +``` + + + +## 默认绑定的管道 + +假如我们希望向一个现成的参数装饰器能拥有管道能力,但是不希望该装饰器有管道参数。 + +就像内置的 `@Query` 等装饰器,没有管道参数,却可以在 validate 组件启用时自动执行管道逻辑。 + +我们使用 `decoratorService` 提供的反向注册 API,这在跨组件提供能力时非常有用。 + +我们以上面编写的 `RegValid` 为例。 + +```typescript +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + decoratorService: MidwayDecoratorService; + + async onReady(container: IMidwayContainer) { + // register default pipe + this.decoratorService.registerParameterPipes('reg-valid', [ + CutPipe, + ]); + } +} +``` + +`registerParameterPipes` 方法用于向某一种参数装饰器隐式注册一些管道,上述实例中,`reg-valid` 是自定义参数的 key,通过 key 我们可以向这个参数装饰器注册。 + +这些管道会在显式传入的管道之前被默认执行。 + +这样在使用时,即使我们不传递管道参数,也依旧会执行管道。 + +```typescript +class UserService { + async invoke(@RegValid(/\d{11}/) phoneNumber: string) { + return phoneNumber; + } +} + +invoke(13712345678) => '345678' +``` + diff --git a/site/versioned_docs/version-3.0.0/pipeline.md b/site/versioned_docs/version-3.0.0/pipeline.md new file mode 100644 index 000000000000..a7af767596f9 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/pipeline.md @@ -0,0 +1,445 @@ +# 流程控制 + +有些场景下,我们希望把一个完整的任务拆分成不同的阶段,每个阶段执行的逻辑相对独立,同时又可以通过并行或者串行的方式提升整体的执行效率。在 Midway 中我们实现了一个优化的 Pipeline 模式。 + + + +## Pipeline + + +在 Node.js 的 Stream 实现中,可以使用 `a.pipe(b).pipe(c).pipe(d)` 这样,把多个 Stream 串连起来。但是这样只能顺序执行的 pipe 实现不一定能够满足不同的业务场景。 + + +在 Midway 中我们使用 `@Pipeline` 装饰器,可以创建一个继承与 `IPipelineHandler` 接口的实例,可以将多个 `IValveHandler` 实例串联起来执行。 + + + `IValveHandler` 就是具体的任务阶段执行单位。整个 IPipelineHandler 执行方式可以是 parallel、series、concat、waterfall (很熟悉是吧?我们参考了 [async](https://github.com/caolan/async) 库提供的方法能力命名)。 + + +Pipeline 执行时期的上下文 IPipelineContext 可以用来存储 Pipeline 入参、上一次 IValveHandler 实例的执行结果、上一次的中间产物等等,提供了非常大的灵活性。 + + + + +## 类型定义 + + +### IPipelineHandler + +```typescript +interface IPipelineHandler { + /** + * 并行执行,使用 Promise.all + * @param opts 执行参数 + */ + parallel(opts: IPipelineOptions): Promise>; + /** + * 并行执行,最终 result 为数组 + * @param opts 执行参数 + */ + concat(opts: IPipelineOptions): Promise>; + /** + * 串行执行,使用 foreach await + * @param opts 执行参数 + */ + series(opts: IPipelineOptions): Promise>; + /** + * 串行执行,使用 foreach await,最终 result 为数组 + * @param opts 执行参数 + */ + concatSeries(opts: IPipelineOptions): Promise>; + /** + * 串行执行,但是会把前者执行结果当成入参,传入到下一个执行中去,最后一个执行的 valve 结果会被返回 + * @param opts 执行参数 + */ + waterfall(opts: IPipelineOptions): Promise>; +} +``` + + + +- 白名单机制 +使用 Pipeline 装饰器时,如果填写了数组参数,那么方法执行函数中的 valves 入参只能是装饰器数组参数中的项。当然,valves 是可选项,不填默认就以装饰器数组参数为准。例如,`@Pipeline(['a', 'b', 'c'])` 那么 series 等执行函数中可选参数 `opts.valves` 数组必须是 `['a', 'b', 'c']` 或者其子集,如果不填则以 `['a', 'b', 'c']` 逻辑顺序来执行。 + + + +### 返回结果 + + +IPipelineResult 的类型如下。 +```typescript +/** + * pipeline 执行返回结果 + */ +export interface IPipelineResult { + /** + * 是否成功 + */ + success: boolean; + /** + * 异常信息(如果有则返回) + */ + error?: { + /** + * 异常出在那个 valve 上 + */ + valveName?: string; + /** + * 异常信息 + */ + message?: string; + /** + * 原始 Error + */ + error?: Error; + }; + /** + * 返回结果 + */ + result: T; +} +``` + + +## 使用举例 + +1. 假设有这样一个场景,我们需要一次性获取页面上的数据信息、当前用户信息以及有几个 Tab。那么我们先声明返回的数据类型 +```typescript +class VideoDto { + videoId: string; + videoUrl: string; + videoTitle: string; +} +class AccountDto { + id: string; + nick: string; + isFollow: boolean; +} +class TabDto { + tabId: string; + title: string; + index: number; +} +interface HomepageDto { + videos: VideoDto[]; + account: AccountDto; + tab: TabDto; +} + +``` + +2. 实现一个 TestService 来封装一下返回的这些数据 +```typescript + +@Provide() +class TestService { + // 返回当前登录用户信息 + async getAccount(args: any): Promise { + return { + id: 'test_account_id', + nick: 'test hello', + isFollow: true, + }; + } + // 返回视屏列表 + async getVideos(args: any): Promise { + return [{ + videoId: '123', + videoUrl: 'https://www.taobao.com/xxx.mp4', + videoTitle: 'test 1 video' + }, { + videoId: '234', + videoUrl: 'https://www.taobao.com/xxx234.mp4', + videoTitle: 'test 2 video' + }, { + videoId: '456', + videoUrl: 'https://www.taobao.com/xxx456.mp4', + videoTitle: 'test 3 video' + }]; + } + // 返回tab页面 + async getTab(args: any): Promise { + return { + title: 'test tab', + tabId: 'firstTab', + index: 0 + }; + } +} + +``` + +3. 将几个任务封装拆分成不同的 IValveHandler 实现 +```typescript +// 返回视屏信息的 +@Provide() +class VideoFeeds implements IValveHandler { + alias = 'videos'; + + @Inject() + service: TestService; + + async invoke(ctx: IPipelineContext): Promise { + return this.service.getVideos(ctx.args); + } +} +// 返回账户信息的 +@Provide() +class AccountMap implements IValveHandler { + alias = 'account'; + + @Inject() + service: TestService; + + async invoke(ctx: IPipelineContext): Promise { + + // 获取数据执行逻辑 + return this.service.getAccount(ctx.args); + } +} +// 返回tab信息的 +@Provide() +class CrowFeeds implements IValveHandler { + alias = 'tab'; + @Inject() + service: TestService; + + async invoke(ctx: IPipelineContext): Promise { + // 获取数据执行逻辑 + return this.service.getTab(ctx.args); + } +} +// 捕捉整个错误异常的 +@Provide() +class ErrorFeeds implements IValveHandler { + alias = 'tab'; + @Inject() + service: TestService; + + async invoke(ctx: IPipelineContext): Promise { + // 获取数据执行逻辑 + throw new Error('this is error feeds'); + } +} +``` +### parallel + + +通过该方法执行的结果,最终返回的是一个 object 对象,且每个 IValveHandler 实现 alias 作为对象返回值的 key +```typescript +class StageTest { + // 这里声明一个 pipeline + @Pipeline([VideoFeeds, AccountMap, CrowFeeds]) + stages: IPipelineHandler; + + async runParallel(): Promise { + // 这里并发执行 videoFeeds、accountMap、crowFeeds + return this.stages.parallel({ + args: {aa: 123} + }); + + // 返回的 result 结构 + /* + { + // 以 accountMap 的 alias account 作为返回对象的 key + account: { + id: 'test_account_id', + nick: 'test hello', + isFollow: true, + }, + // 以 videoFeeds 的 alias video 作为返回对象的 key + video: [ + { + videoId: '123', + videoUrl: 'https://www.taobao.com/xxx.mp4', + videoTitle: 'test 1 video' + }, { + videoId: '234', + videoUrl: 'https://www.taobao.com/xxx234.mp4', + videoTitle: 'test 2 video' + }, { + videoId: '456', + videoUrl: 'https://www.taobao.com/xxx456.mp4', + videoTitle: 'test 3 video' + } + ], + // 以 crowFeeds 的 alias tab 作为返回对象的 key + tab: { + title: 'test tab', + tabId: 'firstTab', + index: 0 + } + } + */ + } +} +``` + + +### concat + + +执行方式同 parallel 只不过最终返回结果 result 是一个数组 +```typescript +class StageTest { + // 这里声明一个 pipeline + @Pipeline([VideoFeeds, AccountMap, CrowFeeds]) + stages: IPipelineHandler; + + async runConcat(): Promise { + // 这里并发执行 videoFeeds、accountMap、crowFeeds + return this.stages.concat({ + args: {aa: 123} + }); + + // 这里返回的 result 是一个数组 + /* + [ + // 以 videoFeeds 作为第一个返回对象 + [ + { + videoId: '123', + videoUrl: 'https://www.taobao.com/xxx.mp4', + videoTitle: 'test 1 video' + }, { + videoId: '234', + videoUrl: 'https://www.taobao.com/xxx234.mp4', + videoTitle: 'test 2 video' + }, { + videoId: '456', + videoUrl: 'https://www.taobao.com/xxx456.mp4', + videoTitle: 'test 3 video' + } + ], + // 以 accountMap 作为第二个返回对象 + { + id: 'test_account_id', + nick: 'test hello', + isFollow: true, + }, + // 以 crowFeeds 作为第三个返回对象 + { + title: 'test tab', + tabId: 'firstTab', + index: 0 + } + ] + */ + } +} +``` + + +### series + + +这里 series 是串行执行,按照 Pipeline 装饰器参数顺序,一个一个执行下去,且 IPipelienContext 中的 prev 就是前一个 valve,current 是当前,next 即下一个即将执行的 valve +```typescript +class StageTest { + // 这里声明一个 pipeline + @Pipeline([VideoFeeds, AccountMap, CrowFeeds]) + stages: IPipelineHandler; + + async runSeries(): Promise { + // 这里串行执行 videoFeeds、accountMap、crowFeeds + return this.stages.series({ + args: {aa: 123} + }); + + // 这里返回的 result 是一个对象,结果同 parallel 返回的对象拼装规则 + } +} +``` + + +### concatSeries + + +原理同 series,只不过返回结果是一个数组 +```typescript +class StageTest { + // 这里声明一个 pipeline + @Pipeline([VideoFeeds, AccountMap, CrowFeeds]) + stages: IPipelineHandler; + + async runConcatSeries(): Promise { + // 这里串行执行 videoFeeds、accountMap、crowFeeds + return this.stages.concatSeries({ + args: {aa: 123} + }); + + // 这里返回的 result 是一个数组,同 concat 返回对象拼装 + } +} +``` + + +### waterfall + + +串行执行,最终只返回最后一个 valve 执行结果 + + +```typescript +@Provide() +class StageOne implements IValveHandler { + async invoke(ctx: IPipelineContext): Promise { + if (ctx.args.aa !== 123) { + throw new Error('args aa is undefined'); + } + ctx.set('stageone', 'this is stage one'); + ctx.set('stageone_date', Date.now()); + if (ctx.info.current !== 'stageOne') { + throw new Error('current stage is not stageOne'); + } + if (ctx.info.next !== 'stageTwo') { + throw new Error('next stage is not stageTwo'); + } + if (ctx.info.prev) { + throw new Error('stageOne prev stage is not undefined'); + } + + return 'stageone'; + } +} + +@Provide() +class StageTwo implements IValveHandler { + async invoke(ctx: IPipelineContext): Promise { + const keys = ctx.keys(); + if (keys.length !== 2) { + throw new Error('keys is not equal'); + } + ctx.set('stagetwo', ctx.get('stageone') + 1); + ctx.set('stagetwo_date', Date.now()); + // 验证是否是执行 stageOne 返回的结果 + if (ctx.info.prevValue !== 'stageone') { + throw new Error('stageone result empty'); + } + if (ctx.info.current !== 'stageTwo') { + throw new Error('current stage is not stageTwo'); + } + if (ctx.info.next) { + throw new Error('stageTwo next stage is not undefined'); + } + if (ctx.info.prev !== 'stageOne') { + throw new Error('prev stage is not stageOne'); + } + + return 'stagetwo'; + } +} + +class StageTest { + // 这里声明一个 pipeline + @Pipeline([StageOne, StageTwo]) + stages: IPipelineHandler; + + async runStagesWaterfall(): Promise { + // 这里通过串行方式执行,可以看到 stageTwo 中做了校验,prevValue 即 stageOne 执行的结果 + return this.stages.waterfall({ + args: {aa: 123} + }); + } +} +``` \ No newline at end of file diff --git a/site/versioned_docs/version-3.0.0/quick_guide.md b/site/versioned_docs/version-3.0.0/quick_guide.md new file mode 100644 index 000000000000..dda18a30b928 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/quick_guide.md @@ -0,0 +1,607 @@ +# 快速入门 + +如果你没有接触过 Midway,没关系,本章节我们将从实例的角度,一步步地搭建出一个 Midway 标准应用,展示天气信息,让你能快速的入门 Midway。 + + + +## 环境准备 + +- 操作系统:支持 macOS,Linux,Windows +- 运行环境:[Node.js 环境要求](/docs/intro#环境准备工作) + + + +## 初始化项目 + +我们推荐直接使用脚手架,只需几条简单指令,即可快速生成项目。 + +```bash +$ npm init midway@latest -y +``` + +选择 `koa-v3` 项目进行初始化创建,项目名可以自定,比如 `weather-sample`。 + +现在可以启动应用来体验下。 + +```bash +$ npm run dev +$ open http://localhost:7001 +``` + +同时,我们也提供了完整的实例,可以在 `npm init midway` 之后,选择 `quick-start` 项目,创建即可,方便对照学习。 + + + +## 编写 Controller + +如果你熟悉 Web 开发或 MVC,就知道第一步我们需要编写 [Controller 和 Router](./controller)。 + +在脚手架创建的文件中,我们已经有了一些文件,我们暂时忽略他们。 + +在 `controller` 目录中,新建一个 `src/controller/weather.controller.ts` 文件,内容如下。 + +```typescript +import { Controller, Get } from '@midwayjs/core'; + +@Controller('/') +export class WeatherController { + // 这里是装饰器,定义一个路由 + @Get('/weather') + async getWeatherInfo(): Promise { + // 这里是 http 的返回,可以直接返回字符串,数字,JSON,Buffer 等 + return 'Hello Weather!'; + } +} +``` + +现在我们可以通过访问 `/weather` 接口返回数据了。 + + + +## 添加参数处理 + +在示例中,我们需要一个 URL 参数来动态展示不同城市的天气。 + +通过添加 `@Query` 装饰器,我们可以获取到 URL 上的参数。 + +```typescript +import { Controller, Get, Query } from '@midwayjs/core'; + +@Controller('/') +export class WeatherController { + @Get('/weather') + async getWeatherInfo(@Query('cityId') cityId: string): Promise { + return cityId; + } +} +``` + +除了 `@Query` 装饰器,Midway 也提供了其他请求参数的获取,可以查看 [路由和控制](./controller) 文档。 + +## 编写 Service + +在实际项目中,Controller 一般用来接收请求参数,校验参数,不会包括特别复杂的逻辑,复杂而复用的逻辑,我们应该封装为 Service 文件。 + +我们来添加一个 Service 用来获取天气信息,其中包括一个 http 请求,获取远端的数据。 + +代码如下: + +```typescript +// src/service/weather.service.ts +import { Provide, makeHttpRequest } from '@midwayjs/core'; + +@Provide() +export class WeatherService { + async getWeather(cityId: string) { + return makeHttpRequest(`https://midwayjs.org/resource/${cityId}.json`, { + dataType: 'json', + }); + } +} +``` + +:::info + +- 1、`makeHttpRequest` 方法是 Midway 内置的 http 请求方法,更多参数请查看 [文档](./extensions/axios) + +::: + +然后我们来添加定义,良好的类型定义可以帮助我们减少代码错误。 + +在 `src/interface.ts` 文件中,我们增加天气信息的数据定义。 + +```typescript +// src/interface.ts + +// ... + +export interface WeatherInfo { + weatherinfo: { + city: string; + cityid: string; + temp: string; + WD: string; + WS: string; + SD: string; + AP: string; + njd: string; + WSE: string; + time: string; + sm: string; + isRadar: string; + Radar: string; + } +} +``` + +这样,我们就可以在 Service 中进行标注了。 + +```typescript +import { Provide, makeHttpRequest } from '@midwayjs/core'; +import { WeatherInfo } from '../interface'; + +@Provide() +export class WeatherService { + async getWeather(cityId: string): Promise { + const result = await makeHttpRequest(`https://midwayjs.org/resource/${cityId}.json`, { + dataType: 'json', + }); + + if (result.status === 200) { + return result.data as WeatherInfo; + } + } +} + +``` + +:::info + +- 1、这里使用 `@Provide` 装饰器修饰类,便于后续 Controller 注入该类 + +::: + + + +同时,我们修改下之前的 Controller 文件。 + +```typescript +import { Controller, Get, Inject, Query } from '@midwayjs/core'; +import { WeatherInfo } from '../interface'; +import { WeatherService } from '../service/weather.service'; + +@Controller('/') +export class WeatherController { + + @Inject() + weatherService: WeatherService; + + @Get('/weather') + async getWeatherInfo(@Query('cityId') cityId: string): Promise { + return this.weatherService.getWeather(cityId); + } +} +``` + +:::info + +- 1、这里使用 `@Inject` 装饰器注入 `WeatherService`,是 Midway 依赖注入的标准用法,可以查看 [这里](./service) 了解更多 +- 2、这里也同步修改了方法的返回值类型 + +::: + +到这里,我们可以请求 `http://127.0.0.1:7001/weather?cityId=101010100` 查看返回的结果。 + +你的第一个 Midway 接口已经开发完成了,你可以在前端代码中直接调用了,接下去,我们将利用这个接口完成一个服务端渲染的页面。 + + + +## 模板渲染 + +从这里开始,我们需要用到一些 Midway 的扩展能力。 + +Midway 对应的扩展包我们称为 “组件”,也是标准的 npm 包。 + +这里我们需要用到 `@midwayjs/view-nunjucks` 组件。 + +可以使用下面的命令安装。 + +```bash +$ npm i @midwayjs/view-nunjucks --save +``` + +安装完成后,我们在 `src/configuration.ts` 文件中启用组件。 + +```typescript +// ... +import * as view from '@midwayjs/view-nunjucks'; + +@Configuration({ + imports: [ + koa, + // ... + view + ], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration { + // ... +} + +``` + +:::info + +- 1、`configuration` 文件是 Midway 的生命周期入口文件,承担了组件开关,配置加载和生命周期管理的作用 +- 2、`imports` 就使用来导入(开启)组件的方法 + +::: + +在 `src/config/config.default.ts` 中配置组件,指定为 `nunjucks` 模板。 + +```typescript +import { MidwayConfig } from '@midwayjs/core'; + +export default { + // ... + view: { + defaultViewEngine: 'nunjucks', + }, +} as MidwayConfig; + +``` + +在根目录(非 src 里)添加模板 `view/info.html` 文件,内容如下: + +```html + + + + 天气预报 + + + +
+
+

+ {{city}}({{WD}}{{WS}}) +

+

{{temp}}

+

+ 气压 +

+

+ 湿度 +

+
+
+ + + +``` + +同时,我们调整 Controller 的代码,将返回 JSON 变为模板渲染。 + +```typescript +// src/controller/weather.controller.ts +import { Controller, Get, Inject, Query } from '@midwayjs/core'; +import { WeatherService } from '../service/weather.service'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class WeatherController { + + @Inject() + weatherService: WeatherService; + + @Inject() + ctx: Context; + + @Get('/weather') + async getWeatherInfo(@Query('cityId') cityId: string): Promise { + const result = await this.weatherService.getWeather(cityId); + if (result) { + await this.ctx.render('info', result.weatherinfo); + } + } +} +``` + +到这一步,我们访问 `http://127.0.0.1:7001/weather?cityId=101010100` 已经可以看到渲染的模板内容了。 + + + +## 错误处理 + +别忘了,我们还有一些异常的逻辑需要处理。 + +一般来说,每个对外的调用都需要做异常捕获,并且将异常转变为我们自己业务的错误,这样才能有更好的体验。 + +为此,我们需要定义一个我们自己的业务错误,创建一个 `src/error/weather.error.ts` 文件。 + +```typescript +// src/error/weather.error.ts +import { MidwayError } from '@midwayjs/core'; + +export class WeatherEmptyDataError extends MidwayError { + constructor(err?: Error) { + super('weather data is empty', { + cause: err, + }); + if (err?.stack) { + this.stack = err.stack; + } + } +} +``` + +然后,我们调整 Service 代码抛出异常。 + +```typescript +// src/service/weather.service.ts +import { Provide, makeHttpRequest } from '@midwayjs/core'; +import { WeatherInfo } from '../interface'; +import { WeatherEmptyDataError } from '../error/weather.error'; + +@Provide() +export class WeatherService { + async getWeather(cityId: string): Promise { + if (!cityId) { + throw new WeatherEmptyDataError(); + } + + try { + const result = await makeHttpRequest(`https://midwayjs.org/resource/${cityId}.json`, { + dataType: 'json', + }); + if (result.status === 200) { + return result.data as WeatherInfo; + } + } catch (error) { + throw new WeatherEmptyDataError(error); + } + } +} +``` + +:::info + +- 1、将 http 的调用请求进行错误捕获,将错误包裹,返回一个我们系统的业务错误 +- 2、如有必要,我们可以定义更多的错误,分配错误 Code 等 + +::: + +到这一步,我们还需要将异常进行业务处理,比如有多个位置抛出 `WeatherEmptyDataError` 时,我们需要统一的格式返回。 + +错误处理器可以完成这个功能,我们需要创建一个 `src/filter/weather.filter.ts` 文件,内容如下: + +```typescript +//src/filter/weather.filter.ts +import { Catch } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { WeatherEmptyDataError } from '../error/weather.error'; + +@Catch(WeatherEmptyDataError) +export class WeatherErrorFilter { + async catch(err: WeatherEmptyDataError, ctx: Context) { + ctx.logger.error(err); + return '

weather data is empty

'; + } +} + +``` + +然后应用到当前的框架中。 + +```typescript +import { Configuration, App } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import { WeatherErrorFilter } from './filter/weather.filter'; +// ... + +@Configuration({ + // ... +}) +export class MainConfiguration { + @App() + app: koa.Application; + + async onReady() { + // ... + + // add filter + this.app.useFilter([WeatherErrorFilter]); + } +} +``` + +这样,当每次请求中获取到了 `WeatherEmptyDataError` 错误,会使用相同的返回值返回给浏览器,同时会在日志中记录原始的错误信息。 + +异常处理的更多信息,可以查阅 [文档](./error_filter)。 + + + +## 数据模拟 + +在编写代码时,我们的接口经常还处在无法使用的阶段,为了尽可能降低影响,可以使用模拟数据来代替。 + +比如我们的天气接口,就可以在本地和测试环境模拟掉。 + +我们需要创建一个 `src/mock/data.mock.ts` 文件,内容如下: + +```typescript +// src/mock/data.mock.ts +import { + Mock, + ISimulation, + App, + Inject, + IMidwayApplication, + MidwayMockService, +} from '@midwayjs/core'; +import { WeatherService } from '../service/weather.service'; + +@Mock() +export class WeatherDataMock implements ISimulation { + @App() + app: IMidwayApplication; + + @Inject() + mockService: MidwayMockService; + + async setup(): Promise { + const originMethod = WeatherService.prototype.getWeather; + this.mockService.mockClassProperty( + WeatherService, + 'getWeather', + async cityId => { + if (cityId === '101010100') { + return { + weatherinfo: { + city: '北京', + cityid: '101010100', + temp: '27.9', + WD: '南风', + WS: '小于3级', + SD: '28%', + AP: '1002hPa', + njd: '暂无实况', + WSE: '<3', + time: '17:55', + sm: '2.1', + isRadar: '1', + Radar: 'JC_RADAR_AZ9010_JB', + }, + }; + } else { + return originMethod.apply(this, [cityId]); + } + } + ); + } + + enableCondition(): boolean | Promise { + // 模拟类启用的条件 + return ['local', 'test', 'unittest'].includes(this.app.getEnv()); + } +} + +``` + +`WeatherDataMock` 类用于模拟天气数据,其中的 `setup` 方法,用于实际的初始化模拟,其中,我们使用了内置的 `MidwayMockService` 的 `mockClassProperty` 方法,将 `WeatherService` 的 `getWeather` 方法模拟掉。 + +在模拟过程中,我们仅仅将单个城市的数据进行了处理,其他依旧走了原来的接口。 + +`enableCondition` 用于标识这个模拟类在哪些场景下生效,比如我们上面的代码就仅在本地和测试环境生效。 + +这样,当本地开发和测试时,我们请求 `101010100` 的数据,将直接被拦截和返回,且在部署到服务器环境后,也不会受到影响。 + +数据模拟还有更多的接口可以使用,可以查阅 [文档](./mock)。 + + + +## 单元测试 + +Midway 默认使用 jest 作为基础的测试框架,一般我们的测试文件会放在根目录的 `test` 目录中,以 `*.test.ts` 作为后缀。 + +比如我们要测试编写的 `/weather` 接口。 + +我们需要测试它的成功和失败两种状态。 + +```typescript +import { createApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework, Application } from '@midwayjs/koa'; + +describe('test/controller/weather.test.ts', () => { + + let app: Application; + beforeAll(async () => { + // create app + app = await createApp(); + }); + + afterAll(async () => { + // close app + await close(app); + }); + + it('should test /weather with success request', async () => { + // make request + const result = await createHttpRequest(app).get('/weather').query({ cityId: 101010100 }); + + expect(result.status).toBe(200); + expect(result.text).toMatch(/北京/); + }); + + it('should test /weather with fail request', async () => { + const result = await createHttpRequest(app).get('/weather'); + + expect(result.status).toBe(200); + expect(result.text).toMatch(/weather data is empty/); + }); +}); + +``` + +执行测试: + +```bash +$ npm run test +``` + +就这么简单,更多请参见 [测试](./testing)。 + +:::info + +- 1、jest 测试时,以单文件作为单位,使用 `beforeAll` 和 `afterAll` 控制 app 的启停 +- 2、使用 `createHttpRequest` 来创建一个测试请求 +- 3、使用 expect 来断言返回的结果是否符合预期 + +::: + + + +## 继续学习 + +恭喜你,你对 Midway 已经有了一些初步的认识,我们来简单的回顾一下。 + +- 1、我们使用 `npm init midway` 来创建了示例 +- 2、使用 `@Controller` 装饰器定义路由和控制器类 +- 3、使用 `@Query` 获取请求参数 +- 4、使用 `@Provide` 和 `@Inject` 注入服务类 +- 5、使用 `imports` 启用组件,并配置了 nunjucks 模板 +- 6、自定义了错误,并使用错误过滤器来拦截错误,返回自定义的数据 +- 7、使用 jest 创建了测试,添加了成功和失败的测试用例 + +以上的这些,仅仅是 Midway 的一小部分内容,随着使用的深入,会使用到更多的能力。 + +你可以从 [创建](./quickstart) 开始,去选择 Midway 不同场景下的解决方案,也可以继续深入 [路由和控制器](./controller) 的部分,增加一些请求方法,也可以了解 [Web 中间件](./middleware) 或者 [依赖注入](./container) 相关的知识。 diff --git a/site/versioned_docs/version-3.0.0/quickstart.md b/site/versioned_docs/version-3.0.0/quickstart.md new file mode 100644 index 000000000000..2ce955a2a592 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/quickstart.md @@ -0,0 +1,154 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 创建第一个应用 + + +## 技术选型 + +Midway 有多套技术方案可以选择,我们以部署的方式来做区分: + +| 技术选型 | 描述 | +| --------------- | ------------------------------------------------------------ | +| 纯 Node.js 项目 | Midway 传统项目,纯 Node.js 研发,以 `@midwayjs/koa` 为代表的模块,最完整的支持后端项目,采用 **依赖注入 + Class** 为技术栈。 | +| Serverless 项目 | Midway 为 Serverless 场景单独开发的技术栈,以 `@midwayjs/faas` 为代表的模块,使用轻量的方式接入不同的 Serverless 平台。 | +| 一体化项目 | Midway 创新技术方案,采用前后端一体化开发方式,节省前后端联调时间,以 `@midwayjs/hooks` 为代表的模块,使用 **函数式** 为主要编码范式。 | + +:::tip +本章节及后续的文档将以 **纯 Node.js 项目** 作为基础示例,如需使用 Serverless 项目,请跳转到 [Serverless](serverless/serverless_intro),如需了解一体化项目,请访问 [一体化](hooks/intro) 。 +::: + + + +## 快速初始化 + + +使用 `npm init midway` 查看完整的脚手架列表,选中某个项目后,Midway 会自动创建示例目录,代码,以及安装依赖。 + +```bash +$ npm init midway@latest -y +``` + +针对 v3 项目,请选择 `koa-v3`,注意 [Node.js 环境要求](/docs/intro#环境准备工作)。 + +示例将创建一个类似下面的目录结构,其中最精简的 Midway 项目示例如下。 + +``` +➜ my_midway_app tree +. +├── src ## midway 项目源码 +│ └── controller ## Web Controller 目录 +│ └── home.controller.ts +├── test +├── package.json +└── tsconfig.json +``` +整个项目包括了一些最基本的文件和目录。 + + +- `src` 整个 Midway 项目的源码目录,你之后所有的开发源码都将存放于此 +- `test` 项目的测试目录,之后所有的代码测试文件都在这里 +- `package.json` Node.js 项目基础的包管理配置文件 +- `tsconfig.json` TypeScript 编译配置文件 + + +除了整个目录,我们还有一些其他的目录,比如 `controller` 目录。 + + +## 开发习惯 + + +Midway 对目录没有特别的限制,但是我们会遵守一些简单的开发习惯,将一部分常用的文件进行归类,放到一些默认的文件夹中。 + + +以下 ts 源码文件夹均在 `src` 目录下。 + + +常用的有: + + +- `controller` Web Controller 目录 +- `middleware` 中间件目录 +- `filter` 过滤器目录 +- `aspect` 拦截器 +- `service` 服务逻辑目录 +- `entity` 或 `model` 数据库实体目录 +- `config` 业务的配置目录 +- `util` 工具类存放的目录 +- `decorator` 自定义装饰器目录 +- `interface.ts` 业务的 ts 定义文件 + + + +随着不同场景的出现,目录的习惯也会不断的增加,具体的目录内容会体现在不同的组件功能中。 + + +## Web 框架选择 + + +Midway 设计之初就可以兼容多种上层框架,比如常见的 `Express`、`Koa` 和 `EggJS` 。 + +从 v3 开始,我们使用 Koa 来做基础示例的演示。 + +这些上层框架在 Midway 中都以组件的能力提供,都可以使用 Midway 提供的装饰器能力,但是 Midway 不会对特有的能力做出封装,比如 Egg.js 的插件体系,或者 Express 的中间件能力,如果你对其中的某个框架比较熟悉,或者希望使用特定框架的能力,就可以选择它作为你的主力 Web 框架。 + + +| 名称 | 描述 | +| --- | --- | +| @midwayjs/koa | 默认,Koa 是一个 Express 的替代框架,它默认支持了异步中间件等能力,是第二大通用的 Node.js Web 框架。 | +| @midwayjs/web | Egg.js 是国内相对常用的 Web 框架,包含一些默认插件。 | +| @midwayjs/express | Express 是一个众所周知的 node.js 简约 Web 框架。 这是一个经过实战考验,适用于生产的库,拥有大量社区资源。 | + + +如果你希望替代默认的 Web 框架,请参考对应的 [egg](extensions/egg) 或者 [express](extensions/express) 章节。 + + +## 启动项目 + + +```bash +$ npm run dev +$ open http://localhost:7001 +``` +Midway 会启动 HTTP 服务器,打开浏览器,访问 `http://127.0.0.1:7001` ,浏览器会打印出 `Hello midwayjs!` 的信息。 + + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01KoUxO91jydMw41Vv4_!!6000000004617-2-tps-1268-768.png) + +如果需要修改开发的启动端口,可以在 `package.json` 的 scripts 段落里修改,如修改为 6001: + + + + + +```typescript +"scripts": { + //... + "dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js --port 6001", +}, +``` + + + + + +```typescript +"scripts": { + //... + "dev": "cross-env NODE_ENV=local midway-bin dev --ts --port=6001", +}, +``` + + + + + + + +## 常见问题 + +### windows eslint 报错 + +:::caution +Windows 可能会碰到 eslint 报错的问题,请关注 [windows 下换行问题](faq/git_problem#XCAgm)。 +::: diff --git a/site/versioned_docs/version-3.0.0/release_schedule.md b/site/versioned_docs/version-3.0.0/release_schedule.md new file mode 100644 index 000000000000..3338bb71d69c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/release_schedule.md @@ -0,0 +1,10 @@ +# Midway 维护计划 + +下表是 Midway 整体的维护节奏和计划。 + +| Release | Status | Initial Release | Active LTS Start | Maintenance LTS Start | End-of-life | +| --- | --- | --- | --- | --- | --- | +| midway v1(inner v6) | **End-of-Life** | 2018-06-14 | 2018-10 | 2020-04 | 2022-04 | +| midway v2(inner v7) | **End-of-Life** | 2020-10 | 2021-02 | 2021-10 | 2024-04 | +| midway v3(inner v8) | **Maintenance LTS** | 2022-01 | 2022-06 | 2023-10 | | +| midway v4(inner v9) | **Development** | | | | | diff --git a/site/versioned_docs/version-3.0.0/req_res_app.md b/site/versioned_docs/version-3.0.0/req_res_app.md new file mode 100644 index 000000000000..0e3fd02da30c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/req_res_app.md @@ -0,0 +1,445 @@ +# Application 和 Context + +Midway 的应用会同时对外暴露不同协议,比如 Http,WebSocket 等等,这里每个协议对 Midway 来说都是由独立的组件提供的。 + +比如我们前面示例中的 `@midwayjs/koa` ,就是一个提供 Http 服务的组件,下面我们将以这个组件为例,来介绍内置对象。 + +每个使用的 Web 框架会提供自己独特的能力,这些独特的能力都会体现在各自的 **上下文**(Context)和 **应用**(Application)之上。 + + + +## 定义约定 + +为了简化使用,所有的暴露协议的组件会导出 **上下文**(Context)和 **应用**(Application)定义,我们都保持一致。即 `Context` 和 `Application` 。 + +比如: + +```typescript +import { Application, Context } from '@midwayjs/koa'; +import { Application, Context } from '@midwayjs/faas'; +import { Application, Context } from '@midwayjs/web'; +import { Application, Context } from '@midwayjs/express'; +``` + +且非 Web 框架,我们也保持了一致。 + +```typescript +import { Application, Context } from '@midwayjs/socketio'; +import { Application, Context } from '@midwayjs/grpc'; +import { Application, Context } from '@midwayjs/rabbitmq'; +``` + + + +## Application + +Application 是某一个组件中的应用对象,在不同的组件中,可能有着不同的实现。Application 对象上会包含一些统一的方法,这些方法统一来自于 `IMidwayApplication` 定义。 + +```typescript +import { Application } from '@midwayjs/koa'; +``` + + + +### 获取方式 + +在所有被依赖注入容器管理的类中,都可以使用 `@App()` 装饰器来获取 **当前最主要** 的 Application。 + +比如: + +```typescript +import { App, Controller, Get } from '@midwayjs/core'; +import { Application } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @App() + app: Application; + + @Get('/') + async home() { + // this.app.getConfig() + // this.app.getEnv() + } +} +``` + + + +### Main Application + +Midway 应用对外暴露的协议是组件带来的,每个组件都会暴露自己协议对应的 Application 对象。 + +这就意味着在一个应用中会包含多个 Application,我们默认约定,在 `src/configuration.ts` 中第一个引入的 Application 即为 **Main Application** (**主要的 Application**)。 + +比如,下面的 koa 中的 Application 实例即为 **Main Application** (**主要的 Application**)。 + +```typescript +// src/configuration.ts + +import { Configuration, ILifeCycle } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as ws from '@midwayjs/ws'; + +@Configuration({ + imports: [koa, ws] +}) +export class MainConfiguration implements ILifeCycle { + // ... +} +``` + +事实上 Application 都实现与 `IMidwayApplication` 接口,如果使用通用的 API,没有差别。 + +成为 Main Application 稍微有一些优势: + +- 在大部分的场景下,使用 `@App()` 即可注入获取,无需其他参数 +- 优先初始化 + +比如在多个导出 Application 组件需要加载中间件的情况下,可以简单的编码。 + +```typescript +// src/configuration.ts + +import { Configuration, ILifeCycle } from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as ws from '@midwayjs/ws'; + +@Configuration({ + imports: [koa, ws] +}) +export class MainConfiguration implements ILifeCycle { + @App() + koaApp: koa.Application; + + @App('webSocket') + wsApp: ws.Application; + + async onReady() { + this.koaApp.useMiddleweare(...); + this.wsApp.useMiddleweare(...); + } +} +``` + +非主要的 Application,需要通过 `@App()` 装饰器的参数或者 [ApplicationManager](./built_in_service#midwayapplicationmanager) 来获取。 + + `@App()` 装饰器的参数为组件的 `namespace`。 + +常见的 namespace 如下: + +| Package | Namespace | +| ------------------ | --------- | +| @midwayjs/web | egg | +| @midwayjs/koa | koa | +| @midwayjs/express | express | +| @midwayjs/grpc | gRPC | +| @midwayjs/ws | webSocket | +| @midwayjs/socketio | socketIO | +| @midwayjs/faas | faas | +| @midwayjs/kafka | kafka | +| @midwayjs/rabbitmq | rabbitMQ | +| @midwayjs/bull | bull | + + + +### getAppDir + +用于获取项目根目录路径。 + +```typescript +this.app.getAppDir(); +// => /my_project +``` + + + +### getBaseDir + +用于获取项目 TypeScript 基础路径,默认开发中为 `src` 目录,编译后为 `dist` 目录。 + +```typescript +this.app.getBaseDir(); +// => /my_project/src +``` + + + +### getEnv + +获取当前项目环境。 + +```typescript +this.app.getEnv(); +// => production +``` + + + +### getApplicationContext + +获取当前全局依赖注入容器。 + +```typescript +this.app.getApplicationContext(); +``` + + + +### getConfig + +获取配置。 + +```typescript +// 获取所有配置 +this.app.getConfig(); +// 获取特定 key 配置 +this.app.getConfig('koa'); +// 获取多级配置 +this.app.getConfig('midwayLoggers.default.dir'); +``` + + + +### getLogger + +获取某个 Logger,不传参数,默认返回 appLogger。 + +```typescript +this.app.getLogger(); +// => app logger +this.app.getLogger('custom'); +// => custom logger +``` + + + +### getCoreLogger + +获取 Core Logger。 + +```typescript +this.app.getCoreLogger(); +``` + + + +### getProjectName + +获取项目名,一般从 `package.json` 中获取。 + + + +### setAttr & getAttr + +直接在 Application 上挂载一个对象会导致定义和维护的困难。 + +在大多数情况下,用户需要的是临时的全局数据存储的方式,比如在一个应用或者组件内部跨文件临时存取一个数据,从一个类保存,另一个类获取。 + +为此 Midway 提供了一个全局数据存取的 API,解决这类需求。 + +```typescript +this.app.setAttr('abc', { + a: 1, + b: 2, +}); +``` + +在另一个地方获取即可。 + +```typescript +const value = this.app.getAttr('abc'); +console.log(value); +// { a: 1, b: 2 } +``` + + + +### getNamespace + +通过 `getNamespace` API ,可以获取到当前 app 归属的组件的 [框架的类型](#main-application)(即组件的 `namespace`)。 + +比如在 `koa` 组件中。 + +```typescript +this.app.getNamespace(); +// 'koa' +``` + + + + + +## Context + +Context 是一个**请求级别的对象**,在每一次收到用户请求时,框架会实例化一个 Context 对象, + +在 Http 场景中,这个对象封装了这次用户请求的信息,或者其他获取请求参数,设置响应信息的方法,在 WebSocket,Rabbitmq 等场景中,Context 也有各自的属性,以框架的定义为准。 + +下面的 API 是每个上下文实现通用的属性或者接口。 + + + +### 获取方式 + + +在 **默认的请求作用域 **中,也就是说在 控制器(Controller)或者普通的服务(Service)中,我们可以使用 `@Inject` 来注入对应的实例。 + + +比如可以这样获取到对应的 ctx 实例。 + +```typescript +import { Inject, Controller, Get } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async home() { + // ... + } +} +``` + +由于 `ctx` 是一个框架内置 ctx 实例关键字,如果你希望用不同的属性名,也可以通过修改装饰器参数。 + +```typescript +import { Inject, Controller, Get } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject('ctx') + customContextName: Context; + + @Get('/') + async home() { + // ... + } +} +``` + +如果一个服务可以被多个上层框架调用,由于不同框架提供的 ctx 类型不同,可以通过类型组合来解决。 + +```typescript +import { Inject, Controller, Get } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { Context as BullContext } from '@midwayjs/bull'; + +@Provide() +export class UserService { + + @Inject() + ctx: Context & BullContext; + + async getUser() { + // ... + } +} +``` + + + +除了显式声明外,在拦截器或者装饰器设计的时候,由于我们无法得知用户是否写了 ctx 属性,还可以通过内置的 `REQUEST_OBJ_CTX_KEY` 字段来获取。 + +比如: + +```typescript +import { Inject, Controller, Get, REQUEST_OBJ_CTX_KEY } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class HomeController { + + @Inject() + ctx: Context; + + @Get('/') + async home() { + ctx.logger.info(this.ctx === this[REQUEST_OBJ_CTX_KEY]); + // => true + } +} +``` + + + +### requestContext + +Midway 会为每个 Context 挂载一个 `requestContext` 属性,即请求作用域下的依赖注入容器,用来创建请求作用域下的对象。 + +```typescript +const userService = await this.ctx.requestContext.getAsync(UserService); +// ... +``` + + + +### logger + +请求作用域下的默认 logger 对象,包含上下文数据。 + +```typescript +this.ctx.logger.info('xxxx'); +``` + + + +### startTime + +上下文执行开始的时间。 + +```typescript +this.ctx.startTime +// 1642820640502 +``` + + + +### setAttr & getAttr + +和 `app` 上的方法相同,这些方法的数据是保存在请求链路中,随着请求销毁,你可以在其中放一些请求的临时数据。 + +```typescript +this.ctx.setAttr('abc', { + a: 1, + b: 2, +}); +``` + +在另一个地方获取即可。 + +```typescript +const value = this.ctx.getAttr('abc'); +console.log(value); +// { a: 1, b: 2 } +``` + + + +### getLogger + +获取某个自定义 Logger 对应的上下文日志。 + +```typescript +this.ctx.getLogger('custom'); +// => custom logger +``` + + + +### getApp + +从 ctx 上获取当前框架类型的 app 对象。 + +```typescript +const app = this.ctx.getApp(); +// app.getConfig(); +``` + diff --git a/site/versioned_docs/version-3.0.0/retry.md b/site/versioned_docs/version-3.0.0/retry.md new file mode 100644 index 000000000000..ad79cfe19075 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/retry.md @@ -0,0 +1,215 @@ +# 重试机制 + +从 Midway v3.5.0 开始,支持方法自定义重试逻辑。 + +很多时候,我们在某些容易失败,或者异步的方法调用上,需要多次使用 `try` 去包裹函数,同时处理错误。 + +比如: + +```typescript +// 定义了一个异步函数 + +async function invoke(id) { + + // 一些远程调用逻辑 + +} + + +async invokeNew() { + let error; + try { + return await invoke(1); + } catch(err) { + error = err; + } + + try { + return await invoke(2); + } catch(err) { + error = err; + } + + if (error) { + // .... + } +} +``` + +我们可能会尝试多次调用 `invoke`执行,同时配合 try/catch 的异常捕获,导致业务代码写起来重复且冗长。 + + + +## 定义重试函数 + +我们可以使用 `retryWithAsync`方法进行包裹,简化整个流程。 + +```typescript +import { retryWithAsync } from '@midwayjs/core'; + +async function invoke(id) { + // ... +} + +async function someServiceMethod() { + // 默认调用,加上重试两次,最多执行三次 + const invokeNew = retryWithAsync(invoke, 2); + + try { + return await invokeNew(1); + } catch(err) { + + // err + } +} +``` + +包裹后的方法参数和返回值,和原有方法完全一致。 + +当在重试周期内调用成功,未抛出错误,则会将成功的返回值返回。 + +如果失败,则会抛出 `MidwayRetryExceededMaxTimesError` 异常。 + +如果在类中使用,可能要注意 `this` 的指向。 + +示例如下: + +```typescript +import { retryWithAsync } from '@midwayjs/core'; + +export class UserService { + + async getUserData(userId: string) { + // wrap + const getUserDataOrigin = retryWithAsync( + this.getUserDataFromRemote, + 2, + { + receiver: this, + } + ); + + // invoke + return getUserDataOrigin(userId); + } + + async getUserDataFromRemote(userId: string) { + // get data from remote + } +} +``` + + + +## this 绑定 + +从 Midway v3.5.1 起,增加了一个 `receiver` 参数,用于在类的场景下绑定 this,用于处理: + +- 1、方法正确的 this 指向 +- 2、包裹方法定义的正确性 + +```typescript +// wrap +const getUserDataOrigin = retryWithAsync( + this.getUserDataFromRemote, + 2, + { + receiver: this, // 此参数用于处理 this 指向 + } +); +``` + +如果没有该参数,代码需要写成下面的样子才能绑定 this,同时返回的 `getUserDataOrigin` 方法的定义才正确。 + +```typescript +// wrap +const getUserDataOrigin = retryWithAsync( + this.getUserDataFromRemote.bind(this) as typeof this.getUserDataFromRemote, + 2, + { + receiver: this, + } +); + + +``` + + + + + +## 重试次数 + +`retryWithAsync` 提供了第二个参数,用于声明额外的重试次数,默认为 1(仅重试一次)。 + +这个值指代的是在默认调用后,额外重试的次数。 + + + +## 同步的重试 + +和 `retryWithAsync` 类似,我们也提供了 `retryWith` 这个同步方法,参数和 `retryWithAsync` 几乎相同,不再额外描述。 + + + +## 重试延迟 + +为了避免频繁重试对服务端造成压力,可以设置重试的间隔。 + +比如: + +```typescript +const invokeNew = retryWithAsync(invoke, 2, { + retryInterval: 2000, // 执行失败后,2s 后继续重试 +}); +``` + +:::tip + +同步方法 `retryWith` 没有该属性。 + +::: + + + +## 抛出的错误 + +默认情况下,如果超过重试次数,则会抛出 `MidwayRetryExceededMaxTimesError` 异常。 + +`MidwayRetryExceededMaxTimesError` 是框架默认的异常,可以进行错误过滤器的捕获梳理,或者从其属性中拿到原始的异常进行处理。 + +```typescript +import { retryWithAsync, MidwayRetryExceededMaxTimesError } from '@midwayjs/core'; + +async function invoke(id) { + // ... +} + +async function someServiceMethod() { + // 默认调用,加上重试两次,最多执行三次 + const invokeNew = retryWithAsync(invoke, 2); + + try { + return await invokeNew(1); + } catch(err) { + // err.name === 'MidwayRetryExceededMaxTimesError' + // err.cause instanceof CustomError => true + } + +} + +async invokeNew() { + throw new CustomError('customError'); +} +``` + +如果希望直接抛出原始的 error 对象,可以通过配置参数。 + +比如: + +```typescript +const invokeNew = retryWithAsync(invoke, 2, { + throwOriginError: true, +}); +``` + diff --git a/site/versioned_docs/version-3.0.0/router_table.md b/site/versioned_docs/version-3.0.0/router_table.md new file mode 100644 index 000000000000..8c344ab26322 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/router_table.md @@ -0,0 +1,551 @@ +# Web 路由表 + +从 v2.8.0 开始,Midway 提供了内置的路由表能力,所有的 Web 框架都将使用这份路由表注册路由。 + +从 v3.4.0 开始,路由服务将作为 Midway 内置服务提供。 + + +在应用启动,onReady 生命周期以及之后可用。 + + + +## 获取路由表服务 + +已默认实例化,可以直接注入使用。 + +```typescript +import { Configuration, Inject, MidwayWebRouterService, MidwayServerlessFunctionService } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + webRouterService: MidwayWebRouterService; + + @Inject() + serverlessFunctionService: MidwayServerlessFunctionService; + + async onReady() { + // Web 路由 + const routes = await this.webRouterService.getFlattenRouterTable(); + + // serverless 函数 + const routes = await this.serverlessFunctionService.getFunctionList(); + } +} +``` + +`MidwayServerlessFunctionService` 仅在 Serverless 场景下生效,方法和 `MidwayWebRouterService` 几乎相同。 + + + +## 路由信息定义 + + +每个路由信息由一个 `RouterInfo` 定义表示,包含一些属性。 + + +定义如下: +```typescript +export interface RouterInfo { + /** + * router prefix + */ + prefix: string; + /** + * router alias name + */ + routerName: string; + /** + * router path, without prefix + */ + url: string | RegExp; + /** + * request method for http, like get/post/delete + */ + requestMethod: string; + /** + * invoke function method + */ + method: string; + description: string; + summary: string; + /** + * router handler function key,for IoC container load + */ + handlerName: string; + /** + * serverless func load key + */ + funcHandlerName: string; + /** + * controller provideId + */ + controllerId: string; + /** + * router middleware + */ + middleware: any[]; + /** + * controller middleware in this router + */ + controllerMiddleware: any[]; + /** + * request args metadata + */ + requestMetadata: any[]; + /** + * response data metadata + */ + responseMetadata: any[]; +} +``` +| 属性 | 类型 | 描述 | +| --- | --- | --- | +| prefix | string | 路由前缀,比如 / 或者 /api,用户写在 @Controller 装饰器上的部分 | +| routerName | string | 路由名 | +| url | string | 路由的去除路由前缀的部分,也是用户写在 @Get 等装饰器上的部分 | +| requestMethod | string | get/post/delete/put/all 等 | +| method | string | 实际调用的类上的方法名 | +| description | string | 描述,路由装饰器上的参数 | +| summary | string | 摘要,路由装饰器上的参数 | +| handlerName | string | 等价于 controllerId.method | +| funcHandlerName | string | 使用 @Func 写的 handler 名字 | +| controllerId | string | controller 的依赖注入容器的 key(providerId) | +| middleware | string[] | 路由中间件字符串数组 | +| controllerMiddleware | string[] | 控制器中间件字符串数组 | +| requestMetadata | any[] | 请求参数的元数据,@Query/@Body 等元数据 | +| responseMetadata | any[] | 响应参数的元数据,@SetHeader/@ContentType 等元数据 | + + + +## 路由优先级 + + +以往我们需要关心路由的加载顺序,比如通配的 `/*` 比如在实际的 `/abc` 之后,否则会加载到错误的路由。在新版本中,我们对此种情况做了自动排序。 + + +规则如下: + + +- 1. 绝对路径规则优先级最高如 `/ab/cb/e` +- 2. 星号只能出现最后且必须在/后面,如 `/ab/cb/**` +- 3. 如果绝对路径和通配都能匹配一个路径时,绝对规则优先级高 +- 4. 有多个通配能匹配一个路径时,最长的规则匹配,如 `/ab/**` 和 `/ab/cd/**` 在匹配 `/ab/cd/f` 时命中 `/ab/cd/**` +- 5. 如果 `/` 与 `/*` 都能匹配 / ,但 `/` 的优先级高于 `/*` + + +此规则也与 Serverless 下函数的路由规则保持一致。 + + +简单理解为,“明确的路由优先级最高,长的路由优先级高,通配的优先级最低”。 + + +比如,排序完的优先级如下(高到低): +```typescript +/api/invoke/abc +/api/invoke/* +/api/abc +/api/* +/abc +/* +``` + + + +## 当前匹配的路由 + +通过 `getMatchedRouterInfo` 方法,我们可以知道当前的路由,匹配到哪个路由信息(RouterInfo),从而进一步处理,这个逻辑在鉴权等场景很有用。 + +比如,在中间件中,我们可以在进入控制器之前提前判断。 + +```typescript +import { Middleware, Inject, httpError, MidwayWebRouterService } from '@midwayjs/core'; + +@Middleware() +export class AuthMiddleware { + @Inject() + webRouterService: MidwayWebRouterService; + + resolve() { + return async (ctx, next) => { + // 查询当前路由是否在路由表中注册 + const routeInfo = await this.webRouterService.getMatchedRouterInfo(ctx.path, ctx.method); + if (routeInfo) { + await next(); + } else { + throw new httpError.NotFoundError(); + } + } + } +} +``` + + + +## 路由信息 + + +### 获取扁平化路由列表 + + +获取当前所有可注册到 HTTP 服务的路由列表(包括 @Func/@Controller,以及一切按照标准信息注册的自定义装饰器)。 + + +会按照优先级从高到低自动排序。 + + +定义: +```typescript +async getFlattenRouterTable(): Promise +``` + + +获取路由表 API。 +```typescript +const result = await this.webRouterService.getFlattenRouterTable(); +``` +输出示例: +```typescript +[ + { + "prefix": "/", + "routerName": "", + "url": "/set_header", + "requestMethod": "get", + "method": "homeSet", + "description": "", + "summary": "", + "handlerName": "apiController.homeSet", + "funcHandlerName": "apiController.homeSet", + "controllerId": "apiController", + "middleware": [], + "controllerMiddleware": [], + "requestMetadata": [], + "responseMetadata": [ + { + "type": "web:response_header", + "setHeaders": { + "ccc": "ddd" + } + }, + { + "type": "web:response_header", + "setHeaders": { + "bbb": "aaa" + } + } + ], + }, + { + "prefix": "/", + "routerName": "", + "url": "/ctx-body", + "requestMethod": "get", + "method": "getCtxBody", + "description": "", + "summary": "", + "handlerName": "apiController.getCtxBody", + "funcHandlerName": "apiController.getCtxBody", + "controllerId": "apiController", + "middleware": [], + "controllerMiddleware": [], + "requestMetadata": [], + "responseMetadata": [], + }, + // ... +] +``` + + +### 获取 Router 信息列表 + + +在 Midway 中,每个 Controller 对应一个 Router 对象,每个 Router 都会有一个路由前缀(prefix),在此之中的所有路由都会按照上面的规则进行排序。 + + +Router 本身也会按照 prefix 进行排序。 + + +定义: +```typescript +export interface RouterPriority { + prefix: string; + priority: number; + middleware: any[]; + routerOptions: any; + controllerId: string; +} + +async getRoutePriorityList(): Promise +``` +Router 的数据相对简单。 + + + +| 属性 | 类型 | 描述 | +| --- | --- | --- | +| prefix | string | 路由前缀,比如 / 或者 /api,用户写在 @Controller 装饰器上的部分 | +| priority | number | Router 的优先级,@Priority 装饰器填写的值,/ 根 Router 默认优先级最低,为 -999 | +| middleware | string[] | 控制器中间件字符串数组 | +| controllerId | string | controller 的依赖注入容器的 key(providerId) | +| routerOptions | any | @Controller 装饰器的 options | + + + +获取路由表 API。 + +```typescript +const list = await collector.getRoutePriorityList(); +``` +输出示例: +```typescript +[ + { + "prefix": "/case", + "priority": 0, + "middleware": [], + "routerOptions": { + "middleware": [], + "sensitive": true + }, + "controllerId": "caseController" + }, + { + "prefix": "/user", + "priority": 0, + "middleware": [], + "routerOptions": { + "middleware": [], + "sensitive": true + }, + "controllerId": "userController" + }, + { + "prefix": "/", + "priority": -999, + "middleware": [], + "routerOptions": { + "middleware": [], + "sensitive": true + }, + "controllerId": "apiController" + } +] +``` + + +### 获取带层级的路由 + + +某些情况下,我们需要拿到带层级的路由,包括哪些路由在哪个控制器(Controller)下,这样能更好的创建路由。 + + +Midway 也提供了获取带层级的路由表方法。层级内会按照优先级从高到低自动排序。 + + +定义: +```typescript +async getRouterTable(): Promise> +``` + + +获取层级路由表 API,返回的是个 Map,key 为控制器的路由前缀 prefix 字符串。未明确写明路由前缀的(比如函数或者其他场景),都将归为 / 路由前缀下。 +```typescript +const result = await collector.getRouterTable(); +``` +输出示例: +```typescript +Map(3) { + '/' => [ + { + prefix: '/', + routerName: '', + url: '/set_header', + requestMethod: 'get', + method: 'homeSet', + description: '', + summary: '', + handlerName: 'apiController.homeSet', + funcHandlerName: 'apiController.homeSet', + controllerId: 'apiController', + middleware: [], + controllerMiddleware: [], + requestMetadata: [], + responseMetadata: [Array], + }, + { + prefix: '/', + routerName: '', + url: '/ctx-body', + requestMethod: 'get', + method: 'getCtxBody', + description: '', + summary: '', + handlerName: 'apiController.getCtxBody', + funcHandlerName: 'apiController.getCtxBody', + controllerId: 'apiController', + middleware: [], + controllerMiddleware: [], + requestMetadata: [], + responseMetadata: [], + }, + // ... + ] +} +``` + + + +### 获取所有函数信息 + +和 `getFlattenRouterTable` 相同,只是返回的内容多了函数部分的信息。 + +定义: + +```typescript +async getFunctionList(): Promise +``` + + +获取函数路由表 API。 + +```typescript +const result = await this.serverlessFunctionService.getFunctionList(); +``` + + + + + +## 添加路由 + +### 动态添加 Web 控制器 + +有些时候我们希望根据某些条件动态的添加一个控制器,就可以使用这个方法。 + +首先,我们需要有一个控制器类,但是不使用 `@Controller` 装饰器修饰。 + +```typescript +import { Get, Provide } from '@midwayjs/core'; + +// 注意这里未使用 @Controller 修饰 +@Provide() +export class DataController { + @Get('/query_data') + async getData() { + return 'hello world'; + } +} +``` + +我们可以通过 `addController` 方法动态添加它。 + +```typescript +// src/configuration.ts +import { Configuration, Inject, MidwayWebRouterService } from '@midwayjs/core'; +import { DataController } from './controller/data.controller'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + webRouterService: MidwayWebRouterService; + + async onReady() { + if (process.env.NODE_ENV === 'test') { + this.webRouterService.addController(DataController, { + prefix: '/test', + routerOptions: { + middleware: [ + // ... + ] + } + }); + } + // ... + } +} +``` + +`addController` 的方法,第一个参数为类本身,第二个参数和 `@Controller` 装饰器参数相同。 + + + +### 动态添加 Web 路由函数 + +在某些场景下,用户可以直接动态添加方法。 + +```typescript +// src/configuration.ts +import { Configuration, Inject, MidwayWebRouterService } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + webRouterService: MidwayWebRouterService; + + async onReady() { + // koa/egg 格式 + this.webRouterService.addRouter(async (ctx) => { + return 'hello world'; + }, { + url: '/api/user', + requestMethod: 'GET', + }); + // ... + + // express 格式 + this.webRouterService.addRouter(async (req, res) => { + return 'hello world'; + }, { + url: '/api/user', + requestMethod: 'GET', + }); + } +} +``` + +`addRouter` 的方法,第一个参数为路由方法体,第二个参数为路由的元数据。 + + + +### 动态添加 Serverless 函数 + +和添加动态 Web 路由类似,使用内置的 `MidwayServerlessFunctionService` 服务来添加。 + +比如,添加一个 http 函数。 + +```typescript +// src/configuration.ts +import { Configuration, Inject, MidwayServerlessFunctionService } from '@midwayjs/core'; + +@Configuration({ + // ... +}) +export class MainConfiguration { + @Inject() + serverlessFunctionService: MidwayServerlessFunctionService; + + async onReady() { + this.serverlessFunctionService.addServerlessFunction(async (ctx, event) => { + return 'hello world'; + }, { + type: ServerlessTriggerType.HTTP, + metadata: { + method: 'get', + path: '/api/hello' + }, + functionName: 'hello', + handlerName: 'index.hello', + }); + } +} +``` + +`metadata` 的信息和 @ServerlessTrigger 的参数相同。 + diff --git a/site/versioned_docs/version-3.0.0/serverless/aliyun_faas.md b/site/versioned_docs/version-3.0.0/serverless/aliyun_faas.md new file mode 100644 index 000000000000..8e6481041542 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/serverless/aliyun_faas.md @@ -0,0 +1,726 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 部署到阿里云函数计算 + +阿里云 Serverless 是国内最早提供 Serverless 计算服务的团队之一, 依托于阿里云强大的云基础设施服务能力,不断实现技术突破。目前,淘宝、支付宝、钉钉、高德等已经将 Serverless 应用于生产业务,云上的 Serverless 产品在南瓜电影、网易云音乐、爱奇艺体育、莉莉丝等数万家企业成功落地。 + +阿里云 Serverless 包含许多产品,如函数计算 FC,轻量应用引擎 SAE 等,本文主要使用了其 **函数计算** 部分。 + +下面是常见的函数触发器的使用、测试和部署方法。 + + + +## 部署类型 + +阿里云的函数计算部署类型比较多,根据运行的不同容器有以下几种。 + +| 名称 | 能力限制 | 描述 | 部署媒介 | +| ------------------------------ | ------------------------------------------------ | ------------------------------------------------------------ | --------------- | +| 内置运行时 | 不支持流式请求和响应;不支持太大的请求和响应入参 | 只能部署函数接口,不需要自定义端口,构建出 zip 包给平台部署 | zip 包部署 | +| 自定义运行时(Custom Runtime) | | 可以部署标准应用,启动 9000 端口,使用平台提供的系统镜像,构建出 zip 包给平台部署 | zip 包部署 | +| 自定义容器(Custom Container) | | 可以部署标准应用,启动 9000 端口,自己控制所有环境依赖,构建出 Dockerfile 提供给平台部署 | Dockerfile 部署 | + +在平台上分别对应创建函数时的三种方式。 + +![](https://img.alicdn.com/imgextra/i1/O1CN01JrlhOw1EJBZmHklbq_!!6000000000330-2-tps-1207-585.png) + + + +## 纯函数开发(内置运行时) + +下面我们将以使用 **"内置运行时部署"** 纯函数作为示例。 + + + +### 触发器代码 + + + + +发布不包含触发器的函数,这是最简单的类型,可以直接通过 event 手动触发参数,也可以在平台绑定其他触发器。 + +通过直接在代码中的 `@ServerlessTrigger` 装饰器绑定事件触发器。 + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.EVENT) + async handleEvent(event: any) { + return event; + } +} +``` + + + + + +阿里云的 HTTP 触发器和其他平台的有所区别,是独立于 API 网关的另一套服务于 HTTP 场景的触发器。相比于 API 网关,该触发器更易于使用和配置。 + +通过直接在代码中的 `@ServerlessTrigger` 装饰器绑定 HTTP 触发器。 + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.HTTP, { + path: '/', + method: 'get', + }) + async handleHTTPEvent(@Query() name = 'midway') { + return `hello ${name}`; + } +} +``` + + + + + +API 网关在阿里云函数体系中比较特殊,他类似于创建一个无触发器函数,通过平台网关的绑定到特定的路径上。 + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.API_GATEWAY, { + path: '/api_gateway_aliyun', + method: 'post', + }) + async handleAPIGatewayEvent(@Body() name) { + return `hello ${name}`; + } +} +``` + + + + + +定时任务触发器用于定时执行一个函数。 + +:::info +温馨提醒,测试函数后请及时关闭触发器自动执行,避免超额扣费。 +::: + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; +import type { TimerEvent } from '@midwayjs/fc-starter'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.TIMER) + async handleTimerEvent(event: TimerEvent) { + this.ctx.logger.info(event); + return 'hello world'; + } +} +``` + +**事件结构** + +Timer 消息返回的结构如下,在 `TimerEvent` 类型中有描述。 + +```json +{ + triggerTime: new Date().toJSON(), + triggerName: 'timer', + payload: '', +} +``` + + + + + +OSS 用于存储一些资源文件,是阿里云的资源存储产品。 当 OSS 中有文件创建,更新,对应的函数就会被触发而执行。 + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; +import type { OSSEvent } from '@midwayjs/fc-starter'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.OS) + async handleOSSEvent(event: OSSEvent) { + // xxx + } +} +``` + + + +**事件结构** + +OSS 消息返回的结构如下,在 `FC.OSSEvent` 类型中有描述。 + +```json +{ + "events": [ + { + "eventName": "ObjectCreated:PutObject", + "eventSource": "acs:oss", + "eventTime": "2017-04-21T12:46:37.000Z", + "eventVersion": "1.0", + "oss": { + "bucket": { + "arn": "acs:oss:cn-shanghai:123456789:bucketname", + "name": "testbucket", + "ownerIdentity": "123456789", + "virtualBucket": "" + }, + "object": { + "deltaSize": 122539, + "eTag": "688A7BF4F233DC9C88A80BF985AB7329", + "key": "image/a.jpg", + "size": 122539 + }, + "ossSchemaVersion": "1.0", + "ruleId": "9adac8e253828f4f7c0466d941fa3db81161e853" + }, + "region": "cn-shanghai", + "requestParameters": { + "sourceIPAddress": "140.205.128.221" + }, + "responseElements": { + "requestId": "58F9FF2D3DF792092E12044C" + }, + "userIdentity": { + "principalId": "123456789" + } + } + ] +} +``` + + + + + +:::info + +* 1、阿里云消息队列会对 Topic 和 Queue 产生一定的费用。 +* 2、提供的默认消息队列格式为 JSON + +::: + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; +import type { MNSEvent } from '@midwayjs/fc-starter'; + +@Provide() +export class HelloAliyunService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.MQ) + async handleMNSEvent(event: MNSEvent) { + // ... + } +} +``` + + + +**事件结构** + +MNS 消息返回的结构如下,在 `FC.MNSEvent` 类型中有描述。 + +```json +{ + "Context": "user custom info", + "TopicOwner": "1186202104331798", + "Message": "hello topic", + "Subscriber": "1186202104331798", + "PublishTime": 1550216302888, + "SubscriptionName": "test-fc-subscibe", + "MessageMD5": "BA4BA9B48AC81F0F9C66F6C909C39DBB", + "TopicName": "test-topic", + "MessageId": "2F5B3C281B283D4EAC694B7425288675" +} +``` + + + + + +:::info + +触发器的更多配置由于和平台相关,将写在 `s.yaml` 中,如定时任务的时间间隔等,更多细节请查看下面的部署段落。 + +::: + + + +### 类型定义 + +FC 的定义将由适配器导出,为了让 `ctx.originContext` 的定义保持正确,需要将其添加到 `src/interface.ts` 中。 + +```typescript +// src/interface.ts +import type {} from '@midwayjs/fc-starter'; +``` + +此外,还提供了各种 Event 类型的定义。 + +```typescript +// Event 类型 +import type { + OSSEvent, + MNSEvent, + SLSEvent, + CDNEvent, + TimerEvent, + APIGatewayEvent, + TableStoreEvent, +} from '@midwayjs/fc-starter'; +// InitializeContext 类型 +import type { InitializeContext } from '@midwayjs/fc-starter'; +``` + + + +### 本地开发 + +HTTP 触发器和 API Gateway 类型可以通过本地 `npm run dev` 和传统应用类似的开发方式进行本地开发,其他类型的触发器本地无法使用 dev 开发,只能通过运行 `npm run test` 进行测试执行。 + + + +### 本地测试 + +和传统应用测试类似,使用 `createFunctionApp` 方法创建函数 app, 使用 `close` 方法关闭。 + +```typescript +import { Application, Context, Framework } from '@midwayjs/faas'; +import { mockContext } from '@midwayjs/fc-starter'; +import { createFunctionApp } from '@midwayjs/mock'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from event trigger', async () => { + + // create app + const app: Application = await createFunctionApp(join(__dirname, '../'), { + initContext: mockContext(), + }); + + // ... + + await close(app); + }); +}); +``` + +`mockContext` 方法用来模拟一个 FC Context 数据结构,可以自定传递一个类似的结构或者修改部分数据。 + +```typescript +import { Application, Context, Framework } from '@midwayjs/faas'; +import { mockContext } from '@midwayjs/fc-starter'; +import { createFunctionApp } from '@midwayjs/mock'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from event trigger', async () => { + + // create app + const app: Application = await createFunctionApp(join(__dirname, '../'), { + initContext: Object.assign(mockContext(), { + function: { + name: '***', + handler: '***' + } + }), + }); + + // ... + + await close(app); + }); +}); +``` + +不同的触发器有着不同的测试方法,下面列出了一些常见的触发器。 + + + + +通过 `getServerlessInstance` 获取类实例,直接调用实例方法,传入参数进行测试。 + +```typescript +import { HelloAliyunService } from '../src/function/hello_aliyun'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from event trigger', async () => { + // ... + const instance = await app.getServerlessInstance(HelloAliyunService); + expect(await instance.handleEvent('hello world')).toEqual('hello world'); + // ... + }); +}); +``` + + + + + +和应用类似相同,通过 `createFunctionApp` 创建函数 app,通过 `createHttpRequest` 方式进行测试。 + +```typescript +import { HelloAliyunService } from '../src/function/hello_aliyun'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from http trigger', async () => { + // ... + const result = await createHttpRequest(app).get('/').query({ + name: 'zhangting', + }); + expect(result.text).toEqual('hello zhangting'); + // ... + }); +}); +``` + + + + + +和 HTTP 测试相同,通过 `createFunctionApp` 创建函数 app,通过 `createHttpRequest` 方式进行测试。 + +```typescript +import { createHttpRequest } from '@midwayjs/mock'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from http trigger', async () => { + // ... + const result = await createHttpRequest(app).post('api_gateway_aliyun').send({ + name: 'zhangting', + }); + + expect(result.text).toEqual('hello zhangting'); + // ... + }); +}); +``` + + + + + +和 HTTP 测试不同,通过 `createFunctionApp` 创建函数 app,通过 `getServerlessInstance` 获取整个类的实例,从而调用到特定方法来测试。 + +可以通过 `mockTimerEvent` 方法快速创建平台传入的结构。 + +```typescript +import { HelloAliyunService } from '../src/function/hello_aliyun'; +import { mockTimerEvent } from '@midwayjs/fc-starter'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from timer trigger', async () => { + // ... + const instance = await app.getServerlessInstance(HelloAliyunService); + expect(await instance.handleTimerEvent(mockTimerEvent())).toEqual('hello world'); + // ... + }); +}); +``` + + + + + +和 HTTP 测试不同,通过 `createFunctionApp` 创建函数 app,通过 `getServerlessInstance` 获取整个类的实例,从而调用到特定方法来测试。 + +可以通过 `createOSSEvent` 方法快速创建平台传入的结构。 + +```typescript +import { HelloAliyunService } from '../src/function/hello_aliyun'; +import { mockOSSEvent } from '@midwayjs/fc-starter'; + +describe('test/hello_aliyun.test.ts', () => { + it('should get result from oss trigger', async () => { + // ... + const instance = await app.getServerlessInstance(HelloAliyunService); + expect(await instance.handleOSSEvent(mockOSSEvent())).toEqual('hello world'); + // ... + }); +}); +``` + + + + + +和 HTTP 测试不同,通过 `createFunctionApp` 创建函数 app,通过 `getServerlessInstance` 获取整个类的实例,从而调用到特定方法来测试。 + +可以通过 `createMNSEvent` 方法快速创建平台传入的结构。 + +```typescript +import { HelloAliyunService } from '../src/function/hello_aliyun'; +import { mockMNSEvent } from '@midwayjs/fc-starter'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from oss trigger', async () => { + // ... + const instance = await app.getServerlessInstance(HelloAliyunService); + expect(await instance.handleMNSEvent(mockMNSEvent())).toEqual('hello world'); + // ... + }); +}); +``` + + + + + +## 纯函数部署(内置运行时) + +以下将简述如何使用 Serverless Devs 部署到阿里云函数。 + +### 1、确认启动器 + +在项目根目录的 `f.yml` 的 `provider` 段落处确保 starter 为 `@midwayjs/fc-starter`。 + +```yaml +provider: + name: aliyun + starter: '@midwayjs/fc-starter' +``` + + + +### 2、安装 Serverless Devs 工具 + +aliyun 使用 [Serverless Devs 工具](https://www.serverless-devs.com/) 进行函数部署。 + +你可以将其安装到全局。 + +```bash +$ npm install @serverless-devs/s -g +``` + +参考 [密钥配置](https://docs.serverless-devs.com/serverless-devs/quick_start) 文档进行配置。 + + + +### 3、编写一个 Serverless Devs 描述文件 + +在根目录创建一个 `s.yaml` ,添加以下内容。 + +```yaml +edition: 1.0.0 +name: "midwayApp" # 项目名称 +access: "default" # 秘钥别名 + +vars: + service: + name: fc-build-demo + description: 'demo for fc-deploy component' +services: + project-0981cd9b07: + component: devsapp/fc + props: + region: cn-hangzhou + service: ${vars.service} + function: + name: hello # 函数名 + handler: helloHttpService.handleHTTPEvent + codeUri: '.' + initializer: helloHttpService.initializer + customDomains: + - domainName: auto + protocol: HTTP + routeConfigs: + - path: /* + serviceName: ${vars.service.name} + functionName: helloHttpService-handleHTTPEvent + triggers: + - name: http + type: http + config: + methods: + - GET + authType: anonymous + +``` + +每加一个函数都需要调整 `s.yaml` 文件,为此Midway 提供了一个 `@midwayjs/serverless-yaml-generator` 工具用来将装饰器函数信息写入 `s.yaml`。 + +```diff +{ + "scripts": { ++ "generate": "serverless-yaml-generator", + }, + "devDependencies": { ++ "@midwayjs/serverless-yaml-generator": "^1.0.0", + }, +} +``` + +通过执行下面的命令,可以将现有函数信息填充到 `s.yaml` 中,并生成入口文件,方便排查问题。 + +```bash +$ npm run generate +``` + +工具将以函数名作为 key 在 `s.yaml` 中查找配置。 + +* 1、如果存在函数,则会覆盖特定字段,比如 handler,http 触发器的 methods +* 2、如果不存在函数,则会添加一个新函数 +* 3、工具不会写入 http 的路由方法,为了简化后续更新,可以提供一个 `/*` 路由(如示例) + +我们推荐用户只在装饰器定义基础函数名,函数 handler,以及基础触发器信息(比如 http 触发器的 path 和 method),其余都写在 `yaml` 中。 + +`s.yaml` 的完整配置较为复杂,具体请参考 [描述文件规范](https://docs.serverless-devs.com/serverless-devs/yaml)。 + + + +### 4、编写一个部署脚本 + +由于部署有构建,拷贝等多个步骤,我们可以编写部署脚本统一这个过程。 + +比如在项目根目录新建一个 `deploy.sh` 文件,内容如下。 + +```bash +#!/bin/bash + +set -e + +# 构建产物目录 +export BUILD_DIST=$PWD/.serverless +# 构建开始时间,单位毫秒 +export BUILD_START_TIME=$(date +%s%3N) + +echo "Building Midway Serverless Application" + +# 打印当前目录 cwd +echo "Current Working Directory: $PWD" +# 打印结果目录 BUILD_DIST +echo "Build Directory: $BUILD_DIST" + +# 安装当前项目依赖 +npm i + +# 执行构建 +./node_modules/.bin/tsc || return 1 +# 生成入口文件 +./node_modules/.bin/serverless-yaml-generator || return 1 + +# 如果 .serverless 文件夹存在,则删除后重新创建 +if [ -d "$BUILD_DIST" ]; then + rm -rf $BUILD_DIST +fi + +mkdir $BUILD_DIST + +# 拷贝 dist、 *.json、*.yml 到 .serverless 目录 +cp -r dist $BUILD_DIST +cp *.yaml $BUILD_DIST 2>/dev/null || : +cp *.json $BUILD_DIST 2>/dev/null || : +# 移动入口文件到 .serverless 目录 +mv *.js $BUILD_DIST 2>/dev/null || : + +# 进入 .serverless 目录 +cd $BUILD_DIST +# 安装线上依赖 +npm install --production + +echo "Build success" + +# 在 .serverless 目录进行部署 +s deploy + +``` + +可以将这个 `deploy.sh` 文件放到 `package.json` 的 `deploy` 指令中,后续部署执行 `npm run deploy` 即可。 + +```json +{ + "scripts": { + "deploy": "sh deploy.sh" + } +} +``` + +:::tip + +* 1、 `deploy.sh` 只测试了 mac,其余平台可以自行调整 +* 2、脚本内容可以根据业务逻辑自行调整,比如拷贝的文件等 + +::: + + + +## 自定义运行时部署 + +### 1、创建项目 + +自定义运行时可以使用标准项目来部署,由于需要提供 9000 端口,需要创建 Midway koa/express/express 项目。 + +初始化项目请参考 [创建第一个应用](/docs/quickstart)。 + +### 2、调整端口 + +为了避免影响本地开发,我们仅在入口 `bootstrap.js` 处增加端口。 + +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); + +// 显式以组件方式引入用户代码 +Bootstrap.configure({ + globalConfig: { + koa: { + port: 9000, + } + } +}).run() +``` + +不同的框架修改端口请参考: + +* [koa 修改端口](/docs/extensions/koa) +* [Egg 修改端口](/docs/extensions/egg) +* [Express 修改端口](/docs/extensions/express) + + + +### 3、平台部署配置 + +* 1、选择运行环境,比如 `Node.js 18` +* 2、选择代码上传方式,比如可以本地打 zip 包上传 +* 3、启动命令指定 node bootstrap.js +* 4、监听端口 9000 + +![](https://img.alicdn.com/imgextra/i3/O1CN010JA2GU1lxNeqm81AR_!!6000000004885-2-tps-790-549.png) + +配置完成之后,上传压缩包即可部署完成。 diff --git a/site/versioned_docs/version-3.0.0/serverless/aws_lambda.md b/site/versioned_docs/version-3.0.0/serverless/aws_lambda.md new file mode 100644 index 000000000000..92bfd6b1c1ca --- /dev/null +++ b/site/versioned_docs/version-3.0.0/serverless/aws_lambda.md @@ -0,0 +1,82 @@ +# 部署到 AWS Lambda + +AWS Lambda是Amazon Web Services (AWS)提供的无服务器计算服务。它允许您在无需预配或管理服务器的情况下运行代码。您可以为几乎任何类型的应用程序或后端服务运行代码,全部无需管理。 + +下面我们将介绍如何将 Midway 标准应用部署到 AWS Lambda。 + + + +### 1、创建项目 + +需要创建 Midway koa/express/express 项目。 + +初始化项目请参考 [创建第一个应用](/docs/quickstart),下面以 koa 应用为例。 + + + +### 2、调整端口 + +为了避免影响本地开发,我们仅在入口 `bootstrap.js` 处增加端口,比如 `8080`。 + +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); + +// 显式以组件方式引入用户代码 +Bootstrap.configure({ + globalConfig: { + koa: { + port: 8080, + } + } +}).run() +``` + +不同的框架修改端口请参考: + +* [koa 修改端口](/docs/extensions/koa) +* [Egg 修改端口](/docs/extensions/egg) +* [Express 修改端口](/docs/extensions/express) + + + +### 3、安装和配置 AWS 工具 + +- [安装 AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) +- [配置AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html) +- [安装 AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + + + +### 4、编写 template.yaml + +```yaml +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Resources: + EasySchoolBackendFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./dist.zip + Handler: dist/index/index.handler + Runtime: nodejs14.x + Timeout: 900 + PackageType: Zip + Events: + ApiEvent: + Type: Api + Properties: + Path: /{any+} + Method: ANY + +``` + + + +### 4、构建和部署 + +```bash +$ cd sam +$ sam build # builds sam +$ sam local start-api # start local api +``` + diff --git a/site/versioned_docs/version-3.0.0/serverless/serverless_context.md b/site/versioned_docs/version-3.0.0/serverless/serverless_context.md new file mode 100644 index 000000000000..213ce655ec12 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/serverless/serverless_context.md @@ -0,0 +1,212 @@ +# 函数上下文 + +## Event 转换 + +Midway Serverless 针对不同平台的情况,进行了入参包裹,同时,在函数使用了 apigw(API 网关)和 http (阿里云)触发器的情况下,对入参(event)做了特殊处理,为了简化和统一写法,将 event 统一规则化成了类似 koa 写法的代码。 + +普通触发器场景: + +```typescript +import { Context } from '@midwayjs/faas'; +import { Provide } from '@midwayjs/core'; + +@Provide() +export class Index { + + @Inject() + ctx: Context; + + @ServerlessTrigger(...) + async handler(event) { + return 'hello world'; + } +} +``` + +HTTP 、API 网关触发器场景: + +```typescript +import { Context } from '@midwayjs/faas'; +import { Provide } from '@midwayjs/core'; + +@Provide() +export class Index { + + @Inject() + ctx: Context; + + @ServerlessTrigger(...) + async handler() { + // 下面两种写法相同 + // this.ctx.body = 'hello world'; + return 'hello world'; + } +} +``` + +## Context + +每次函数调用,都会创建一个全新的 ctx(函数上下文)。针对 ctx 上的属性或者方法,我们提供 ts 定义。 + +:::info +在 Serverless v1 时代,我们的定义叫 FaaSContext,在 v2 我们将定义和应用做了统一,更为一致。 +::: + +### ctx.logger + +- return `ILogger` + +运行时传递下来的每次请求的日志对象,默认为 console。 + +```typescript +ctx.logger.info('hello'); +ctx.logger.warn('hello'); +ctx.logger.error('hello'); +``` + +### ctx.env + +- return `string` + +当前启动的环境,即 NODE_ENV 或者 MIDWAY_SERVER_ENV 的值,默认为 prod。 + +```typescript +ctx.env; // 默认 prod +``` + +### ctx.requestContext + +- return `MidwayRequestContainer` + +midway faas 的 IoC 请求作用域容器,用于获取其他 IoC 容器中的对象实例。 + +```typescript +const userService = await ctx.requestContext.getAsync(UserService); +``` + +## FaaSHTTPContext + +`Context` 定义继承于 `FaaSHTTPContext`,前者保留了后者,大部分场景下可以直接使用前者,后者是在 apigw(API 网关)和 http (阿里云)触发器下才有的能力。 + +对于普通用户,直接使用 `Context` 定义即可。 + +```typescript +import { Context } from '@midwayjs/faas'; + +@Inject() +ctx: Context; +``` + +在 ctx 对象中,我们提供了一些和编写传统 Koa Web 应用程序类似的 API。这样的好处是减少用户的认知成本,并且,在一定程度上,兼容原有传统代码,兼容社区 middleware 成为了可能。 + +我们提供了一些和传统类似的 API,支持常用的能力,**在不同的平台可能不一定完全相同**,我们会在特定 API 中指出。 + +### ctx.request + +- return `FaaSHTTPRequest` + +FaaS 模拟的 HTTP Request 对象。 + +### ctx.response + +- return `FaaSHTTPResponse` + +FaaS 模拟的 HTTP Response 对象。 + +### ctx.params + +代理自 `request.pathParameters`,在 http 触发器(阿里云)和 API 网关触发器下可用。 + +```typescript +// /api/user/[id] /api/user/faas +ctx.params.id; // faas +``` + +### ctx.set + +设置响应头,此方法代理自 `response.setHeader` 。 + +```typescript +ctx.set('X-FaaS-Duration', 2100); +``` + +### ctx.status + +设置返回状态码,此属性代理自 `response.statusCode` 。 + +```typescript +ctx.status = 404; +``` + + + +### Request aliases + +以下列出的属性是从 [Request](#k6AZp) 对象代理过来 + +- `ctx.headers` +- `ctx.method` +- `ctx.url` +- `ctx.path` +- `ctx.ip` +- `ctx.query` +- `ctx.get()` + +### Response aliases + +以下列出的属性是从 [Response](#kfTOD) 对象代理过来 + +- `ctx.body=` +- `ctx.status=`alias to `response.statusCode` +- `ctx.type=` +- `ctx.set()`alias to `response.setHeader` + + + +## FaaSHTTPRequest + +此对象是通过将函数的 `event` 和 `context` 入参进行转换得来。 + +### request.headers + +包含所有请求头的对象,键值对存储。 + +### request.ip + +获取客户端请求 ip。 + +:::info +在阿里云 FC 上,只有 HTTP 触发器能获取到值,api 网关暂时无法获取。 +::: + +### request.url + +客户端请求完整 url。 + +### request.path + +客户端请求 path。 + +### request.method + +请求的 method。 + +### request.body + +POST 请求的 body,已经解析为 JSON。 + +## FaaSHTTPResponse + +此对象是通过将函数的 `event` 和 `context` 入参进行转换得来。 + +### response.setHeader + +设置响应头。 + +### response.statusCode + +设置返回状态码。 + +### response.body + +设置返回响应体内容, `string` 或者 `buffer`。 diff --git a/site/versioned_docs/version-3.0.0/serverless/serverless_dev.md b/site/versioned_docs/version-3.0.0/serverless/serverless_dev.md new file mode 100644 index 000000000000..8f2df33bc3e2 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/serverless/serverless_dev.md @@ -0,0 +1,153 @@ +# 开发函数 + +## 初始化代码 + +让我们来开发第一个纯 HTTP 函数,来尝试将它部署到云环境。 + +执行 `npm init midway`,选择 `faas` 脚手架。 + + + +## 目录结构 + +以下就是一个函数的最精简的结构,核心会包括一个 `f.yml` 标准化函数文件,以及 TypeScript 的项目结构。 + +```bash +. +├── f.yml # 标准化 spec 文件 +├── package.json # 项目依赖 +├── src +│ └── function +│ └── hello.ts ## 函数文件 +└── tsconfig.json +``` + +我们来简单了解一下文件内容。 + +- `f.yml` 函数定义文件 +- `tsconfig.json` TypeScript 配置文件 +- `src` 函数源码目录 +- `src/function/hello.ts` 示例函数文件 + +我们将函数放在 `function`目录下,是为了更好的和其他类型的代码分开。 + +## 函数文件 + +我们首先来看看函数文件,传统的函数是一个 `function` ,为了更符合 midway 体系,以及使用我们的依赖注入,这里将它变成了 Class。 + +通过 `@ServerlessTrigger` 装饰器,我们将方法标注为一个 HTTP 接口,并且标示 `path` 和 `method` 属性。 + +```typescript +import { Provide, Inject, ServerlessTrigger, ServerlessTriggerType, Query } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloHTTPService { + @Inject() + ctx: Context; + + @ServerlessTrigger(ServerlessTriggerType.HTTP, { + path: '/', + method: 'get', + }) + async handleHTTPEvent(@Query() name = 'midway') { + return `hello ${name}`; + } +} +``` + +除了触发器外,我们还可以使用 `@ServerlessFunction` 装饰器描述函数层面的元信息,比如函数名,并发度等等。 + + +这样,当我们在一个函数上,使用多个触发器时,就可以这样设置。 + +```typescript +import { Provide, Inject, ServerlessFunction, ServerlessTrigger, ServerlessTriggerType } from '@midwayjs/core'; +import { Context } from '@midwayjs/faas'; + +@Provide() +export class HelloServerlessService { + @Inject() + ctx: Context; + + // 一个函数多个触发器 + @ServerlessFunction({ + functionName: 'abcde', + }) + @ServerlessTrigger(ServerlessTriggerType.TIMER, { + name: 'timer' + }) + async handleTimerEvent() { + // TODO + } +} +``` + +:::caution +注意,有些平台无法将不同类型的触发器放在同一个函数中,比如阿里云规定,HTTP 触发器和其他触发器不能同时在一个函数生效。 +::: + +## 函数定义文件 + +`f.yml` 是框架识别函数信息的文件,内容如下。 + +```yaml +provider: + name: aliyun # 发布的平台,这里是阿里云 + starter: '@midwayjs/fc-starter' + +``` + +这里的 `@midwayjs/fc-starter` 就是适配 aliyun 函数的适配器。 + + + +## 触发器装饰器参数 + +`@ServerlessTrigger` 装饰器用于定义不同的触发器,它的参数为每个触发器信息,以及通用触发器参数。 + +比如触发器的名称修改为 abc。 + +```typescript + @ServerlessTrigger(ServerlessTriggerType.TIMER, { + name: 'abc', // 触发器名称 + }) +``` + +如果只有一个触发器,可以将函数名信息写入到触发器上。 + +```typescript + @ServerlessTrigger(ServerlessTriggerType.TIMER, { + functionName: 'hello' // 如果只有一个触发器,可以省略一个装饰器 + name: 'abc', + }) +``` + + + +## 函数装饰器参数 + +`@ServerlessFunction` 装饰器用于定义函数,如果有多个触发器,通过它可以统一修改函数名。 + + +比如: + +```typescript +@ServerlessFunction({ + functionName: 'abcde' // 函数名称 +}) +``` + +## 本地开发 + +HTTP 函数本地开发和传统 Web 相同,输入以下命令。 + +```shell +$ npm run dev +$ open http://localhost:7001 +``` + +Midway 会启动 HTTP 服务器,打开浏览器,访问 [http://127.0.0.1:7001](http://127.0.0.1:7001) ,浏览器会打印出 `Hello midwayjs` 的信息。 + +非 HTTP 函数,无法直接触发,作为代替,可以编写测试函数执行。 + diff --git a/site/versioned_docs/version-3.0.0/serverless/serverless_error.md b/site/versioned_docs/version-3.0.0/serverless/serverless_error.md new file mode 100644 index 000000000000..7388e56e1d78 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/serverless/serverless_error.md @@ -0,0 +1,60 @@ +# 默认错误行为 + +## 错误值处理 + + + +为了保证安全性,Midway 针对 Serverless 场景下返回的错误做了一些特殊处理。 + + +在函数业务抛出错误的情况下,框架侧会捕获所有的错误,并返回 “Internal Server Error” 的错误。 + + +比如我们的函数返回一个错误: + +```typescript +@ServerlessTrigger(//...) +async invoke() { + throw new Error('abc'); +} +``` + + + +不管是 HTTP 还是非 HTTP 触发器,框架部分都有相应的处理。 + + +在 **非线上环境**,比如 `NODE_ENV=local` 环境,框架会将整个错误通过网关透出。 + + +比如(完整的错误堆栈): + +``` +2021-07-02T05:57:08.553Z 19be4d99-c9cb-4c4c-aac2-9330d31b4408 [error] Error: abc + at hello (/code/dist/function/index.js:17:15) + at invokeHandler (/code/node_modules/_@midwayjs_faas@2.11.2-beta.1@@midwayjs/faas/dist/framework.js:174:56) + at processTicksAndRejections (internal/process/task_queues.js:97:5) + at (/code/node_modules/_@midwayjs_faas@2.11.2-beta.1@@midwayjs/faas/dist/framework.js:117:40) + at cors (/code/node_modules/_@koa_cors@3.1.0@@koa/cors/index.js:98:16) + at invokeHandlerWrapper (/code/node_modules/_@midwayjs_runtime-engine@2.11.1@@midwayjs/runtime-engine/dist/lightRuntime.js:18:28) { +} +``` + + + +在 线上环境,框架将直接返回 **“Internal Server Error”** ,但是日志中是完整的堆栈。 + + +如图所示。 + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1625205528496-96f7d2b8-d728-4f04-82f4-f2617e00720b.png) + + + +## 调整错误返回 + +以上为默认行为,在特殊环境下,如果需要显示出错误,可以使用环境变量开启强制输出。 + +```typescript +process.env.SERVERLESS_OUTPUT_ERROR_STACK = 'true'; +``` diff --git a/site/versioned_docs/version-3.0.0/serverless/serverless_intro.md b/site/versioned_docs/version-3.0.0/serverless/serverless_intro.md new file mode 100644 index 000000000000..50e93a4be4e9 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/serverless/serverless_intro.md @@ -0,0 +1,68 @@ +# 介绍 + +## Midway Serverless 能做什么 + +Midway Serverless 是用于构建 Node.js 云函数的 Serverless 框架。帮助您在云原生时代大幅降低维护成本,更专注于产品研发。 + +## Midway Serverless 和 Midway 的关系 + +Midway Serverless 是 Midway 产出的一套面向 Serverless 云平台的开发方案。其内容主要包括函数框架 `@midwayjs/faas`,以及一系列跟平台配套的工具链,启动器等。 + +在 Midway Serverless 2.0 之后,Midway Serverless 和 Midway 的能力复用,有着相同的 CLI 工具链,编译器,装饰器等等。 + +当前,Midway Serverless 主要面向的是 函数(FaaS)场景。 + +## 函数(FaaS)能做什么 + +很多人对函数还不是很清楚或者不了解他能做什么。当前的函数,可以当做一个小容器,原来我们要写一个完整的应用来承载能力,现在只需要写中间的逻辑部分,以及考虑输入和输出的数据。 + +通过绑定平台的触发器,可以承载例如 HTTP,Socket 等流量。 + +通过平台提供的 BaaS SDK,可以对外调用数据库,Redis 等服务。 + +通过函数,能提供传统的 HTTP API 服务,结合现有的前端框架(react,vue 等)渲染出一个个美丽的页面,也可以做为一个独立的数据模块,等待被调用(触发),比如常见的文件上传变更,解压等等,也能作为定时任务的逻辑部分,到了指定的时间或者时间间隔被执行。 + +随着时间的更替,平台的迭代,函数的能力会越来越强,而用户的上手成本,服务器成本则会越来越低。 + +## 函数不能做什么 + +函数的架构决定了,有些需求是无法支持的,另外,函数和应用在能力上还是有一定的区别。 + +函数不适用: + +- 执行时间超过函数配置下限制的(最好不超过 5s) +- 有状态,在本地存储数据的 +- 长链接,比如 ws 等 +- 后台任务,有大数据执行的 +- 依赖多进程通信的 +- 大文件上传(比如网关限制的 2M 以上) +- 自定义环境的,比如 nginx 配置,c++ 库(c++ addon 动态链接库等),python 版本依赖的 +- 大量服务端缓存的 +- 固定 ip 的情况 + +## 术语描述 + +### 函数 + +逻辑意义上的一段代码片段,通过常见的入口文件包裹起来执行。函数是单一链路,并且无状态的,现在很多人认为,Serverless = FaaS + BaaS ,而 FaaS 则是无状态的函数,BaaS 解决带状态的服务。 + +### 函数组 + +多个函数聚合到一起的逻辑分组名,对应原有的应用概念。 + +### 触发器 + +触发器,也叫 Event(事件),Trigger 等,特指触发函数的方式。 +与传统的开发理念不同,函数不需要自己启动一个服务去监听数据,而是通过绑定一个(或者多个)触发器,数据是通过类似事件触发的机制来调用到函数。 + +### 函数运行时 + +英文叫 Runtime,具体指执行函数的环境,具体在各个平台可能是镜像,也可能是 Node.js 代码包,比如常见的社区运行时有 kubeless 等,该代码包会实现对接平台的各种接口,处理异常,转发日志等能力。 + +### 发布平台 + +函数最后承载的平台,现在社区最常见的有阿里云 FC 、腾讯云 SCF,AWS 的 Lambda 等等。 + +### Layer + +由于运行时的代码比较简单,且需要保证稳定性无法经常性的更新,Layer 被设计出来扩展运行时的能力,并且可以精简本地的函数代码量(有一些平台限制了上传压缩包的大小)。 diff --git a/site/versioned_docs/version-3.0.0/serverless/serverless_post_difference.md b/site/versioned_docs/version-3.0.0/serverless/serverless_post_difference.md new file mode 100644 index 000000000000..15e2dc2d4a7e --- /dev/null +++ b/site/versioned_docs/version-3.0.0/serverless/serverless_post_difference.md @@ -0,0 +1,246 @@ +# Serverless 触发器 POST 情况差异 + +## 阿里云 API 网关 + +阿里云 API 网关支持不同类型的的 POST 请求。 + +### 入参透传的 POST + +网关配置如下。 + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593175823751-f9b305fc-ddeb-4b04-ba13-481a616be260.png) + +网关透传的 event 特征为有 `body` 字段以及 `isBase64Encoded` 为 true,解码比较容易,直接解 base64 即可。 + +:::info +透传了之后,即为所有的结果交给函数处理。 +::: + +#### 示例一 (text/html) + +下面的 event,是一个最简单的透传示例,因为其中的 `content-type` 为 `text/html`,所以 body 传递过来 base64 解码的结果也同样是字符串。 + +```json +{ + "body": "eyJjIjoiYiJ9", + "headers": { + "x-ca-dashboard-action": "DEBUG", + "x-ca-dashboard-uid": "125087", + "x-ca-stage": "RELEASE", + "x-ca-dashboard-role": "USER", + "user-agent": "Apache-HttpClient/4.5.6 (Java/1.8.0_172)", + "accept-encoding": "gzip,deflate", + "content-md5": "Kry+hjKjc2lvIrwoJqdY9Q==", + "content-type": "text/html; charset=utf-8" + }, + "httpMethod": "POST", + "isBase64Encoded": true, + "path": "/api/321", + "pathParameters": { + "userId": "321" + }, + "queryParameters": {} +} +``` + +函数结果。 + +```typescript +ctx.request.body; // '{"c":"b"}' => string +``` + +#### 示例二(application/json) + +使用 `content-type` 为 `application/json` ,这样框架认为是一个 JSON,会自动被 JSON.parse。 + +```json +{ + "body": "eyJjIjoiYiJ9", + "headers": { + "X-Ca-Dashboard-Action": "DEBUG", + "X-Ca-Dashboard-Uid": "125087", + "X-Ca-Stage": "RELEASE", + "X-Ca-Dashboard-Role": "USER", + "User-Agent": "Apache-HttpClient/4.5.6 (Java/1.8.0_172)", + "Accept-Encoding": "gzip,deflate", + "Content-MD5": "Kry+hjKjc2lvIrwoJqdY9Q==", + "Content-Type": "application/json; charset=utf-8" + }, + "httpMethod": "POST", + "isBase64Encoded": true, + "path": "/api/321", + "pathParameters": { + "userId": "321" + }, + "queryParameters": {} +} +``` + +函数结果。 + +```typescript +ctx.request.body; // {"c":"b"} => object +``` + +#### 示例三 (application/x-www-form-urlencoded) + +使用 `content-type` 为 `application/x-www-form-urlencoded`,这个时候网关不会以 base64 格式透传,这也是前端原生表单的默认提交类型。 + +:::info +在 API 网关侧测试,保持“入参透传”下,似乎没有效果,于是我换到了 Postman 进行测试。 +::: + +Postman 模拟请求如下: + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593188653464-2a5659de-40ad-4611-ba86-f5754c7d4425.png) + +函数拿到的 event 值如下。 + +```json +{ + "body": "{\"c\":\"b\"}", + "headers": { + "accept": "*/*", + "cache-control": "no-cache", + "user-agent": "PostmanRuntime/7.24.1", + "postman-token": "feb51b11-9103-463a-92ff-73076d37b683", + "accept-encoding": "gzip, deflate, br", + "content-type": "application/x-www-form-urlencoded" + }, + "httpMethod": "POST", + "isBase64Encoded": false, + "path": "/api/321", + "pathParameters": { + "userId": "321" + }, + "queryParameters": {} +} +``` + +函数结果。 + +```typescript +ctx.request.body; // {"c":"b"} => object +``` + +### 入参映射的 POST + +网关配置选择入参映射之后,body 数据类型有两种选择。 + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593186831907-7975c65c-aee5-4f96-9ae4-ffaeee66c7dd.png) + +一旦选了映射,整个函数拿到的 Headers 中就 **没有了 content-type**。 + +这个时候,网关的返回 event 为 + +```json +{ + "body": "eyJjIjoiYiJ9", + "headers": { + "X-Ca-Dashboard-Action": "DEBUG", + "X-Ca-Dashboard-Uid": "111111", + "X-Ca-Dashboard-Role": "USER" + }, + "httpMethod": "POST", + "isBase64Encoded": true, + "path": "/api/321", + "pathParameters": { + "userId": "321" + }, + "queryParameters": {} +} +``` + +函数由于默认没有拿到 header 头,只会对 base64 的结果做处理,结果为字符串。 + +```typescript +ctx.request.body; // '{"c":"b"}' => string +``` + +## 阿里云 HTTP 触发器 + +函数提供的 HTTP 触发器(和网关不同)。 + +### 普通 POST(application/json) + +验证代码如下。 + +```typescript +const body = this.ctx.request.body; +return { + type: typeof body, + body, +}; +``` + +字符串格式。 + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593321679770-a7609684-ec5e-4f93-99f2-d346ed79c1fa.png) + +```typescript +ctx.request.body; // "bbb" => string +``` + +JSON 格式 + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593321730423-f9b2860f-7902-4f3a-81cf-bfbcfd4ee57f.png) + +```typescript +ctx.request.body; // {"b":"c"} => object +``` + +### 表单(application/x-www-form-urlencoded) + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593321823455-23ec3970-35a5-4746-8995-d9146eaa4ab0.png) + +```typescript +ctx.request.body; // {"b":"c"} => object +``` + +### 文件上传(Binary) + +暂未支持 + +## 腾讯云网关 + +腾讯云提供单独网关。 + +### 普通 POST(application/json) + +验证代码如下。 + +```typescript +const body = this.ctx.request.body; +return { + type: typeof body, + body, +}; +``` + +使用 Postman 请求。 + +字符串格式,正常解析。 + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593323223487-c4e5f365-b500-4a2d-85e3-45bd4aba4653.png) + +```typescript +ctx.request.body; // "bbb" => string +``` + +JSON 格式,能正常解析。 + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593323187488-e7b4e32e-4195-404d-b309-ba436c3f5f8e.png) + +```typescript +ctx.request.body; // {"c":"b"} => object +``` + +### 表单(application/x-www-form-urlencoded) + +正常解析为 JSON。 + +![](https://cdn.nlark.com/yuque/0/2020/png/501408/1593323279728-983fd844-f37d-419b-90f3-f96d1ee8236d.png) + +```typescript +ctx.request.body; // {"c":"b"} => object +``` diff --git a/site/versioned_docs/version-3.0.0/serverless/serverless_testing.md b/site/versioned_docs/version-3.0.0/serverless/serverless_testing.md new file mode 100644 index 000000000000..99e2876bebb2 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/serverless/serverless_testing.md @@ -0,0 +1,64 @@ +# 测试函数 + +## HTTP 类的函数 + +该方法适用于所有的类 HTTP 触发器的函数,包括 `HTTP` 和 `API_GATEWAY`。 + +使用和应用相同的测试方法来测试,针对 HTTP 函数,使用封装了 supertest 的 `createHttpRequest` 方法创建 HTTP 客户端。 + +唯一和应用不同的是,使用 `createFunctionApp` 方法创建函数应用(app)。 + +`createFunctionApp` 是 `createApp` 在函数场景下的定制方法。 + +HTTP 测试代码如下: + +```typescript +import { createFunctionApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework, Application } from '@midwayjs/faas'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from api gateway trigger', async () => { + + const app: Application = await createFunctionApp(); + + const result = await createHttpRequest(app).get('/').query({ + name: 'zhangting', + }); + expect(result.text).toEqual('hello zhangting'); + + await close(app); + + }); +}); +``` + + + +## 普通触发器 + +除了类 HTTP 触发器之外,我们还有其他比如定时器、对象存储等函数触发器,这些触发器由于和网关关系密切,不能使用 HTTP 行为来测试,而是使用传统的方法调用来做。 + +通过 `createFunctionApp` 方法创建函数 app,通过 `getServerlessInstance` 方法获取类实例,然后通过实例的方法直接调用,传入参数进行测试。 + +```typescript +import { createFunctionApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework, Application } from '@midwayjs/faas'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from event trigger', async () => { + // 创建函数 app + let app: Application = await createFunctionApp(); + + // 拿到服务类 + const instance = await app.getServerlessInstance(HelloAliyunService); + + // 调用函数方法,传入参数 + expect(await instance.handleEvent('hello world')).toEqual('hello world'); + + await close(app); + }); +}); +``` + diff --git a/site/versioned_docs/version-3.0.0/serverless/serverless_v2_upgrade_serverless_v3.md b/site/versioned_docs/version-3.0.0/serverless/serverless_v2_upgrade_serverless_v3.md new file mode 100644 index 000000000000..d5132cd281d2 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/serverless/serverless_v2_upgrade_serverless_v3.md @@ -0,0 +1,139 @@ +# 从 Serverless v2 迁移到 v3 + +基于 Midway 升级到 v3 的缘故,Serverless 体系也同步升级到了 v3 版本。 + +本文章介绍如何从 Serverless v2.0 迁移到 Serverless v3.0,和传统应用升级非常的类似。 + +:::caution + +新的 Serverless 当前只支持阿里云函数。 + +::: + + + +## 1、项目包版本的升级 + +一些依赖包升级,包括: + +* midway 及组件版本升级到 3.x +* CLI,Jest 等版本升级 +* 移除了一些不再使用的依赖,比如 `@midwayjs/serverless-app` + +```diff +"scripts": { + "dev": "cross-env NODE_ENV=local midway-bin dev --ts", + "test": "cross-env midway-bin test --ts", +- "deploy": "cross-env UDEV_NODE_ENV=production midway-bin deploy", + "lint": "mwts check", + "lint:fix": "mwts fix" +}, +"dependencies": { +- "@midwayjs/core": "^2.3.0", +- "@midwayjs/decorator": "^2.3.0", +- "@midwayjs/faas": "^2.0.0" ++ "@midwayjs/core": "^3.12.0", ++ "@midwayjs/faas": "^3.12.0", ++ "@midwayjs/fc-starter": "^3.12.0", ++ "@midwayjs/logger": "^2.0.0" +}, +"devDependencies": { +- "@midwayjs/cli": "^1.2.45", +- "@midwayjs/cli-plugin-faas": "^1.2.45", +- "@midwayjs/fcli-plugin-fc": "^1.2.45", +- "@midwayjs/mock": "^2.8.7", +- "@midwayjs/serverless-app": "^2.8.7", +- "@midwayjs/serverless-fc-trigger": "^2.10.3", +- "@midwayjs/serverless-fc-starter": "^2.10.3", +- "@types/jest": "^26.0.10", +- "@types/node": "14", +- "cross-env": "^6.0.0", +- "jest": "^26.4.0", +- "mwts": "^1.0.5", +- "ts-jest": "^26.2.0", +- "typescript": "~4.6.0" ++ "@midwayjs/mock": "^3.12.0", ++ "@types/jest": "29", ++ "@types/node": "16", ++ "cross-env": "^7.0.3", ++ "jest": "29", ++ "mwts": "^1.3.0", ++ "ts-jest": "29", ++ "ts-node": "^10.9.1", ++ "typescript": "~5.1.0" +} +``` + + + +## 2、入口主框架的变化 + +显式声明 faas 作为主框架。 + +```typescript +// src/configuration +import * as faas from '@midwayjs/faas'; + +@Configuration({ + // ... + imports: [ + faas + ], +}) +export class MainConfiguration { + // ... +} + +``` + + + +## 3、测试代码变化 + +移除了 `@midwayjs/serverless-app` 的依赖。 + +```diff +import { createFunctionApp, close, createHttpRequest } from '@midwayjs/mock'; +- import { Framework, Application } from '@midwayjs/serverless-app'; ++ import { Framework, Application } from '@midwayjs/faas'; +``` + +移除了 `@midwayjs/serverless-fc-trigger` 和 `@midwayjs/serverless-fc-starter` 依赖,修改为 `@midwayjs/fc-starter`。 + +```typescript +import { Application, Context, Framework } from '@midwayjs/faas'; +import { mockContext } from '@midwayjs/fc-starter'; +import { createFunctionApp } from '@midwayjs/mock'; + +describe('test/hello_aliyun.test.ts', () => { + + it('should get result from event trigger', async () => { + + // create app + const app: Application = await createFunctionApp(join(__dirname, '../'), { + initContext: Object.assign(mockContext(), { + function: { + name: '***', + handler: '***' + } + }), + }); + + // ... + + await close(app); + }); +}); +``` + +一些 API 的替代,比如原有的 `createXXXEvent`,将变为 `mockXXXEvent`,原有的 `createInitializeContext` 将变为 `mockContext` 方法。 + +这些 API 将直接从 `@midwayjs/fc-starter` 中导出。 + + + +## 5、部署方式的变化 + +不再使用 `midway-bin deploy` 进行部署,将采用平台自己的 CLI 工具,Midway 只提供框架和本地开发能力。 + +更多的部署调整,请查询 [纯函数部署](/docs/serverless/aliyun_faas)。 diff --git a/site/versioned_docs/version-3.0.0/service.md b/site/versioned_docs/version-3.0.0/service.md new file mode 100644 index 000000000000..02a803175698 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/service.md @@ -0,0 +1,173 @@ +# 服务和注入 + +在业务中,只有控制器(Controller)的代码是不够的,一般来说会有一些业务逻辑被抽象到一个特定的逻辑单元中,我们一般称为服务(Service)。 + + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01LLV2Qd20Fbu1NWXVA_!!6000000006820-2-tps-2130-344.png) + + +提供这个抽象有以下几个好处: + +- 保持 Controller 中的逻辑更加简洁。 +- 保持业务逻辑的独立性,抽象出来的 Service 可以被多个 Controller 重复调用。 +- 将逻辑和展现分离,更容易编写测试用例。 + + + +## 创建服务 + + +在 Midway 中,普通的服务就是一个 Class,比如我们之前创建了一个接受 user 请求的 Controller,我们来新增一个处理这些数据的服务。 + + +对于服务的文件,我们一般会存放到 `src/service` 目录中。我们来添加一个 user 服务。 + +```typescript +➜ my_midway_app tree +. +├── src +│ ├── controller +│ │ ├── user.ts +│ │ └── home.ts +│ ├── interface.ts +│ └── service +│ └── user.ts +├── test +├── package.json +└── tsconfig.json +``` + +内容为: + +```typescript +// src/service/user.ts +import { Provide } from '@midwayjs/core'; + +@Provide() +export class UserService { + + async getUser(id: number) { + return { + id, + name: 'Harry', + age: 18, + }; + } +} +``` +除了一个 `@Provide` 装饰器外,整个服务的结构和普通的 Class 一模一样,这样就行了。 + + +之前我们还增加了一个 User 定义,这里也可以直接使用。 + +```typescript +import { Provide } from '@midwayjs/core'; +import { User } from '../interface'; + +@Provide() +export class UserService { + + async getUser(id: number): Promise { + return { + id, + name: 'Harry', + age: 18', + }; + } +} +``` + + +## 使用服务 + + +在 Controller 处,我们需要来调用这个服务。传统的代码写法,我们需要初始化这个 Class(new),然后将实例放在需要调用的地方。在 Midway 中,你**不需要这么做**,只需要编写我们提供的** "依赖注入" **的代码写法。 + + +```typescript +import { Inject, Controller, Get, Provide, Query } from '@midwayjs/core'; +import { UserService } from '../service/user'; + +@Controller('/api/user') +export class APIController { + + @Inject() + userService: UserService; + + @Get('/') + async getUser(@Query('id') uid) { + const user = await this.userService.getUser(uid); + return {success: true, message: 'OK', data: user}; + } +} + +``` + +使用服务的过程分为几部分: + + +- 1、使用 `@Provide` 装饰器暴露你的服务 +- 2、在调用的代码处,使用 `@Inject` 装饰器注入你的服务 +- 3、调用注入服务,执行对应的方法 + + +Midway 的核心 “依赖注入” 容器会**自动关联**你的控制器(Controller) 和服务(Service),在运行过程中**会自动初始化**所有的代码,你**无需手动初始化**这些 Class。 + + +## 注入行为描述 + +看到这里,你会有一些疑惑,为什么服务(Service)上有一个 `@Provide` 装饰器,但是控制器(Controller) 上没有。 + +事实上,控制器(Controller) 上也有这个装饰器,只是在新版本中,Controller 包含了 Provide 的功能。如果你不确定什么时候可以隐藏,可以都写上。 + +你如果不写,默认等价于下面的代码。 + +```ts +@Provide() +@Controller('/api/user') +export class APIController { +``` + +`@Provide` 装饰器的作用: + + +- 1、这个 Class,被依赖注入容器托管,会自动被实例化(new) +- 2、这个 Class,可以被其他在容器中的 Class 注入 + + +而对应的 `@Inject` 装饰器,作用为: + + +- 1、在依赖注入容器中,找到对应的属性名,并赋值为对应的实例化对象 + + + +:::info +`@Inject` 的类中,必须有对应的 `@Provide` 才会生效。 +::: + + +`@Provide` 和 `@Inject` 装饰器是成对出现的,两者通过冒号后的类名进行关联。 +```typescript +// service +@Provide() +export class UserService { + //... +} + +// controller +@Provide() // <------ 由于有 Controller 包含了 Provide 的能力,这里展示的更加完整 +@Controller('/api/user') +export class APIController { + + @Inject() + userService: UserService; // <------ 这里的类型是 Class,即会注入一个该类型的实例 + + //... +} + +``` +这样的组合之后会用到很多地方,**请务必记住这个用法**。 + + +依赖注入还有更为复杂的情况,可以阅读 [依赖注入](container) 参考。 diff --git a/site/versioned_docs/version-3.0.0/service_factory.md b/site/versioned_docs/version-3.0.0/service_factory.md new file mode 100644 index 000000000000..a175d6c21c74 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/service_factory.md @@ -0,0 +1,534 @@ +# 服务工厂 + +有时候编写组件或者编写服务,会碰到某个服务有多实例的情况,这个时候服务工厂(Service Factory)就适合这种场景。 + +比如我们的 oss 组件,由于会创建多个 oss 对象,在编写的时候就需要留好多实例的接口。为了这种场景,midway 抽象了 `ServiceFactory` 类。 + + +`ServiceFactory` 是个抽象类,每个需要实现的服务,都需要继承他。 + + +我们以一个 http 客户端为例,需要准备一个创建 http 客户端实例的方法,其中包含几个部分: + + +- 1、创建客户端实例的方法 +- 2、客户端的配置 +- 3、实例化服务类 + +```typescript +// 创建客户端的配置 +const config = { + baseUrl: '', + timeout: 1000, +}; + +// 创建客户端实例的方法 +const httpClient = new HTTPClient(config); +``` + + +## 实现一个服务类 + + +我们希望实现一个上述 HTTPClient 的服务工厂,用于在 midway 体系中创建多个 httpClient 对象。 + + +服务工厂在 midway 中也是一个普通的导出类,作为服务的一员,比如我们也可以把他放到 `src/service/httpServiceFactory.ts` 中。 + + +### 1、实现创建实例接口 + +`ServiceFactory` 是个用于继承的抽象类,它包含一个泛型(创建的实例类型,比如下面就是创建出 HTTPClient 类型)。 + + +我们只需要继承它,同时,一般服务工厂为单例。 +```typescript +import { ServiceFactory, Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class HTTPClientServiceFactory extends ServiceFactory { + // ... +} +``` +由于是抽象类,我们需要实现其中的两个方法。 +```typescript +import { ServiceFactory, Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class HTTPClientServiceFactory extends ServiceFactory { + + // 创建单个实例 + protected createClient(config: any): any { + return new HTTPClient(config); + } + + getName() { + return 'httpClient'; + } +} +``` + +`createClient` 方法用于传入一个创建服务配置(比如 httpClient 配置),返回一个具体的实例,就像示例中的那样。 + + +`getName` 方法用于返回这个服务工厂的名字,方便框架识别和日志输出。 + + +### 2、增加配置和初始化方法 + + +我们需要注入一个配置,比如我们使用 `httpClient` 作为这个服务的配置。 +```typescript +// config.default.ts +export const httpClient = { + // ... +} +``` +然后注入到服务工厂中,同时,我们还需要在初始化时,调用创建多个实例的方法。 +```typescript +import { ServiceFactory, Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class HTTPClientServiceFactory extends ServiceFactory { + + @Config('httpClient') + httpClientConfig; + + @Init() + async init() { + await this.initClients(this.httpClientConfig); + } + + protected createClient(config: any): any { + // 创建实例 + return new HTTPClient(config); + } + + getName() { + return 'httpClient'; + } +} +``` +`initClients` 方法是基类中实现的,它需要传递一个完整的用户配置,并循环调用 `createClient` 来创建对象,保存到内存中。 + + +### 3、实例化服务类 + +为了方便用户使用,我们还需要提前将服务类创建,一般来说,只需要在组件或者项目的生命周期中实例化即可。 + +```typescript +import { Configuration } from '@midwayjs/core'; + +@Configuration({ + imports: [ + // ... + ] +}) +export class ContainerConfiguration { + async onReady(container) { + // 实例化服务类 + await container.getAsync(HTTPClientServiceFactory); + } +} +``` + + +## 获取实例 + + +`createClient` 方法只是定义了创建对象的方法,我们还需要定义配置的结构。 + +配置的结构分为几部分: + +- 1、默认配置,即所有对象都能复用的配置 +- 2、单个实例需要的配置 +- 3、多个实例需要的配置 + + + +我们来分别说明, + + +**默认配置** + + +默认的配置,我们约定为 `default` 属性。 +```typescript +// config.default.ts +export const httpClient = { + default: { + timeout: 3000 + } +} +``` + + +### 单个实例 + + +**单个配置** +```typescript +// config.default.ts +export const httpClient = { + default: { + timeout: 3000 + }, + client: { + baseUrl: '' + } +} +``` +`client` 用于单个实例结构的描述,创建对象时会和 `default` 做合并。使用 `get` 方法获取默认实例。 +```typescript +import { HTTPClientServiceFactory } from './service/httpClientServiceFactory'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + serviceFactory: HTTPClientServiceFactory; + + async invoke() { + const httpClient = this.serviceFactory.get(); + } +} + +``` + + +### 多个实例 + + +使用 `clients` 来配置多个实例,每个 key 都是一个独立的实例配置。 +```typescript +// config.default.ts +export const httpClient = { + default: { + timeout: 3000 + }, + clients: { + aaa: { + baseUrl: '' + }, + bbb: { + baseUrl: '' + } + } +} +``` +通过 key 来获取实例。 +```typescript +import { HTTPClientServiceFactory } from './service/httpClientServiceFactory'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + serviceFactory: HTTPClientServiceFactory; + + async invoke() { + + const aaaInstance = this.serviceFactory.get('aaa'); + // ... + + const bbbInstance = this.serviceFactory.get('bbb'); + // ... + + } +} +``` + + + +### 装饰器获取实例 + +从 v3.9.0 开始,ServiceFactory 添加了一个 `@InjectClient` 装饰器,方便在多客户端的的时候选择注入。 + +```typescript +import { HTTPClientServiceFactory } from './service/httpClientServiceFactory'; +import { join } from 'path'; +import { InjectClient } from '@midwayjs/core'; + +@Provide() +export class UserService { + + @InjectClient(HTTPClientServiceFactory, 'aaa') + aaaInstance: HTTPClientServiceFactory; + + @InjectClient(HTTPClientServiceFactory, 'bbb') + bbbInstance: HTTPClientServiceFactory; + + async invoke() { + // this.aaaInstance.xxx + // this.bbbInstance.xxx + // ... + } +} +``` + +`@InjectClient` 装饰器用于快速注入 `ServiceFactory` 派生实现的多实例,所有扩展与 `ServiceFactory` 的类,都能使用。 + +装饰器包含两个参数,定义如下: + +```typescript +export function InjectClient( + serviceFactoryClz: new (...args) => IServiceFactory, + clientName?: string +) { + // ... +} +``` + +| 参数 | 描述 | +| ----------------- | ------------------------------------------------------------ | +| serviceFactoryClz | 必填,`ServiceFactory` 的派生类,装饰器会从中获取查找实例。 | +| clientName | 可选,如果不填,默认会查找配置中的默认实例名 `defaultClientName` 配置项。 | + + + +### 动态创建实例 + + +也可以通过基类的 `createInstance` 方法动态获取实例。 + + +:::caution +注意,这里使用的不是子类的 createClient,createClient 不包含和默认配置的逻辑。 +::: + + +```typescript +import { HTTPClientServiceFactory } from './service/httpClientServiceFactory'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + serviceFactory: HTTPClientServiceFactory; + + async invoke() { + + // 会合并 config.bucket3 和 config.default + let customHttpClient = await this.serviceFactory.createInstance({ + baseUrl: 'xxxxx' + }, 'custom'); + + // 传了名字之后也可以从 factory 中获取 + customHttpClient = this.serviceFactory.get('custom'); + + } +} +``` +`createInstance` 方法的第一个参数是配置,如果动态调用的时候,可以手动传参,第二个参数是一个字符串名称,如果传入了名称,创建完的实例将会保存到内存中,后续可以从服务工厂中再次获取。 + + + +## 实例配置合并逻辑 + +在实际代码运行时,即使是单实例,配置一个 `client`,也会在内存中将配置变换为 `clients`。 + +比如下面的代码: + +```typescript +// config.default.ts +export const httpClient = { + client: { + baseUrl: '' + } +} +``` + +在内存中会变为: + +```typescript +// config.default.ts +export const httpClient = { + clients: { + default: { + baseUrl: '' + } + } +} +``` + +会多出一个名为 `default` 的默认实例,服务工厂会以 `clients` 的配置进行初始化。 + + + +## 默认实例代理(可选) + +如果用户每次使用时,都通过 `serviceFactory` 去获取,会非常的繁琐,对于最常用的默认实例,可以提供一个代理类,使其代理所有的目标实例方法。 + +```typescript +import { + Provide, + Scope, + ScopeEnum, + Init, + ServiceFactory, + MidwayCommonError, + delegateTargetAllPrototypeMethod +} from '@midwayjs/core'; + +// ... +export class HTTPClientServiceFactory extends ServiceFactory { + // ... +} + +// 下面是默认代理类 +@Provide() +@Scope(ScopeEnum.Singleton) +export class HTTPClientService implements HTTPClient { + @Inject() + private serviceFactory: HTTPClientServiceFactory; + + // 这个属性用于保存实际的实例 + private instance: HTTPClient; + + @Init() + async init() { + // 在初始化阶段,从工厂拿到默认实例 + this.instance = this.serviceFactory.get( + this.serviceFactory.getDefaultClientName() || 'default' + ); + if (!this.instance) { + throw new MidwayCommonError('http client default instance not found.'); + } + } +} + +// 下面这段代码,用于默认实例类的 ts 定义正确被继承 + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface HTTPClientService extends HTTPClient { + // empty +} + +// 下面这段代码,用于默认实例类的实现可以被代理 +delegateTargetAllPrototypeMethod(HTTPClientService, HTTPClient); + +``` + +通过上面的代码,我们就可以直接使用 `HTTPClientService` ,而无需从 `HTTPClientServiceFactory` 获取默认实例。 + +`delegateTargetAllPrototypeMethod` 是 Midway 提供的代理实例方法的工具方法。 + +此外,还有一些其他可用的工具方法,列举如下: + +- `delegateTargetAllPrototypeMethod` 用于代理目标所有的原型方法,包括原型链,不包括构造器和内部隐藏方法 +- `delegateTargetPrototypeMethod` 用于代理目标所有的原型方法,不包括构造器和内部隐藏方法 +- `delegateTargetMethod` 代理目标上指定的方法 + + + +## 修改默认实例名 + +默认情况下,默认的实例名为 `default` ,默认的实例代理内部会根据该实例进行代理。 + +假如用户没有配置 `default` 实例,或者希望修改默认实例,用户通过配置修改。 + +```typescript +// config.default.ts +export const httpClient = { + clients: { + default: { + baseUrl: '' + }, + default2: { + baseUrl: '' + } + }, + defaultClientName: 'default2', +} +``` + +在默认的实例代理中,会通过 `this.serviceFactory.getDefaultClientName()` 来获取这个值。 + +```typescript +import { HTTPClientService } from './service/httpClientServiceFactory'; +import { join } from 'path'; + +@Provide() +export class UserService { + + @Inject() + httpClientService: HTTPClientService; + + async invoke() { + // this.httpClientService 中指向的是 default2 + } +} +``` + + + +## 实例优先级 + +从 v3.14.0 开始,服务工厂的实例可以增加一个优先级属性,在不同的场景,会根据优先级做一些不同处理。 + +实例的优先级有 `L1`,`L2`, `L3`三个等级,分别对应高,中,低三个层级。 + +定义如下: + +```typescript +export const DEFAULT_PRIORITY = { + L1: 'High', + L2: 'Medium', + L3: 'Low', +}; +``` + +通过配置,我们可以指定不同实例的优先级。 + +```typescript +// config.default.ts +import { DEFAULT_PRIORITY } from '@midwayjs/core'; + +export default { + httpClient: { + clients: { + default: { + baseUrl: '' + }, + default2: { + baseUrl: '' + } + }, + clientPriority: { + default: DEFAULT_PRIORITY.L1, + default2: DEFAULT_PRIORITY.L2, + } + } +} +``` + +如果不做设置, 默认情况下优先级为中等,即 `DEFAULT_PRIORITY.L2`。 + +为了更好的判断优先级,`ServiceFactory` 基类中会增加一些方法。 + +```typescript +@Provide() +@Scope(ScopeEnum.Singleton) +export class HTTPClientService implements HTTPClient { + @Inject() + private serviceFactory: HTTPClientServiceFactory; + + @Init() + async init() { + // 获取优先级 + this.serviceFactory.getClientPriority('default'); // DEFAULT_PRIORITY.L2 + + // 判断优先级 + this.serviceFactory.isHighPriority('default'); + this.serviceFactory.isMediumPriority('default'); + this.serviceFactory.isLowPriority('default'); + } +} +``` + diff --git a/site/versioned_docs/version-3.0.0/sidebars.json b/site/versioned_docs/version-3.0.0/sidebars.json new file mode 100644 index 000000000000..388d406a299f --- /dev/null +++ b/site/versioned_docs/version-3.0.0/sidebars.json @@ -0,0 +1,321 @@ +{ + "common": [ + { + "type": "category", + "label": "新手指南", + "collapsed": false, + "collapsible": false, + "items": [ + "intro", + "quick_guide", + "how_to_update_midway", + "upgrade_v3" + ] + }, + { + "type": "category", + "label": "基础", + "collapsed": true, + "collapsible": true, + "items": [ + "quickstart", + "controller", + "middleware", + "req_res_app", + "service", + "error_filter", + "pipe", + "guard", + "midway_component", + "deployment" + ] + }, + { + "type": "category", + "label": "进阶", + "collapsed": true, + "collapsible": true, + "items": [ + "container", + "testing", + "mock", + "debugger", + "environment", + "env_config", + "lifecycle", + "logger_v3", + "cookie_session", + "built_in_service", + "router_table", + "decorator_index", + "error_code", + "esm" + ] + }, + { + "type": "category", + "label": "设计模式", + "collapsed": true, + "collapsible": true, + "items": [ + "aspect", + "auto_run", + "pipeline", + "service_factory", + "data_listener", + "data_source", + "data_response", + "retry" + ] + }, + { + "type": "category", + "label": "自定义", + "collapsed": true, + "collapsible": true, + "items": [ + "context_definition", + "component_development", + "custom_decorator", + "custom_error", + "change_start_dir" + ] + }, + { + "type": "category", + "label": "Serverless", + "collapsed": true, + "collapsible": true, + "items": [ + [ + "serverless/serverless_intro", + "serverless/serverless_v2_upgrade_serverless_v3", + "serverless/serverless_dev", + "serverless/serverless_testing", + "serverless/serverless_context", + "serverless/serverless_error", + "serverless/aliyun_faas" + ] + ] + }, + { + "type": "category", + "label": "开发工具", + "collapsed": true, + "collapsible": true, + "items": [ + "tool/create_midway", + "tool/mwtsc", + "tool/version_check", + "tool/mwts", + "tool/luckyeye", + "tool/egg-ts-helper" + ] + }, + { + "type": "category", + "label": "常见问题", + "collapsed": true, + "collapsible": true, + "items": [ + "faq/framework_problem", + "faq/git_problem", + "faq/npm_problem", + "faq/ts_problem", + "faq/alias_path", + "how_to_install_nodejs", + "ops/ecs_start_err", + "midway_slow_problem" + ] + }, + { + "type": "category", + "label": "其他", + "collapsed": true, + "collapsible": true, + "items": [ + "release_schedule", + "contributing", + "awesome_midway" + ] + }, + { + "type": "category", + "label": "历史文档", + "collapsed": true, + "collapsible": true, + "items": [ + "logger", + "tool/cli", + { + "type": "category", + "label": "一体化", + "collapsed": true, + "collapsible": true, + "items": [ + { + "type": "doc", + "id": "hooks/intro", + "label": "介绍" + }, + { + "type": "category", + "label": "基础功能", + "collapsed": false, + "collapsible": false, + "items": [ + "hooks/api", + "hooks/builtin-hooks", + "hooks/validate", + "hooks/middleware", + "hooks/cors", + "hooks/component", + "hooks/prisma", + "hooks/test", + "hooks/config", + "hooks/file-route", + "hooks/safe", + "hooks/upload" + ] + }, + { + "type": "category", + "label": "一体化", + "collapsed": false, + "collapsible": false, + "items": [ + "hooks/fullstack", + "hooks/client" + ] + }, + { + "type": "category", + "label": "进阶", + "collapsed": false, + "collapsible": false, + "items": [ + "hooks/deploy" + ] + } + ] + } + ] + } + ], + "component": [ + { + "type": "category", + "label": "通用", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/axios", + "extensions/i18n", + "extensions/info", + "extensions/validate", + "extensions/swagger", + "extensions/bull", + "extensions/cron", + "extensions/jwt" + ] + }, + { + "type": "category", + "label": "Http 服务", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/koa", + "extensions/egg", + "extensions/express", + "extensions/security", + "extensions/render", + "extensions/busboy", + "extensions/passport", + "extensions/casbin", + "extensions/static_file", + "extensions/cross_domain", + "extensions/http-proxy", + "extensions/captcha", + "extensions/tenant" + ] + }, + { + "type": "category", + "label": "数据存储", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/orm", + "extensions/sequelize", + "extensions/redis", + "extensions/mongodb", + "extensions/caching", + "extensions/oss", + "extensions/cos", + "extensions/tablestore", + "extensions/mikro" + ] + }, + { + "type": "category", + "label": "微服务", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/grpc", + "extensions/consul", + "extensions/etcd" + ] + }, + { + "type": "category", + "label": "WebSocket", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/socketio", + "extensions/ws" + ] + }, + { + "type": "category", + "label": "消息", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/rabbitmq", + "extensions/kafka", + "extensions/mqtt" + ] + }, + { + "type": "category", + "label": "运维", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/otel", + "extensions/code_dye", + "extensions/pm2", + "extensions/cfork", + "extensions/alinode", + "extensions/prometheus" + ] + }, + { + "type": "category", + "label": "历史废弃组件", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/cache", + "extensions/upload", + "legacy/mongodb", + "legacy/sequelize", + "legacy/orm", + "legacy/task" + ] + } + ] +} diff --git a/site/versioned_docs/version-3.0.0/testing.md b/site/versioned_docs/version-3.0.0/testing.md new file mode 100644 index 000000000000..7554c56a04af --- /dev/null +++ b/site/versioned_docs/version-3.0.0/testing.md @@ -0,0 +1,746 @@ + + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 测试 + +应用开发中,测试十分重要,在传统 Web 产品快速迭代的时期,每个测试用例都给应用的稳定性提供了一层保障。 API 升级,测试用例可以很好地检查代码是否向下兼容。 对于各种可能的输入,一旦测试覆盖,都能明确它的输出。 代码改动后,可以通过测试结果判断代码的改动是否影响已确定的结果。 + +所以,应用的 Controller、Service 等代码,都必须有对应的单元测试保证代码质量。 当然,框架和组件的每个功能改动和重构都需要有相应的单元测试,并且要求尽量做到修改的代码能被 100% 覆盖到。 + +当前社区的测试库主要是 `jest` 和 `mocha` ,本文以 `jest` 作为示例 。 + + + +## 测试目录结构 + + +我们约定 `test` 目录为存放所有测试脚本的目录,测试所使用到的 `fixtures` 和相关辅助脚本都应该放在此目录下。 + + +测试脚本文件统一按 `${filename}.test.ts` 命名,必须以 `.test.ts` 作为文件后缀。 + + +一个应用的测试目录示例: +```text +➜ my_midway_app tree +. +├── src +├── test +│ └── controller +│ └── home.controller.test.ts +├── package.json +└── tsconfig.json +``` + + + +## 测试运行工具 + + +Midway 默认提供 `midway-bin` 命令来运行测试脚本。在新版本中,Midway 默认将 mocha 替换成了 Jest,它的功能更为强大,集成度更高,这让我们**聚焦精力在编写测试代码**上,而不是纠结选择那些测试周边工具和模块。 + +只需要在 `package.json` 上配置好 `scripts.test` 即可。 + + + + + + +```json +{ + "scripts": { + "test": "jest" + } +} +``` + +然后就可以按标准的 `npm test` 来运行测试了,默认脚手架中,我们都已经提供了此命令,所以你可以开箱即用的运行测试。 + +```bash +➜ my_midway_app npm run test + +> my_midway_project@1.0.0 test /Users/harry/project/application/my_midway_app +> jest + +Testing all *.test.ts... + PASS test/controller/home.controller.test.ts + PASS test/controller/api.controller.test.ts + +Test Suites: 2 passed, 2 total +Tests: 2 passed, 2 total +Snapshots: 0 total +Time: 3.26 s +Ran all test suites matching /\/test\/[^.]*\.test\.ts$/i. +``` + + + + + + +```json +{ + "scripts": { + "test": "midway-bin test --ts" + } +} +``` + +然后就可以按标准的 `npm test` 来运行测试了,默认脚手架中,我们都已经提供了此命令,所以你可以开箱即用的运行测试。 + +```bash +➜ my_midway_app npm run test + +> my_midway_project@1.0.0 test /Users/harry/project/application/my_midway_app +> midway-bin test + +Testing all *.test.ts... + PASS test/controller/home.controller.test.ts + PASS test/controller/api.controller.test.ts + +Test Suites: 2 passed, 2 total +Tests: 2 passed, 2 total +Snapshots: 0 total +Time: 3.26 s +Ran all test suites matching /\/test\/[^.]*\.test\.ts$/i. +``` + + + + + + + +## 断言库 + + +jest 中自带了强大的 `expect` 断言库,可以直接在全局使用它。 + + +比如常用的。 + + +```typescript +expect(result.status).toBe(200); // 值是否等于某个值,引用相等 +expect(result.status).not.toBe(200); +expect(result).toEqual('hello'); // 简单匹配,对象属性相同也为 true +expect(result).toStrictEqual('hello'); // 严格匹配 +expect(['lime', 'apple']).toContain('lime'); // 判断是否在数组中 +``` + + +更多断言方法,请参考文档 [https://jestjs.io/docs/en/expect](https://jestjs.io/docs/en/expect) + + + +## 创建测试 + + +不同的上层框架的测试方法不同,以最常用的 HTTP 服务举例,如果需要测试一个 HTTP 服务,一般来说,我们需要创建一个 HTTP 服务,然后用客户端请求它。 + + +Midway 提供了一套基础的 `@midwayjs/mock` 工具集,可以帮助上层框架在这方面进行测试。同时也提供了方便的创建 Framework,App ,以及关闭的方法。 + + +整个流程方法分为几个部分: + + +- `createApp` 创建某个 Framework 的 app 对象 +- `close` 关闭一个 Framework 或者一个 app + +为保持测试简单,整个流程目前就透出这两个方法。 +```typescript +// create app +const app = await createApp(); +``` +这里传入的 `Framework` 是用来给 TypeScript 推导类型的。这样就可以返回主框架 app 实例了。 + + +当 app 运行完成后,可以使用 `close` 方法关闭。 +```typescript +import { createApp, close } from '@midwayjs/mock'; + +await close(app); +``` +事实上, `createApp` 方法中都是封装了 `@midwayjs/bootstrap` ,有兴趣的小伙伴可以阅读源码。 + + + +## 测试 HTTP 服务 + + +除了创建 app 之外, `@midwayjs/mock` 还提供了简单的客户端方法,用于快速创建各种服务对应的测试行为。 + + +比如,针对 HTTP,我们封装了 supertest,提供了 `createHttpRequest` 方法创建 HTTP 客户端。 + + +```typescript +// 创建一个客户端请求 +const result = await createHttpRequest(app).get('/'); +// 测试返回结果 +expect(result.text).toBe('Hello Midwayjs!'); +``` + + +推荐在一个测试文件中复用 app 实例。完整的测试示例如下。 +```typescript +import { createApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework, Application } from '@midwayjs/koa'; +import * as assert from 'assert'; + +describe('test/controller/home.test.ts', () => { + + let app: Application; + + beforeAll(async () => { + // 只创建一次 app,可以复用 + app = await createApp(); + }); + + afterAll(async () => { + // close app + await close(app); + }); + + it('should GET /', async () => { + // make request + const result = await createHttpRequest(app) + .get('/') + .set('x-timeout', '5000'); + + // use expect by jest + expect(result.status).toBe(200); + expect(result.text).toBe('Hello Midwayjs!'); + + // or use assert + assert.deepStrictEqual(result.status, 200); + assert.deepStrictEqual(result.text, 'Hello Midwayjs!'); + }); + + it('should POST /', async () => { + // make request + const result = await createHttpRequest(app) + .post('/') + .send({id: '1'}); + + // use expect by jest + expect(result.status).toBe(200); + }); + +}); + +``` + + +**示例:** + + +创建 get 请求,传递 query 参数。 +```typescript +const result = await createHttpRequest(app) + .get('/set_header') + .query({ name: 'harry' }); +``` + + +创建 post 请求,传递 body 参数。 +```typescript +const result = await createHttpRequest(app) + .post('/user/catchThrowWithValidate') + .send({id: '1'}); +``` + + +创建 post 请求,传递 form body 参数。 +```typescript +const result = await createHttpRequest(app) + .post('/param/body') + .type('form') + .send({id: '1'}) +``` + + +传递 header 头。 +```typescript +const result = await createHttpRequest(app) + .get('/set_header') + .set({ + 'x-bbb': '123' + }) + .query({ name: 'harry' }); +``` + 传递 cookie。 +```typescript +const cookie = [ + "koa.sess=eyJuYW1lIjoiaGFycnkiLCJfZXhwaXJlIjoxNjE0MTQ5OTQ5NDcyLCJfbWF4QWdlIjo4NjQwMDAwMH0=; path=/; expires=Wed, 24 Feb 2021 06:59:09 GMT; httponly", + "koa.sess.sig=mMRQWascH-If2-BC7v8xfRbmiNo; path=/; expires=Wed, 24 Feb 2021 06:59:09 GMT; httponly" +] + +const result = await createHttpRequest(app) + .get('/set_header') + .set('Cookie', cookie) + .query({ name: 'harry' }); +``` + + +## 测试服务 + + +在控制器之外,有时候我们需要对单个服务进行测试,我们可以从依赖注入容器中获取这个服务。 + +假设需要测试 `UserService` 。 + + +```typescript +// src/service/user.ts +import { Provide } from '@midwayjs/core'; + +@Provide() +export class UserService { + async getUser() { + // xxx + } +} +``` + +那么在测试代码中这样写。 + +```typescript +import { createApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework } from '@midwayjs/web'; +import * as assert from 'assert'; +import { UserService } from '../../src/service/user'; + +describe('test/controller/home.test.ts', () => { + + it('should GET /', async () => { + // create app + const app = await createApp(); + + // 根据依赖注入 class 获取实例(推荐) + const userService = await app.getApplicationContext().getAsync(UserService); + // 根据依赖注入 Id 获取实例 + const userService = await app.getApplicationContext().getAsync('userService'); + // 传入 class 忽略泛型也能正确推导 + const userService = await app.getApplicationContext().getAsync(UserService); + + // close app + await close(app); + }); + +}); +``` + + +如果你的服务和请求相关联(ctx),可以使用请求作用域获取服务。 + + +```typescript +import { createApp, close, createHttpRequest } from '@midwayjs/mock'; +import { Framework } from '@midwayjs/web'; +import * as assert from 'assert'; +import { UserService } from '../../src/service/user'; + +describe('test/controller/home.test.ts', () => { + + it('should GET /', async () => { + // create app + const app = await createApp(); + + // 根据依赖注入 Id 获取实例 + const userService = await app.createAnonymousContext() + .requestContext.getAsync('userService'); + + // 也能传入 class 获取实例 + const userService = await app.createAnonymousContext() + .requestContext.getAsync(UserService); + + // close app + await close(app); + }); + +}); +``` + + + +## createApp 选项参数 + +`createApp` 方法用于创建一个框架的 app 实例,通过传入泛型的框架类型,来使得我们推断出的 app 能够是该框架返回的 app。 + + +比如: + +```typescript +import { Framework } from '@midwayjs/grpc'; + +// 这里的 app 能确保是 grpc 框架返回的 app +const app = await createApp(); +``` +`createApp` 方法其实是有参数的,它的方法签名如下。 +```typescript +async createApp( + appDir = process.cwd(), + options: IConfigurationOptions = {} +) +``` +第一个参数为项目的绝对根目录路径,默认为 `process.cwd()` 。 +第二个参数为 Bootstrap 的启动参数,比如一些全局行为的配置,具体可以参考 ts 定义。 + + + +## close 选项参数 + + +`close` 方法用于关闭该 app 实例相关的框架。 + +```typescript +await close(app); +``` + +其有一些参数。 + +```typescript +export declare function close( + app: IMidwayApplication | IMidwayFramework, + options?: { + cleanLogsDir?: boolean; + cleanTempDir?: boolean; + sleep?: number; +}): Promise; +``` +第一个参数是 app 或者 framework 的实例。 + + +第二个参数是个对象,在执行关闭时可以执行一些行为: + + +- 1、 `cleanLogsDir` 默认为 false,控制测试完成后删除日志 logs 目录(windows 除外) +- 2、 `cleanTempDir` 默认为 false,清理一些临时目录(比如 egg 生成的 run 目录) +- 3、 `sleep` 默认为 50,单位毫秒,关闭 app 后延迟的时间(防止日志没有成功写入) + +## 使用 bootstrap 文件测试 + +一般情况下,你无需用到 `bootstrap.js` 来测试。如果你希望直接使用 `bootstrap.js` 入口文件直接测试,那么可以在测试的时候传递入口文件信息。 + +和 dev/test 启动不同的是,使用 `bootstrap.js` 启动是一个真实的服务,会同时运行多个框架,创建出多个框架的 app 实例。 + +`@midwayjs/mock` 提供了 `createBootstrap` 方法做启动文件类型的测试。我们可以将入口文件 `bootstrap.js` 作为启动参数传入,这样 `createBootstrap` 方法会通过入口文件来启动代码。 + +```typescript +it('should GET /', async () => { + // create app + const bootstrap = await createBootstrap(join(process.cwd(), 'bootstrap.js')); + // 根据框架类型获取 app 实例 + const app = bootstrap.getApp('koa'); + + // expect and test + + // close bootstrap + await bootstrap.close(); +}); +``` + + + +## 运行单个测试 + +和 mocha 的 `only` 不同,jest 的 `only` 方法只针对单个文件生效。 + + + + + +执行单个文件。 + +```bash +$ jest test/controller/api.ts +``` + +如果你想运行文件中的特定测试,你可以使用 jest 的 `-t` 或 `--testNamePattern` 选项,后面跟上你想运行的测试的名称。例如: + +```bash +$ jest -t "name of your test" +``` + +这将只运行名称匹配的测试。 + + + + + + `midway-bin` 提供可以运行单个文件的能力。 + +```bash +$ midway-bin test -f test/controller/api.ts +``` + +这样可以指定运行某个文件的测试,再配合 `describe.only` 和 `it.only` ,这样可以只运行单个文件中的单个测试方法。 + + +`midway-bin test --ts` 等价于直接使用 jest 的下面的命令。 + +```bash +$ node --require=ts-node/register ./node_modules/.bin/jest +``` + + + + + + + +## 自定义 Jest 文件内容 + + +一般情况下,Midway 工具链内置了 jest 配置,使得用户无需再添加该文件,但是有些特殊的场景下,比如使用 VSCode 或者 Idea 等编辑器,需要在可视化区域进行开发和测试时,可能会需要指定一个 `jest.config.js` 的场景,这种情况下,Midway 支持创建一个自定义的 jest 配置文件。 + + +在项目根目录创建一个 `jest.config.js` 文件。 + +``` +➜ my_midway_app tree +. +├── src +├── test +│ └── controller +│ └── home.test.ts +├── jest.config.js +├── package.json +└── tsconfig.json +``` +内容如下,配置和标准的 jest 相同。 + +```javascript +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], +}; +``` + + +## 常见设置 + + +如果需要在单测前执行一些代码,可以增加 `jest.setup.js` ,增加配置如下。 +```javascript +const path = require('path'); + +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + setupFilesAfterEnv: ['/jest.setup.js'], // 预先读取 jest.setup.js +}; +``` + +:::caution +注意, `jest.setup.js` 只能使用 js 文件。 +::: + + +### 示例一:测试代码时间较长的问题 + + +如果测试出现下面的错误,说明你的代码执行时间比较长(比如连接数据库,跑任务等),如果确定代码没有问题,就需要延长启动时间。 +``` +Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout. +``` +jest 默认时间为 **5000ms(5秒钟)**,我们可以将它调整到更多。 + +可以通过在 `package.json` 启动时修改。 + + + + + +```json +{ + "scripts": { + "test": "jest --testTimeout=30000" + } +} +``` + + + + + +```json +{ + "scripts": { + "test": "midway-bin test --ts --testTimeout=30000" + } +} +``` + +这里的 `testTimeout` 是 jest 的启动参数。 + + + + + +我们可以在 `jest.setup.js` 文件中写入下面的代码,对 jest 超时时间做调整。 + +```javascript +// jest.setup.js +jest.setTimeout(30000); +``` + + +### 示例二:全局环境变量 + + +同理, `jest.setup.js` 也可以执行自定义的代码,比如设置全局环境变量。 + +```javascript +// jest.setup.js +process.env.MIDWAY_TS_MODE = 'true'; +``` +### 示例三:程序无法正常退出的处理 + +有时候,由于一些代码(定时器,监听等)在后台运行,导致单测跑完后会无法退出进程,对于这个情况,jest 提供了 `--forceExit` 参数。 + + + + + +```bash +$ jest --forceExit +$ jest --coverage --forceExit +``` + + + + + +```bash +$ midway-bin test --ts --forceExit +$ midway-bin cov --ts --forceExit +``` + +这里的 `testTimeout` 是 jest 的启动参数。 + + + + + +也可以在自定义文件中,增加属性。 + +```javascript +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + forceExit: true, +}; +``` + +### 示例四:并行改串行执行 + +jest 默认为每个测试文件并行处理,如果测试代码中有启动端口等场景,并行处理可能会导致端口冲突而报错,这个时候需要加 `--runInBand` 参数,注意,这个参数只能加载命令中。 + + + + + +```bash +$ jest --runInBand +$ jest --coverage --runInBand +``` + + + + + +```bash +$ midway-bin test --ts --runInBand +$ midway-bin cov --ts --runInBand +``` + + + + + + + +## 编辑器配置 + + +### Jetbrain Webstorm/Idea 配置 + + +在 Jetbrain 的编辑器使用,需要启用 "jest" 插件,由于使用了子进程的方式启动,我们依旧需要在启动时指定加载 `--require=ts-node/register` 。 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01Wa6UaE1p0zU82gnpL_!!6000000005299-2-tps-1500-951.png) + + +### VSCode 配置 + + +先搜索插件,安装 Jest Runner。 +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01D6zTxi1GiwwrqhHVW_!!6000000000657-2-tps-1242-877.png) +打开配置,配置 jest 命令路径。 + + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN017BK54o1n2FL7x8hI0_!!6000000005031-2-tps-1266-849.png) + + +在 jest command 处填入 `node --require=ts-node/register ./node_modules/.bin/jest` 。 + + +或者是在工作区文件夹 .vscode 里面设置 settings.json。 + +```json +{ + "jest.pathToJest": "node --require=ts-node/register ./node_modules/.bin/jest --detectOpenHandles", + "jestrunner.jestCommand": "node --require=ts-node/register ./node_modules/.bin/jest --detectOpenHandles" +} +``` + + +由于 jest runner 插件的调试使用的是 VSCode 的调试,需要单独配置 VSCode 的 launch.json。 + + +在文件夹 .vscode 里面设置 launch.json + +```json +{ + "version": "0.0.1", + "configurations": [ + { + "name": "Debug Jest Tests", + "type": "node", + "request": "launch", + "runtimeArgs": [ + "--inspect-brk", + "--require=ts-node/register", + "${workspaceRoot}/node_modules/.bin/jest", + "--runInBand", + "--detectOpenHandles" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} +``` + + + +## 关于 alias paths + +`mwtsc` 工具不支持 Alias Path 功能。 + + + +## 关于 mock 数据 + +模拟数据是一个可以在开发和测试中都通用的能力,更多请查看 [模拟数据](./mock)。 diff --git a/site/versioned_docs/version-3.0.0/tool/cli.md b/site/versioned_docs/version-3.0.0/tool/cli.md new file mode 100644 index 000000000000..605f1d17f37f --- /dev/null +++ b/site/versioned_docs/version-3.0.0/tool/cli.md @@ -0,0 +1,523 @@ +# Midway CLI + +:::tip + +由于 CLI 底层能力都来源于社区现有的模块功能,为了减少过渡封装带来的维护成本和理解成本,CLI 中的各项功能都将逐步变为社区现有的模块,同时 CLI 库将停止继续迭代。 + +为此后续的变化为 + +* 开发将从 `midway-bin dev` 变为 `mwtsc` +* 编译将从 `midway-bin build` 变为 `tsc` +* 测试将从 `midway-bin test` 变为 `mocha` 或者 `jest` +* 覆盖率将从 `midway-bin cov` 变为 `jest --coverage` 或者其他类似指令 + +::: + + + +`@midwayjs/cli` 是新版本的 Midway 体系工具链,和 Serverless,以及原应用的工具链进行了整合。 + + +## 基础入口 + +`@midwayjs/cli` 提供了两个入口命令。 `midway-bin` 和 `mw` 命令。 + +当 `@midwayjs/cli` 安装到全局时,一般使用 `mw` 命令,比如 `mw dev` 。当安装到项目中,做 cli 工具时,我们一般使用 `midway-bin` 命令,但是请记住,这两个命令是相同的。 + + + +## dev 命令 + +以当前目录启动本地开发命令。 + +```bash +$ mw dev + --baseDir 应用目录,一般为 package.json 所在文件夹,默认为 process.cwd() + --sourceDir ts代码目录,默认会自动分析 + -p, --port dev侦听的端口,默认为 7001 + --ts TS模式运行代码 + --fast 极速模式 + --framework 指定框架,默认会自动分析 + -f, --entryFile 指定使用入口文件来启动 bootstrap.js + --watchFile 更多的文件或文件夹修改侦听 + --notWatch 代码变化时不自动重启 +``` + +### **标准启动** + +```bash +$ midway-bin dev --ts +``` + +### **修改启动端口** + +针对 HTTP 场景, `-p` 或者 `--port` 可以临时修改端口。 + +```bash +$ midway-bin dev --ts --port=7002 +``` + +### **修改启动路径** + +指定应用根目录,一般为 package.json 所在文件夹,默认为 process.cwd() + +```shell +$ midway-bin dev --ts --baseDir=./app +``` + +### **修改ts源码路径** + +指定ts代码目录,默认会自动分析 + +```shell +$ midway-bin dev --ts --sourceDir=./app/src +``` +### **修改 tsconfig.json 的位置** +通过设置 [TS_NODE_PROJECT](https://github.com/TypeStrong/ts-node#project) 环境变量来指定tsconfig.json的位置。 +```shell +$ cross-env TS_NODE_PROJECT=./tsconfig.dev.json midway-bin dev -ts +``` + +### **更快的启动方式** + +默认的启动方式为 ts-node,在文件数量特别多的情况下会比较慢,可以切换为 swc 等新的编译方式。 + +```shell +// 使用 ts-node 的快速dev模式 +$ midway-bin dev --ts --fast + +// 使用 swc 的快速dev模式 +$ midway-bin dev --ts --fast=swc +``` + +### 监听文件变化 + +`--watchFile` 用于指定更多的文件或文件夹修改侦听,默认侦听 `sourceDir` 目录中 `.ts`、`.yml`和 `.json`结尾的文件(可通过 --watchExt 参数指定更多扩展名),以及 `baseDir` 目录中的 `f.yml` 文件 + +```shell +// 指定多个文件,使用英文逗号分隔 +$ midway-bin dev --ts --watchFile=./a.txt,./b.txt + +// 指定多个文件夹和文件,使用英文逗号分隔 +$ midway-bin dev --ts --watchFile=./test,./b.txt +``` + + +- `--watchExt`:指定更多的侦听文件扩展名,默认为 `.ts`、`.yml`和 `.json` + +```shell +// 指定多个文件扩展名,使用英文逗号分隔 +$ midway-bin dev --ts --watchExt=.js,.html +``` + + +### 本地单步Debug调试 + + `--debug` 参数启动 debug 模式,可以通过 `chrome devtools` 进行单步代码调试: + +![69456694-513D-4388-B52F-001562D4A520.png](https://cdn.nlark.com/yuque/0/2021/png/128621/1635994136312-f1eda8ba-165d-4322-82b8-b21d3b9c6beb.png#clientId=u32db4720-b7d0-4&crop=0&crop=0&crop=1&crop=1&from=ui&height=177&id=z4u1f&margin=%5Bobject%20Object%5D&name=69456694-513D-4388-B52F-001562D4A520.png&originHeight=666&originWidth=1538&originalType=binary&ratio=1&rotation=0&showTitle=false&size=276022&status=done&style=none&taskId=ud161d835-1e96-4246-8061-c795e9a0ff1&title=&width=409) +您可以通过 `chrome://inspect/` 打开 `nodejs devtools` 进行断点调试: + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/128621/1635995391144-a9ec0d4a-c6fb-4638-a292-615a3588d33d.png#clientId=u069cda7c-313b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=236&id=u4986bfa4&margin=%5Bobject%20Object%5D&name=image.png&originHeight=942&originWidth=1948&originalType=binary&ratio=1&rotation=0&showTitle=false&size=572568&status=done&style=none&taskId=u07555349-8e09-42b2-bd94-f93160b0431&title=&width=488) + +![image.png](https://cdn.nlark.com/yuque/0/2021/png/128621/1635995418427-282d256a-de65-4eba-9a83-b474d3d74f9f.png#clientId=u069cda7c-313b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=445&id=u83271ad1&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1280&originWidth=2280&originalType=binary&ratio=1&rotation=0&showTitle=false&size=710504&status=done&style=none&taskId=uc2614db9-dea9-48d7-b87d-8cb608c8770&title=&width=792) +您也可以直接通过 chrome 浏览器打开命令行中输出的 `devtools` 协议的链接,给对应代码添加断点后调试: + +![10016148-385E-46A4-8B3A-0A0110BECD18.png](https://cdn.nlark.com/yuque/0/2021/png/128621/1635994137067-f663409a-483d-41f5-bc86-4798182edb38.png#clientId=u32db4720-b7d0-4&crop=0&crop=0&crop=1&crop=1&from=ui&height=135&id=GooAh&margin=%5Bobject%20Object%5D&name=10016148-385E-46A4-8B3A-0A0110BECD18.png&originHeight=950&originWidth=2878&originalType=binary&ratio=1&rotation=0&showTitle=false&size=744085&status=done&style=none&taskId=u892d9925-9206-4946-a1ed-cb6043c557d&title=&width=409) + +如果您使用 `vscode` ,那么您可以使用 vscode 的 js debug terminal,在其中执行 dev 命令(无需添加 `--debug` 参数)启动就可以打断点调试了。![image.png](https://cdn.nlark.com/yuque/0/2021/png/128621/1625237917317-8e7bf448-fded-4bc7-b743-6aade0ebcba2.png#clientId=u7c8a3183-c32b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=650&id=u75e3aec7&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1300&originWidth=2868&originalType=binary&ratio=1&rotation=0&showTitle=false&size=1140427&status=done&style=none&taskId=ubcffa6c8-02eb-4256-ba7e-7ab3128c1ee&title=&width=1434) + + +## test 命令 + +以当前目录启动测试,默认使用 jest 工具,可以使用 --mocha 参数指定使用 mocha。 + +```bash +$ midway-bin test --ts + -c, --cov 获取代码测试覆盖率 + -f, --file 指定测试文件,例如 ./test/index.test.ts + --ts TS模式运行单测 + --forceExit jest forceExit + --runInBand jest runInBand + -w, --watch watch模式 + --mocha 使用 mocha 进行单测 +``` + +使用 mocha 进行单测时,需要手动安装 `mocha` 和 `@types/mocha` 两个依赖到 `devDependencies` 中:`npm i mocha @types/mocha -D` 。 + +:::info +如果项目中使用了 TypeScript 的 path alias,请参考:[测试](../testing#配置-alias-paths) +::: + + + +### 使用 mocha 替代 jest + + +有些同学对 mocha 情有独钟,希望使用 mocha 作为测试工具。 + + +可以使用 mocha 模式进行测试。 + +```bash +$ midway-bin test --ts --mocha +``` + + +使用 mocha 进行单测时,需要手动安装 `mocha` 和 `@types/mocha` 两个依赖到 `devDependencies` 中:`npm i mocha @types/mocha -D` 。 + +### 配置 alias paths + +当你在 `tsconfig.json` 中配置了 paths 之后,并且模块包导入使用了 paths ,则会存在 mocha 做单元测试会导致路径无法被解析,无法使用通过导入 `tsconfig-paths/register` 解决 + +```typescript +// src/configuration.ts + +import 'tsconfig-paths/register'; +// ... +``` + +需要添加 `tsconfig-paths` 并且在测试的时候引用进行处理 + +```bash +$ npm install --save-dev tsconfig-paths +``` + +```bash +$ midway-bin test --ts --mocha -r tsconfig-paths/register +``` + +:::info +注意,由于 mocha 没有自带断言工具,需要使用其他如 assert,chai 等工具进行断言。 +::: + + + + + + +## cov 命令 + +以当前目录启动测试,并输出覆盖率信息,默认使用 jest 工具,可以使用 --mocha 参数指定使用 mocha。 + +```bash +$ midway-bin cov --ts +``` + +当使用 mocha 进行单测覆盖率时,您需要安装以下额外依赖。 + +```bash +$ npm i mocha @types/mocha nyc --save-dev +``` + + + + +## check 命令 +自动分析代码中存在的问题,并给出修复建议。 + +```bash +$ midway-bin check +``` + +目前已提供 `32` 项问题的校验。 + + + + +## build 命令 + +使用 mwcc(tsc)进行 ts 代码编译,适用于标准项目,Serverless 项目请使用 package。 + + +```bash +$ midway-bin build -c + + -c, --clean 清理构建结果目录 + --srcDir 源代码目录,默认 src + --outDir 构建输出目录,默认为 tsconfig 中的 outDir 或 dist + --tsConfig tsConfig json 字符串或文件位置 + --buildCache 保留构建缓存 +``` + + + + +## deploy 命令 + +适用于 Serverless 项目发布到 Aliyun FC、Tencent SCF、Aws Lambda 等运行时。 + +执行 deploy 命令会自动执行 package。 + + +```bash +$ midway-bin deploy + + -y, --yes 发布的确认都是yes + --resetConfig 重置发布配置,AK/AK/Region等 + --serverlessDev 使用 Serverless Dev 进行aliyun fc函数发布,目前默认为 funcraft + ...兼容package命令的所有参数 +``` + + + +#### 函数发布时域名配置 + +在 `f.yml` 中配置 `custom.customDomain` 为 `auto` ,则在发布时会配置一个临时的自动域名: + +```yaml +custom: + customDomain: auto +``` + +如果要取消自动的域名,将 `customDomain` 改为 `false`: + +```yaml +custom: + customDomain: false +``` + +如果有自定义域名,在 `customDomain` 中配置即可: + +```yaml +custom: + customDomain: + domainName: test.example.com +``` + +如果自定义的域名,需要使用 https,那么在 云控制台 配置好 https 证书之后,需要将 customDomain 设置为 false,避免下次发布时重置成 http: + +```cpp +custom: + customDomain: false +``` + + + +#### 每个路由都部署成了一个函数 +可以使用高密度方案,合并成一个函数,f.yml 加如下配置 + +```cpp +aggregation: + main: + functionsPattern: + - '*' +``` + + +#### + +#### aliyun 发布 AK 错误问题 +在第一次进行aliyun发布或使用 `--resetConfig`参数的时候都可以重置 ak。 + +不过要注意的是每次 ak 都会默认创建一个新的 `access` 分组,在修改配置时会自动生成分组名,如果要覆盖之前的 AK 需要手动输入,如图: + +![image.png](https://cdn.nlark.com/yuque/0/2022/png/128621/1645609990378-8a7f92c0-bda4-46e0-93a6-4d6feb6ec66d.png#clientId=u9f50c864-5385-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=122&id=u8a756167&margin=%5Bobject%20Object%5D&name=image.png&originHeight=122&originWidth=693&originalType=binary&ratio=1&rotation=0&showTitle=false&size=17245&status=done&style=none&taskId=u3b825703-abe6-4a2b-ae5f-86a88027cf8&title=&width=693) + +发布时默认使用的分组为 `default`,如果您在修改配置时如上图使用了 `default-2`,那么需要在发布的时候通过 `--access`参数指定使用 `default-2`: + +```cpp +midway-bin deploy --access=default-2 +``` + + + +## package 命令 + +适用于 Serverless 项目构建 + +```bash +$ midway-bin package + --npm npm client,默认为自动识别添加registry + --sourceDir 源代码所在目录,默认会自动分析 + --buildDir 构建结果目标目录 + --sharedTargetDir 共享文件目标目录,默认为static,参考 --sharedDir 参数 + --sharedDir 构建时会拷贝此目录到结果目录内的 $sharedTargetDir 目录 + --skipZip 跳过zip打包 + --skipBuild 跳过ts代码构建 + --tsConfig tsConfig json 字符串或文件位置 + --function 指定打包哪几个函数,多个使用英文 , 分隔 +``` + + +#### 参数详解 + +- `--function`:指定打包哪几个函数,多个函数使用英文 , 分隔 + +```shell +// 打包 +midway-bin package --function=a,b,c + +// 发布 +midway-bin deploy --function=a,b,c +``` + + + + +#### 函数构建打包时文件拷贝逻辑 + +默认拷贝的内容包含 `后端代码文件夹` (一般为 `src` 、faas前后端一体化一般为 `src/apis`)内的所有非 `.ts` 后缀的文件,以及 `项目根目录` 下的以 `.js`、`.json`、`.yml` 为扩展名的所有文件和 `config` 、`app` 文件夹内的所有文件。 + +如果要拷贝额外的文件,可以通过在 `f.yml` 文件中添加 `package`字段 中的 `include` 来指定,可以配置文件名,也可以通过 `fast-glob` [语法↗](https://github.com/mrmlnc/fast-glob#pattern-syntax) 匹配,使用示例如下: + +```cpp +# ...已省略其他属性的展示 + +package: + include: # 通过 include 属性指定额外打包文件配置 + - static # 项目根目录下的 static 文件夹 + - a.json # 项目根目录下的 a.json 文件 + - a/b/c.js # 项目根目录下的 a 目录下的 b 目录下的 c.js 文件 + - a/b/c.json # 项目根目录下的 a 目录下的 b 目录下的 c.js 文件 + - xxx/**/*.js # 项目根目录下的 xxx 目录下的所有 js 文件 +``` + + + +## 实验性功能 + +在 `f.yml` 中通过 `experimentalFeatures` 配置开启实验性功能 + +### 1. ignoreTsError +在构建时忽略ts error,不中断构建过程。 +```yaml +experimentalFeatures: + ignoreTsError: true +``` + + +### 2. removeUselessFiles +在构建时移除大量无效文件,例如 `LICENSE`、`*.ts.map`、`**/test/` 等文件,可以有效减少构建包尺寸。 +```yaml +experimentalFeatures: + removeUselessFiles: true +``` + + + +### 3. fastInstallNodeModules +在构建时从当前的 devDependencies 中挑选出 production 依赖进行发布,可能会显著提升发布速度。 + +```yaml +experimentalFeatures: + fastInstallNodeModules: true +``` + + + +## CLI 扩展 + +### 1. 生命周期扩展 + +用户可以在 `package.json` 中添加 `midway-integration` 字段来根据各个命令的生命周期扩展 cli 的行为。 + +比如,在 package 命令 `installDevDep` 的后面添加自定义逻辑: + + +```bash +{ + "midway-integration": { + "lifecycle": { + "after:package:installDevDep": "npm run build" + } + } +} +``` + +其中 `lifecycle` 的格式为 `${ 'before' | 'after' | '' }:${ 命令 }:${ 命令生命周期 }` 。 + +package命令的声明周期列表: + +```bash + 'cleanup', // 清理构建目录 + 'installDevDep', // 安装开发期依赖 + 'copyFile', // 拷贝文件: package.include 和 shared content + 'compile', // + 'emit', // 编译函数 'package:after:tscompile' + 'analysisCode', // 分析代码 + 'copyStaticFile', // 拷贝src中的静态文件到dist目录,例如 html 等 + 'checkAggregation', // 检测高密度部署 + 'generateSpec', // 生成对应平台的描述文件,例如 serverless.yml 等 + 'generateEntry', // 生成对应平台的入口文件 + 'installLayer', // 安装layer + 'installDep', // 安装依赖 + 'package', // 函数打包 + 'finalize', // 完成 +``` + + + + +### 2. 通过插件进行扩展 + +用户可以自己编写 cli 插件,通过插件来实现更为复杂的 cli 的行为,也可以添加自定义命令。 +目前支持两种插件: + +- npm 插件,插件是一个npm包 +- local 插件,插件在本地位置 +- + + +通过在 f.yml 文件中配置 `plugins` 字段使 cli 加载插件: + +```yaml +plugins: + - npm::test-plugin-model + - local::./test/plugin +``` + +plugin 配置格式为: `${ 'npm' | 'local' }:${ provider || '' }:${ pluginName || path }` + +插件的代码参考: + +```typescript +// src/index.ts + +import { BasePlugin } from '@midwayjs/command-core'; + +export class TestLalalaPlugin extends BasePlugin { + commands = { + lalala: { + usage: '自定义命令', + lifecycleEvents: [ + 'a', // 自定义生命周期 + 'b', + ], + // 暂无 + options: { + name: { + usage: '参数 name, 例如: mw lalala --name=123', + shortcut: 'n', // 参数缩写 + }, + }, + }, + }; + + hooks = { + // 添加当前插件内的命令生命周期扩展 + // lalala 命令的 a 生命周期 + 'lalala:a': async () => { + + // 输出 + this.core.cli.log('lalala command hook'); + + // 获取用户输入的参数 + this.core.cli.log(this.core.options); + + // f.yml 内容 + this.core.cli.log(this.core.service); + + // 仅在 -V 参数下输出的内容 + this.core.debug('lalala'); + }, + + // 添加其他插件内的命令生命周期扩展 + // 在 package 命令的 copyFile 生命周期 “之前” 执行 + 'before:package:copyFile': async () => { + console.log('package command hook'); + }, + + }; +} +``` diff --git a/site/versioned_docs/version-3.0.0/tool/create_midway.md b/site/versioned_docs/version-3.0.0/tool/create_midway.md new file mode 100644 index 000000000000..d69c09744705 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/tool/create_midway.md @@ -0,0 +1,171 @@ +# 脚手架 + +Midway 编写了 `create-midway` 包,通过 npx 命令,可以方便的使用 `npm init midway` 命令创建脚手架。 + +```bash +$ npm init midway@latest -y +``` + +:::tip + +如果不加 @latest 的 tag,可能不会更新到最新版本。 + +::: + + + +## 通过 CLI 创建脚手架 + + + +### 默认行为 + +不传递参数,可以列出当前最常用的模版列表。 + +比如执行 + +```bash +$ npm init midway@latest -y +``` + +则会输出 + +```bash +➜ ~ npm init midway +? Hello, traveller. + Which template do you like? … + + ⊙ v3 +❯ koa-v3 - A web application boilerplate with midway v3(koa) + egg-v3 - A web application boilerplate with midway v3(egg 2.0) + faas-v3 - A serverless application boilerplate with midway v3(faas) + component-v3 - A midway component boilerplate for v3 + quick-start - A midway quickstart exmaple for v3 + + ⊙ v3-esm + koa-v3-esm - A web application boilerplate with midway v3(koa) + + ⊙ v2 + web - A web application boilerplate with midway and Egg.js + koa - A web application boilerplate with midway and koa +``` + +该模式下,会根据用户选择,按照指引创建模版。 + + + +### 关于参数传递 + +由于 `npm init midway` 等价与 `npm exec create-midway`,根据不同的 npm 版本,[传递参数](https://docs.npmjs.com/cli/v10/commands/npm-exec) 的格式不同。 + +比如在最新的 npm 中,使用额外的 `--` 传递参数。 + +比如 + +```bash +$ npm init midway -- -h +``` + +`-h` 参数可以显式所有的可用选项。 + +下面所有的参数示例,都将以这个模式展示。 + + + +### 显式所有模版 + +非当前版本的模版,会默认隐藏,可以通过 `-a` 参数展示所有内置的模版。 + +```bash +$ npm init midway -- -a +``` + + + +### 指定模版名 + +每个模版都有一个模版名和模版描述,比如 `koa-v3 - A web application boilerplate with midway v3(koa)` 的模板名为 `koa-v3`。 + +可以通过 `--type` 参数指定模板名。 + +```bash +$ npm init midway -- --type=koa-v3 +``` + + + +### 指定模版包名 + +当自定义模版在 npm 上发布时,我们可以使用 `-t` 或者 `--template` 来指定包名。 + +```bash +$ npm init midway -- -t=custom-template +``` + +如果包还在本地开发,也可以指定一个相对路径或者绝对路径。 + +```bash +$ npm init midway -- -t=./custom-template +``` + + + +### 指定创建目标目录 + +通过 `--target` 参数可以指定创建的目录,必须和 `type` 或者 `template` 参数一同使用。 + +比如,下面的命令指定了 `koa-v3` 模版,将其生成到当前 abc 目录下,如果目录不存在,则会新建。 + +```bash +$ npm init midway -- --type=koa-v3 --target=abc +``` + +一般 `target` 可以省略,把路径放到最后一个参数即可。 + +```bash +$ npm init midway -- --type=koa-v3 abc +``` + + + +### 指定客户端 + +如果有私有客户端,可以使用 `--npm` 指定客户端。 + +```bash +$ npm init midway -- --npm=tnpm +``` + + + +### 指定源 + +如果有私有源,可以使用 `--registry` 指定私有源。 + +```bash +$ npm init midway -- --registry=https://registry.npmmirror.com +``` + + + +### 脚手架参数 + +如果脚手架中包含用户可传递的参数,也可以通过命令行传递。 + +```bash +$ npm init midway -- --bbb=ccc +``` + +如果参数名和工具的参数重复了,可以使用 `t_` 的参数,在工具传递给脚手架时,会自动处理。 + +```bash +$ npm init midway -- --type=koa-v3 --t_type=ccc +``` + + + +## 编写脚手架 + +Midway 脚手架使用了自研的 light-generator 工具,具体的使用可以参考 [https://github.com/midwayjs/light-generator](https://github.com/midwayjs/light-generator)。 + +也可以参考 Midway 自己的 [模版工程](https://github.com/midwayjs/midway-boilerplate/tree/master/v3)。 \ No newline at end of file diff --git a/site/versioned_docs/version-3.0.0/tool/egg-ts-helper.md b/site/versioned_docs/version-3.0.0/tool/egg-ts-helper.md new file mode 100644 index 000000000000..c673dbf2f840 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/tool/egg-ts-helper.md @@ -0,0 +1,46 @@ +# egg:ts-helper + +针对 midway 支持 Egg.js 的场景,重写了原 [egg-ts-helper](https://github.com/whxaxes/egg-ts-helper) 包,移除了原有的 TS,AST 分析等大依赖。 + +原来的包依赖的 ts v3 环境,依赖 egg 的目录结构,考虑非常多的可能性,在 midway 的场景中不会使用到。基于上述考虑 midway 将此包进行了重写,用最简单的方式提供 egg 定义。 + +[@midwayjs/egg-ts-helper](https://github.com/midwayjs/egg-ts-helper) 包提供 `ets` 全局命令。 + +```bash +$ npm i @midwayjs/egg-ts-helper --save-dev +$ ets +``` + +一般我们会在开发命令里加入。 + +```json + "scripts": { + "dev": "cross-env ets && cross-env NODE_ENV=local midway-bin dev --ts", + }, +``` + +:::info +此包是针对 midway 定制的,只能用于新版本 midway 及其配套代码。 +::: + +最终会在项目根目录生成 `typings` 目录,其定义结构和文件如下: + +``` +. +├── ... +└── typings + ├── extend + │ ├── request.d.ts + │ ├── response.d.ts + │ ├── application.d.ts + │ └── context.d.ts + ├── app + │ └── index.d.ts + └── config + ├── index.d.ts + └── plugin.d.ts +``` + +:::caution +注意,该模块只是将 midway v2(Egg.js)的框架 + 插件定义聚合到一起,让当前的业务代码能够顺利的读取到框架和插件的定义,不支持生成业务代码本身的定义,也不支持在开发 egg 插件时生成定义。 +::: diff --git a/site/versioned_docs/version-3.0.0/tool/luckyeye.md b/site/versioned_docs/version-3.0.0/tool/luckyeye.md new file mode 100644 index 000000000000..296cb10edb59 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/tool/luckyeye.md @@ -0,0 +1,52 @@ +# 规则检查工具 + +Midway 为常见的错误提供了一些检查工具,以方便用户快速排错。`@midwayjs/luckyeye` 包提供了一些基础的检查规则,配合 Midway 新版本可以快速排查问题。 + +> luckyeye,寓意为幸运眼,能快速发现和定位问题。 + +## 使用 + +首先安装 `@midwayjs/luckyeye` 包。 + +```bash +npm i @midwayjs/luckyeye --save-dev +``` + +一般情况下,我们会将它加入到一个检查脚本中,比如: + +```json +"scripts": { + // ...... + "check": "luckyeye" +}, +``` + +接下去,我们需要配置“规则包”,比如 `midway_v2` 就是针对 midway v2 版本的规则检查包。 + +在 `package.json` 中加入下面的段落。 + +```json +"midway-luckyeye": { + "packages": [ + "midway_v2" + ] +}, +``` + +## 执行 + +配置完后,可以执行上面添加的检查脚本。 + +```bash +npm run check +``` + +**蓝色**代表输出的信息,用于排错,**绿色**代表检查项通过,**红色**代表检查项有问题,需要修改,**黄色**代表检查项可以做修改,但是可选。 + +执行效果如下。 + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1610983986151-79c54e7c-3ff0-4f94-98bc-359dda0fa694.png) + +## 自定义规则包 + +请参考 [https://github.com/midwayjs/luckyeye](https://github.com/midwayjs/luckyeye) 的 README。 diff --git a/site/versioned_docs/version-3.0.0/tool/mwts.md b/site/versioned_docs/version-3.0.0/tool/mwts.md new file mode 100644 index 000000000000..ea8a67e23526 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/tool/mwts.md @@ -0,0 +1,85 @@ +# Lint 和格式化 + +Midway 的框架和业务代码都是由 TypeScript 编写的,默认 Midway 提供了一套默认的 lint、编辑器以及格式化规则,用于更方便的进行开发和测试。 + +## 代码风格库 + +Midway 的代码风格库叫 [mwts](https://github.com/midwayjs/mwts),源自于 Google 的 [gts](https://github.com/google/gts)。mwts 是 Midway 的 TypeScript 样式指南,也是格式化程序,linter 和自动代码修复程序的配置。 + +:::info +在 midway 项目中,我们会默认添加 mwts,下面的流程只是为了说明如何使用 mwts。 +::: + +为了使用 mwts,我们需要把它添加到开发依赖中。 + +```json +"devDependencies": { + "mwts": "^1.0.5", + "typescript": "^4.0.0" +}, +``` + +## ESLint 配置 + +mwts 提供了一套默认的 ESLint 配置(TSLint 已经废弃,合并到了 ESLint 中)。 + +在项目根目录创建 `.eslintrc.json` 文件,内容如下(一般脚手架会自带): + +```json +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "interface.ts"], + "env": { + "jest": true + } +} +``` + +上面是 midway 项目的默认配置,其他项目 `ignorePatterns` 和 `env` 可以自行根据 ESLint 自行调整。 + +整个 mwts 的默认规则请参考 [这里](https://github.com/midwayjs/mwts/blob/master/.eslintrc.json),如有需求,可以自行调整。 + +## 执行代码检查和格式化 + +可以通过执行 `mwts check` 命令和 `mwts fix` 命令,来检查代码。比如在项目中增加脚本命令(一般脚手架会自带)。 + +```typescript + "scripts": { + "lint": "mwts check", + "lint:fix": "mwts fix", + }, +``` + +## Prettier 配置 + +mwts 提供了一套默认的 prettier 配置,创建一个 `.prettierrc.js` 文件,配置内容如下即可(一般脚手架自带)。 + +```javascript +module.exports = { + ...require('mwts/.prettierrc.json'), +}; +``` + +## 配置保存自动格式化 + +我们以 VSCode 为例。 + +第一步,安装 Prettier 插件。 + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1618042429530-177c3636-aefc-419d-8d3a-5258cad13631.png) + +打开配置,搜索 “save”,找到右侧的 "Format On Save",勾选即可。 + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1618042494782-71b6cc3c-18ae-4344-987b-ec82084f2dd8.png) + +如果保存文件没有效果,一般是编辑器有多个格式化方式,可以右键进行默认选择。 + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1618125271116-845e8452-0f7b-46a9-a28a-388f2db9c5e3.png) + +选择 “配置默认格式化程序”。 + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1618125381302-d3fe30c1-e56d-43f8-ada2-6e315f4ff2c4.png) + +选择 Prettier 即可。 + +![](https://cdn.nlark.com/yuque/0/2021/png/501408/1618125423564-8e46b0f8-f422-4e3d-a805-3b0a1db037f8.png) diff --git a/site/versioned_docs/version-3.0.0/tool/mwtsc.md b/site/versioned_docs/version-3.0.0/tool/mwtsc.md new file mode 100644 index 000000000000..85918efedaad --- /dev/null +++ b/site/versioned_docs/version-3.0.0/tool/mwtsc.md @@ -0,0 +1,112 @@ +# 开发工具 + +基于标准的 tsc 模块,midway 开发了一个简单的工具,用于本地开发和构建 ts 文件。 + +它的使用和标准 tsc 几乎一致。 + +```bash +$ npx mwtsc +``` + +等价于执行 `tsc` 命令。 + + + +## 常用命令 + +由于 mwtsc 基于 tsc 进行开发,它可以使用所有 tsc 的命令。 + +比如 + +```bash +# 监听模式 +$ npx mwtsc --watch + +# 使用不同的 tsconfig 文件 +$ npx mwtsc --project tsconfig.production.json +``` + +更多的参数可以查询 [tsc cli 工具](https://www.typescriptlang.org/docs/handbook/compiler-options.html)。 + +下面介绍更多 midway 新增的参数。 + + + +## 运行指令 + +为了使得 tsc 在代码开发期生效,midway 提供了一个 `run` 参数,用于在 tsc 编译成功后执行一个文件,这和 `tsc-watch` 模块类似。 + +比如 + +```bash +$ mwtsc --watch --run @midwayjs/mock/app.js +``` + +上述命令会执行下面的逻辑: + +* 1、编译代码,编译成功后执行 `@midwayjs/mock/app.js` 文件 +* 2、如果修改代码,则会自动触发编译,杀掉上一次执行的文件之后,自动执行 `@midwayjs/mock/app.js` 文件 + +`run` 参数可以执行任意的 js 文件,midway 依靠这个参数本地开发。 + +比如 + +```bash +$ npx mwtsc --watch --run ./bootstrap.js +``` + +当然也可以配合其他参数一起使用。 + +```bash +$ npx mwtsc --watch --project tsconfig.production.json --run ./bootstrap.js +``` + +注意 `run` 命令必须放在最后,它之后的所有参数,都将传递给子进程。 + + + +## 框架配置 + +可以在 Midway 项目中用 mwtsc 进行开发测试,比如: + +```json +{ + "scripts": { + "dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app", + "build": "cross-env rm -rf dist && tsc" + }, +} +``` + +这里的 `@midwayjs/mock/app` 指代的是 `@midwayjs/mock` 包中的 `app.js` 文件,这个文件用于本地开发时启动框架。针对 Serverless 环境,也有相应的启动文件。 + +```json +{ + "scripts": { + "dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/function", + }, +} +``` + + + +## 常用能力 + +### 调整端口 + +可以通过参数 `--port` 动态修改启动的 http 端口,这个参数的优先级高于代码中的端口配置。 + +```bash +$ npx mwtsc --watch --run @midwayjs/mock/app --port 7001 +``` + + + +### 开启 https + +框架内置了一个 https 证书用来本地测试,可以通过参数 `--ssl` 启用。 + +```bash +$ npx mwtsc --watch --run @midwayjs/mock/app --ssl +``` + diff --git a/site/versioned_docs/version-3.0.0/tool/sequelize_generator.md b/site/versioned_docs/version-3.0.0/tool/sequelize_generator.md new file mode 100644 index 000000000000..52e8d362a857 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/tool/sequelize_generator.md @@ -0,0 +1,203 @@ +# sequelize-auto-midway + +forked from [sequelize/sequelize-auto](https://github.com/sequelize/sequelize-auto) + +通过已存在的数据库生成用于 `Midway` 的 `Sequelize` 实体。 + +其他详细文档和用法请参考 [sequelize/sequelize-auto](https://github.com/sequelize/sequelize-auto) + +## Installation + +```bash +$ npm i sequelize-auto-midway +``` + +## Usage + +```bash +# 推荐 +# 请替换配置信息 +npx sequelize-auto-midway -h localhost -d yourDBname -u root -x yourPassword -p 13306 --dialect mysql -o ./models --noInitModels true --caseModel c --caseProp c --caseFile c --indentation 1 -a ./additional.json +``` + +additional.json + +```json +{ + "timestamps": true, + "paranoid": true +} +``` + +自动生成的模板文件如下: + +```ts +import { Column, DataType, Table, Model } from 'sequelize-typescript'; + +@Table({ + tableName: 'task', + timestamps: false, + indexes: [ + { + name: 'PRIMARY', + unique: true, + using: 'BTREE', + fields: [{ name: 'task_id' }], + }, + ], +}) +export class TaskEntity extends Model { + @Column({ + autoIncrement: true, + type: DataType.INTEGER.UNSIGNED, + allowNull: false, + primaryKey: true, + field: 'task_id', + }) + taskId: number; + + @Column({ + type: DataType.TINYINT.UNSIGNED, + allowNull: false, + defaultValue: 0, + comment: '任务所属应用ID: 0-无所属', + field: 'app_id', + }) + appId: number; + + @Column({ + type: DataType.STRING(64), + allowNull: false, + comment: '任务名称', + field: 'task_name', + }) + taskName: string; + + @Column({ + type: DataType.TINYINT.UNSIGNED, + allowNull: false, + defaultValue: 0, + comment: '任务类别:1-cron,2-interval', + }) + type: number; + + @Column({ + type: DataType.TINYINT.UNSIGNED, + allowNull: false, + defaultValue: 0, + comment: '任务状态:0-暂停中,1-启动中', + }) + status: number; + + @Column({ + type: DataType.DATE, + allowNull: true, + comment: '任务开始时间', + field: 'start_time', + }) + startTime: string; + + @Column({ + type: DataType.DATE, + allowNull: true, + comment: '任务结束时间', + field: 'end_time', + }) + endTime: string; + + @Column({ + type: DataType.INTEGER, + allowNull: false, + defaultValue: -1, + comment: '任务执行次数', + }) + limit: number; + + @Column({ + type: DataType.STRING(128), + allowNull: true, + defaultValue: '', + comment: '任务cron配置', + }) + cron: string; + + @Column({ + type: DataType.INTEGER.UNSIGNED, + allowNull: true, + defaultValue: 0, + comment: '任务执行间隔时间', + }) + every: number; + + @Column({ + type: DataType.STRING(255), + allowNull: true, + comment: '参数', + }) + args: string; + + @Column({ + type: DataType.STRING(255), + allowNull: true, + comment: '备注', + }) + remark: string; +} +``` + +Use `npx sequelize-auto-midway --help` to see all available parameters with their descriptions. Some basic parameters below: + +```bash +Usage: npx sequelize-auto-midway -h -d -p [port] -u -x +[password] -e [engine] + +Options: + --help Show help [boolean] + --version Show version number [boolean] +-h, --host IP/Hostname for the database. [string] +-d, --database Database name. [string] +-u, --user Username for database. [string] +-x, --pass Password for database. If specified without providing + a password, it will be requested interactively from + the terminal. +-p, --port Port number for database (not for sqlite). Ex: + MySQL/MariaDB: 3306, Postgres: 5432, MSSQL: 1433 + [number] +-c, --config Path to JSON file for Sequelize-Auto options and + Sequelize's constructor "options" flag object as + defined here: + https://sequelize.org/master/class/lib/sequelize.js~Sequelize.html#instance-constructor-constructor + [string] +-o, --output What directory to place the models. [string] +-e, --dialect The dialect/engine that you're using: postgres, + mysql, sqlite, mssql [string] +-a, --additional Path to JSON file containing model options (for all + tables). See the options: https://sequelize.org/master/class/lib/model.js~Model.html#static-method-init + [string] + --indentation Number of spaces to indent [number] +-t, --tables Space-separated names of tables to import [array] +-T, --skipTables Space-separated names of tables to skip [array] +--caseModel, --cm Set case of model names: c|l|o|p|u + c = camelCase + l = lower_case + o = original (default) + p = PascalCase + u = UPPER_CASE +--caseProp, --cp Set case of property names: c|l|o|p|u +--caseFile, --cf Set case of file names: c|l|o|p|u|k + k = kebab-case +--noAlias Avoid creating alias `as` property in relations + [boolean] +--noInitModels Prevent writing the init-models file [boolean] +-n, --noWrite Prevent writing the models to disk [boolean] +-s, --schema Database schema from which to retrieve tables[string] +-v, --views Include database views in generated models [boolean] +-l, --lang Language for Model output: es5|es6|esm|ts + es5 = ES5 CJS modules (default) + es6 = ES6 CJS modules + esm = ES6 ESM modules + ts = TypeScript [string] +--useDefine Use `sequelize.define` instead of `init` for es6|esm|ts +--singularize, --sg Singularize model and file names from plural table + names +``` diff --git a/site/versioned_docs/version-3.0.0/tool/typeorm_generator.md b/site/versioned_docs/version-3.0.0/tool/typeorm_generator.md new file mode 100644 index 000000000000..c01d6fc5e205 --- /dev/null +++ b/site/versioned_docs/version-3.0.0/tool/typeorm_generator.md @@ -0,0 +1,50 @@ +# typeorm:Model Generator + +感谢社区用户 @youtiao66 提供此模块。 + + +通过该工具,你可以快速创建 for Midway 的 TypeORM Model。 + + +## 使用 + +比如生成 mysql 的 model。 + +```bash +# 推荐 +# 请替换配置信息 +$ npx mdl-gen-midway -h localhost -p 3306 -d yourdbname -u root -x yourpassword -e mysql --noConfig --case-property none +``` + +完整参数: + +``` +Usage: npx mdl-gen-midway -h -d -p [port] -u -x +[password] -e [engine] + +Options: + --help Show help [boolean] + --version Show version number [boolean] + -h, --host IP address/Hostname for database server + [default: "127.0.0.1"] + -d, --database Database name(or path for sqlite) [required] + -u, --user Username for database server + -x, --pass Password for database server [default: ""] + -p, --port Port number for database server + -e, --engine Database engine + [choices: "mssql", "postgres", "mysql", "mariadb", "oracle", "sqlite"] + [default: "mssql"] + -o, --output Where to place generated models + [default: "./output"] + -s, --schema Schema name to create model from. Only for mssql + and postgres. You can pass multiple values + separated by comma eg. -s scheme1,scheme2,scheme3 + --ssl [boolean] [default: false] + + --noConfig Doesn't create tsconfig.json and + ormconfig.json [布尔] [默认值: false] + + --cp, --case-property Convert property names to specified case + [可选值: "pascal", "camel", "snake", "none"] [默认值: "camel"] + +``` diff --git a/site/versioned_docs/version-3.0.0/tool/version_check.md b/site/versioned_docs/version-3.0.0/tool/version_check.md new file mode 100644 index 000000000000..1b58795df69c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/tool/version_check.md @@ -0,0 +1,168 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 版本检查工具 + +由于依赖安装版本的不确定性,Midway 提供了 `midway-version` 这一版本检查工具,可以快速检查版本之间的兼容性错误。 + + + +## 检查兼容性 + +你可以使用下面的命令在项目根目录执行进行检查。 + +以下命令会检查 `node_modules` 中实际安装的版本,而非 `package.json` 中写的版本。 + + + + + +```bash +$ npx midway-version@latest +``` + + + + +```bash +$ pnpx midway-version@latest +``` + + + + + +```bash +$ yarn add midway-version@latest +$ yarn midway-version +``` + + + + + + + +## 升级到最新版本 + +你可以使用下面的命令在项目根目录执行进行升级。 + +`-u` 参数会检查 midway 所有模块,根据 `node_modules` 中实际安装的版本以及 `package.json` 中编写的版本,将其升级到 `最新` 版本。 + +如当前安装的组件版本为 `3.16.2`,最新版本为 `3.18.0` ,则会提示升级到 `3.18.0`。 + +在使用 `-u -w` 参数时: + +* 更新 `package.json` 的版本,保留前缀写法,比如 `^3.16.0` 会变为 `^3.18.0` +* 将 `3.18.0` 版本写入到锁文件(如有) + + + + + +```bash +$ npx midway-version@latest -u +``` + +输出确认无误后,可以使用 `-w` 参数写入 `package.json` 和 `package-lock.json` 文件(如有)。 + +```bash +$ npx midway-version@latest -u -w +``` + + + + +```bash +$ pnpx midway-version@latest -u +``` + +输出确认无误后,可以使用 `-w` 参数写入 `package.json` 和 `pnpm-lock.yaml` 文件(如有)。 + +```bash +$ pnpx midway-version@latest -u -w +``` + + + + + +```bash +$ yarn add midway-version@latest +$ yarn midway-version -u +``` + +输出确认无误后,可以使用 `-w` 参数写入 `package.json` 和 `yarn.lock` 文件(如有)。 + +```bash +$ yarn midway-version -u -w +``` + + + + + + + +## 升级到可兼容的最新版本 + +`-m` 参数会检查 midway 所有模块,根据 `node_modules` 中实际安装的版本以及 `package.json` 中编写的版本,将其升级到 `最新的兼容` 版本。 + +如当前安装的组件版本为 `3.16.0`,最新版本为 `3.18.0` ,兼容版本为 `3.16.1` 和 `3.16.2`,则会提示升级到 `3.16.2`。 + +一般使用 `-m` 参数的场景为固化低版本,检查错误的组件版本,所以策略和 `-u` 有所不同。 + +在使用 `-m -w` 参数时: + +* 更新 `package.json` 的版本 + * 如果有锁文件,将会保留前缀,比如 `^3.16.0` 会变为 `^3.16.2` + * 如果没有锁文件,将会移除前缀,固定版本,比如 `^3.16.0` 会变为 `3.16.2` + +* 将 `3.16.2` 版本写入到锁文件(如有) + + + + + +```bash +$ npx midway-version@latest -m +``` + +输出确认无误后,可以使用 `-w` 参数写入 `package.json` 和 `package-lock.json` 文件(如有)。 + +```bash +$ npx midway-version@latest -m -w +``` + + + + +```bash +$ pnpx midway-version@latest -m +``` + +输出确认无误后,可以使用 `-w` 参数写入 `package.json` 和 `pnpm-lock.yaml` 文件(如有)。 + +```bash +$ pnpx midway-version@latest -m -w +``` + + + + + +```bash +$ yarn add midway-version@latest +$ yarn midway-version -m +``` + +输出确认无误后,可以使用 `-w` 参数写入 `package.json` 和 `yarn.lock` 文件(如有)。 + +```bash +$ yarn midway-version -m -w +``` + + + + + diff --git a/site/versioned_docs/version-3.0.0/upgrade_v3.md b/site/versioned_docs/version-3.0.0/upgrade_v3.md new file mode 100644 index 000000000000..9e89143b691c --- /dev/null +++ b/site/versioned_docs/version-3.0.0/upgrade_v3.md @@ -0,0 +1,541 @@ +# 2.x 升级指南 + +本篇将介绍从 midway v2 升级为 midway v3 的方式。 + +从 Midway v2 升级到 Midway v3,会有一些 Breaking Change。本篇文档会详细列出这些 Breaking 的地方,让用户可以提前知道变化,做出应对。 + + + +## 自动升级工具 + +**在升级前,请切出一个新的分支,避免升级失败导致无法恢复!!!** + +拷贝以下脚本,在项目根目录执行: + +```bash +$ npx --ignore-existing midway-upgrade +``` + +:::tip + +由于业务情况各异,请在脚本升级之后,再进行手动升级的核对。 + +::: + + + +## 手动升级 + +**midway v3 支持从 node v12 起。** + + +### 包版本更新 + +所有的组件包,核心包都将升级为 3.x 版本。 + +```json +{ + "dependencies": { + "@midwayjs/bootstrap": "^3.0.0", + "@midwayjs/core": "^3.0.0", + "@midwayjs/decorator": "^3.0.0", + "@midwayjs/koa": "^3.0.0", + "@midwayjs/task": "^3.0.0", + }, + "devDependencies": { + "@midwayjs/cli": "^1.2.90", + "@midwayjs/luckyeye": "^1.0.0", + "@midwayjs/mock": "^3.0.0", + // ... + } +} + +``` + +`@midwayjs/cli` 和 `@midwyajs/luckeye`, `@midwayjs/logger` 的版本除外。 + + + +### Query/Body/Param/Header 装饰器变更 + + +主要是默认无参数下的行为。 + + +旧 +```typescript +async invoke(@Query() name) { + // ctx.query.name +} +``` +新 +```typescript +async invoke(@Query() name) { + // ctx.query +} + +async invoke(@Query('name') name) { + // ctx.query.name +} +``` + + + +### Validate/Rule 装饰器 + + +旧 +```typescript +import { Validate, Rule, RuleType } from '@midwayjs/decorator'; +``` +新 +```typescript +import { Validate, Rule, RuleType } from '@midwayjs/validate'; +``` +由于 validate 抽象成了组件,需要在代码中安装依赖并开启。 +```typescript +// src/configuration +import * as validate from '@midwayjs/validate'; + +@Configuration({ + // ... + imports: [ + validate + ], +}) +export class MainConfiguration { + // ... +} + +``` + +### task 组件配置 key 变更 + +旧 + +```typescript +export const taskConfig = {}; +``` + +新 + +```typescript +export const task = {}; +``` + + + +### 配置的绝对路径 + + +不再支持相对路径 + + +旧 +```typescript +// src/configuration + +@Configuration({ + // ... + importConfigs: [ + './config' // ok + ] +}) +export class MainConfiguration { + // ... +} + +``` +新 +```diff +// src/configuration +import { join } from 'path'; + +@Configuration({ + // ... + importConfigs: [ +- './config' // error ++ join(__dirname, './config') // ok + ] +}) +export class MainConfiguration { + // ... +} + +``` + + + +### 使用默认框架/多框架 + + +旧,会在 bootstrap.js 中引入 +```typescript +const WebFramework = require('@midwayjs/koa').Framework; +const GRPCFramework = require('@midwayjs/grpc').Framework; +const { Bootstrap } = require('@midwayjs/bootstrap'); + +Bootstrap + .load(config => { + return new WebFramework().configure(config.cluster); + }) + .load(config => { + return new GRPCFramemwork().configure(config.grpcServer); + }) + .run(); +``` + + +新版本 + + +bootstrap.js 中不再需要单独实例化 +```typescript +const { Bootstrap } = require('@midwayjs/bootstrap'); +Bootstrap.run(); +``` +作为替代,以组件的形式引入 +```typescript +// src/configuration +import * as web from '@midwayjs/web'; +import * as grpc from '@midwayjs/grpc'; + +@Configuration({ + // ... + imports: [ + web, + grpc, + //... + ], +}) +export class MainConfiguration { + // ... +} + +``` + + +其他影响: + + + +- 1、测试中不再需要使用 createBootstrap 方法从 bootstrap.js 启动 +- 2、原有入口 Framework 的配置现在可以放到 config.*.ts 中,以框架名作为 key + + + +### 删除了一批 IoC 容器 API + + +移除 container 上的下列方法 + + +- getConfigService(): IConfigService; +- getEnvironmentService(): IEnvironmentService; +- getInformationService(): IInformationService; +- setInformationService(service: IInformationService): void; +- getAspectService(): IAspectService; +- getCurrentEnv(): string; + + +现在都有相应的框架内置服务来替代。 + + +比如旧写法: +```typescript +const environmentService = app.getApplicationContext().getEnvironmentService(); +const env = environmentService.getCurrentEnvironment(); +``` + + +新写法 +```typescript +const environmentService = app.getApplicationContext().get(MidwayEnvironmentService) +const env = environmentService.getCurrentEnvironment(); +``` + + + +## @midwayjs/web(egg)部分 + +### 启动端口 + +新版本框架启动会读取一个端口配置,如果未配,可能不会启动端口监听。 + +```json +// src/config/config.default +export default { + // ... + egg: { + port: 7001, + }, +} +``` + + + +### 添加 egg-mock + +由于框架移除了 egg-mock 包,在新版本 `package.json` 需要手动引用。 + +```json +{ + "devDependencies": { + "egg-mock": "^1.0.0", + // ... + } +} +``` + + + +### 日志 + +新版本,统一使用 @midwayjs/logger,不管是不是启用 egg logger。 + +为了和 egg 日志不冲突,我们使用了新的 key,原有的 `midwayFeature` 字段不再使用。 + +旧 + +```typescript +export const logger = { + level: 'warn', + consoleLevel: 'info' +} +``` + +新 + +```typescript +export const midwayLogger = { + default: { + level: 'warn', + consoleLevel: 'info' + } +} +``` + +Egg 的 `customLogger` 字段,针对无法修改的 egg 插件,我们做了兼容,对于业务代码,最好做修改。 + +```typescript +export const midwayLogger = { + default: { + level: 'warn', + consoleLevel: 'info' + }, + clients: { + // 自定义日志 + customLoggerA: { + // ... + } + } +} +``` + +其余的更具体配置,请参考 [日志章节 ](logger) 中的自定义部分。 + + + +### egg 插件 + +在 Midway3 中,为了文档和行为统一,我们关闭了大部分 egg 默认插件。 + +新版本默认插件如下: + +```javascript +module.exports = { + onerror: true, + security: true, + static: false, + development: false, + watcher: false, + multipart: false, + logrotator: false, + view: false, + schedule: false, + i18n: false, +} +``` + +请酌情开启(可能会和 midway 能力冲突)。 + +默认的 egg 日志切割插件(logrotator),由于日志不再支持 egg logger,我们在框架中直接关闭了(midway logger 自带了切割)。 + + + +### 定时任务 + +如果希望使用老的 `@Schedule` 装饰器,需要额外安装 `midway-schedule` 包,并以 egg 插件的形式引入。 + +```typescript +// src/config/plugin.ts + +export default { + schedule: true, + schedulePlus: { + enable: true, + package: 'midway-schedule', + } + // ... +} +``` + + + + + +## 其他面对组件/框架开发者的调整 + + + +### 组件中 registerObject 不再增加 namespace + + +在组件开发时,不再增加 namespace 前缀。 + + +旧,组件入口 +```typescript +@Configuration({ + namespace: 'A' + // ... +}) +export class MainConfiguration { + + async onReady(container) { + container.registerObject('aaa', 'bbb'); + } +} + +container.getAsync('A:aaa'); // => OK +``` + + +新组件入口 +```typescript +@Configuration({ + namespace: 'A' + // ... +}) +export class MainConfiguration { + + async onReady(container) { + container.registerObject('aaa', 'bbb'); + } +} + +container.getAsync('aaa'); // => OK +``` + + + + +### 自定义框架部分 + + +自定义框架的变化比较大,框架组件化是这一版本的目标。有几个地方需要修改。 + + +**1、在原框架上增加 @Framework 标识** + + +旧 +```typescript +export class CustomKoaFramework extends BaseFramework { + // ... +} +``` +新 +```typescript +import { Framework } from '@midwayjs/core'; + +@Framework() +export class CustomKoaFramework extends BaseFramework { + // ... +} +``` + + +**2、在入口处按组件规范导出 Configuration** + + +你可以在 configuration 中使用生命周期,和组件相同。`run` 方法将在新增的 `onServerReady` 这个生命周期显式调用执行。 +```typescript +import { Configuration,Inject } from '@midwayjs/core'; +import { MidwayKoaFramework } from './framework'; + +@Configuration({ + namespace: 'koa', +}) +export class KoaConfiguration { + @Inject() + framework: MidwayKoaFramework; + + async onReady() {} + + async onServerReady() { + // ... + } +} + +``` + + +**3、框架开发时** + +**需要注意的是,由于框架初始化在用户生命周期前,所以 applicationInit 的时候,不要通过 @Config 装饰器注入配置,而是调用 configService 去获取。** + + +```typescript +import { Framework } from '@midwayjs/core'; + +@Framework() +export class CustomKoaFramework extends BaseFramework { + + configure() { + /** + * 这里返回你的配置 + * 返回的值会赋值到 this.configurationOptions,对接原来的用户显式入参 + * + */ + return this.configService.getConfiguration('xxxxxxx'); + } + + /** + * 这个新增的方法,用来判断框架是否加载 + * 有时候组件中包括 server 端(框架)和 client 端,就需要判断 + * + */ + isEnable(): boolean { + return this.configurationOptions.services?.length > 0; + } + + // ... +} +``` + +这样在外面使用时也可以判断。 + +```typescript +import { Configuration,Inject } from '@midwayjs/core'; +import { MidwayKoaFramework } from './framework'; + +@Configuration({ + namespace: 'koa', +}) +export class KoaConfiguration { + @Inject() + framework: MidwayKoaFramework; + + async onReady() {} + + async onServerReady() { + // 如果 isEnable 为 true,框架会默认调用 framework.run() + // 如果一开始 enable 为 false,也可以延后去手动 run + if (/* 延后执行 */) { + await this.framework.run(); + } + } +} + +``` diff --git a/site/versioned_sidebars/version-3.0.0-sidebars.json b/site/versioned_sidebars/version-3.0.0-sidebars.json new file mode 100644 index 000000000000..388d406a299f --- /dev/null +++ b/site/versioned_sidebars/version-3.0.0-sidebars.json @@ -0,0 +1,321 @@ +{ + "common": [ + { + "type": "category", + "label": "新手指南", + "collapsed": false, + "collapsible": false, + "items": [ + "intro", + "quick_guide", + "how_to_update_midway", + "upgrade_v3" + ] + }, + { + "type": "category", + "label": "基础", + "collapsed": true, + "collapsible": true, + "items": [ + "quickstart", + "controller", + "middleware", + "req_res_app", + "service", + "error_filter", + "pipe", + "guard", + "midway_component", + "deployment" + ] + }, + { + "type": "category", + "label": "进阶", + "collapsed": true, + "collapsible": true, + "items": [ + "container", + "testing", + "mock", + "debugger", + "environment", + "env_config", + "lifecycle", + "logger_v3", + "cookie_session", + "built_in_service", + "router_table", + "decorator_index", + "error_code", + "esm" + ] + }, + { + "type": "category", + "label": "设计模式", + "collapsed": true, + "collapsible": true, + "items": [ + "aspect", + "auto_run", + "pipeline", + "service_factory", + "data_listener", + "data_source", + "data_response", + "retry" + ] + }, + { + "type": "category", + "label": "自定义", + "collapsed": true, + "collapsible": true, + "items": [ + "context_definition", + "component_development", + "custom_decorator", + "custom_error", + "change_start_dir" + ] + }, + { + "type": "category", + "label": "Serverless", + "collapsed": true, + "collapsible": true, + "items": [ + [ + "serverless/serverless_intro", + "serverless/serverless_v2_upgrade_serverless_v3", + "serverless/serverless_dev", + "serverless/serverless_testing", + "serverless/serverless_context", + "serverless/serverless_error", + "serverless/aliyun_faas" + ] + ] + }, + { + "type": "category", + "label": "开发工具", + "collapsed": true, + "collapsible": true, + "items": [ + "tool/create_midway", + "tool/mwtsc", + "tool/version_check", + "tool/mwts", + "tool/luckyeye", + "tool/egg-ts-helper" + ] + }, + { + "type": "category", + "label": "常见问题", + "collapsed": true, + "collapsible": true, + "items": [ + "faq/framework_problem", + "faq/git_problem", + "faq/npm_problem", + "faq/ts_problem", + "faq/alias_path", + "how_to_install_nodejs", + "ops/ecs_start_err", + "midway_slow_problem" + ] + }, + { + "type": "category", + "label": "其他", + "collapsed": true, + "collapsible": true, + "items": [ + "release_schedule", + "contributing", + "awesome_midway" + ] + }, + { + "type": "category", + "label": "历史文档", + "collapsed": true, + "collapsible": true, + "items": [ + "logger", + "tool/cli", + { + "type": "category", + "label": "一体化", + "collapsed": true, + "collapsible": true, + "items": [ + { + "type": "doc", + "id": "hooks/intro", + "label": "介绍" + }, + { + "type": "category", + "label": "基础功能", + "collapsed": false, + "collapsible": false, + "items": [ + "hooks/api", + "hooks/builtin-hooks", + "hooks/validate", + "hooks/middleware", + "hooks/cors", + "hooks/component", + "hooks/prisma", + "hooks/test", + "hooks/config", + "hooks/file-route", + "hooks/safe", + "hooks/upload" + ] + }, + { + "type": "category", + "label": "一体化", + "collapsed": false, + "collapsible": false, + "items": [ + "hooks/fullstack", + "hooks/client" + ] + }, + { + "type": "category", + "label": "进阶", + "collapsed": false, + "collapsible": false, + "items": [ + "hooks/deploy" + ] + } + ] + } + ] + } + ], + "component": [ + { + "type": "category", + "label": "通用", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/axios", + "extensions/i18n", + "extensions/info", + "extensions/validate", + "extensions/swagger", + "extensions/bull", + "extensions/cron", + "extensions/jwt" + ] + }, + { + "type": "category", + "label": "Http 服务", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/koa", + "extensions/egg", + "extensions/express", + "extensions/security", + "extensions/render", + "extensions/busboy", + "extensions/passport", + "extensions/casbin", + "extensions/static_file", + "extensions/cross_domain", + "extensions/http-proxy", + "extensions/captcha", + "extensions/tenant" + ] + }, + { + "type": "category", + "label": "数据存储", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/orm", + "extensions/sequelize", + "extensions/redis", + "extensions/mongodb", + "extensions/caching", + "extensions/oss", + "extensions/cos", + "extensions/tablestore", + "extensions/mikro" + ] + }, + { + "type": "category", + "label": "微服务", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/grpc", + "extensions/consul", + "extensions/etcd" + ] + }, + { + "type": "category", + "label": "WebSocket", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/socketio", + "extensions/ws" + ] + }, + { + "type": "category", + "label": "消息", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/rabbitmq", + "extensions/kafka", + "extensions/mqtt" + ] + }, + { + "type": "category", + "label": "运维", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/otel", + "extensions/code_dye", + "extensions/pm2", + "extensions/cfork", + "extensions/alinode", + "extensions/prometheus" + ] + }, + { + "type": "category", + "label": "历史废弃组件", + "collapsed": false, + "collapsible": false, + "items": [ + "extensions/cache", + "extensions/upload", + "legacy/mongodb", + "legacy/sequelize", + "legacy/orm", + "legacy/task" + ] + } + ] +} diff --git a/site/versions.json b/site/versions.json index d2f159d5bbba..bec93a3f0a2a 100644 --- a/site/versions.json +++ b/site/versions.json @@ -1,4 +1,5 @@ [ + "3.0.0", "2.0.0", "1.0.0" ]