@ -0,0 +1,38 @@ |
|||||
|
# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app |
||||
|
|
||||
|
# Number of days of inactivity before a closed issue or pull request is locked |
||||
|
daysUntilLock: 30 |
||||
|
|
||||
|
# Skip issues and pull requests created before a given timestamp. Timestamp must |
||||
|
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable |
||||
|
skipCreatedBefore: false |
||||
|
|
||||
|
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable |
||||
|
exemptLabels: [] |
||||
|
|
||||
|
# Label to add before locking, such as `outdated`. Set to `false` to disable |
||||
|
lockLabel: false |
||||
|
|
||||
|
# Comment to post before locking. Set to `false` to disable |
||||
|
lockComment: > |
||||
|
This thread has been automatically locked since there has not been |
||||
|
any recent activity after it was closed. Please open a new issue for |
||||
|
related bugs. |
||||
|
|
||||
|
# Assign `resolved` as the reason for locking. Set to `false` to disable |
||||
|
setLockReason: true |
||||
|
|
||||
|
# Limit to only `issues` or `pulls` |
||||
|
# only: issues |
||||
|
|
||||
|
# Optionally, specify configuration settings just for `issues` or `pulls` |
||||
|
# issues: |
||||
|
# exemptLabels: |
||||
|
# - help-wanted |
||||
|
# lockLabel: outdated |
||||
|
|
||||
|
# pulls: |
||||
|
# daysUntilLock: 30 |
||||
|
|
||||
|
# Repository to extend settings from |
||||
|
# _extends: repo |
||||
@ -0,0 +1,15 @@ |
|||||
|
# Number of days of inactivity before an issue becomes stale |
||||
|
daysUntilStale: 60 |
||||
|
# Number of days of inactivity before a stale issue is closed |
||||
|
daysUntilClose: 7 |
||||
|
# Set to true to ignore issues in a milestone (defaults to false) |
||||
|
exemptMilestones: true |
||||
|
# Label to use when marking an issue as stale |
||||
|
staleLabel: inactive |
||||
|
# Comment to post when marking an issue as stale. Set to `false` to disable |
||||
|
markComment: > |
||||
|
This issue has been automatically marked as stale because it has not had |
||||
|
recent activity. It will be closed if no further activity occurs. Thank you |
||||
|
for your contributions. |
||||
|
# Comment to post when closing a stale issue. Set to `false` to disable |
||||
|
closeComment: false |
||||
@ -0,0 +1,30 @@ |
|||||
|
# Social/External Logins |
||||
|
|
||||
|
## ASP.NET Core MVC / Razor Pages UI |
||||
|
|
||||
|
The [Account Module](../Modules/Account.md) has already configured to handle social or external logins out of the box. You can follow the ASP.NET Core documentation to add a social/external login provider to your application. |
||||
|
|
||||
|
### Example: Facebook Authentication |
||||
|
|
||||
|
Follow the [ASP.NET Core Facebook integration document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to support the Facebook login for your application. |
||||
|
|
||||
|
#### Add the NuGet Package |
||||
|
|
||||
|
Add the [Microsoft.AspNetCore.Authentication.Facebook](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Facebook) package to your project. Based on your architecture, this can be `.Web`, `.IdentityServer` (for tiered setup) or `.Host` project. |
||||
|
|
||||
|
#### Configure the Provider |
||||
|
|
||||
|
Use the `.AddFacebook(...)` extension method in the `ConfigureServices` method of your [module](../Module-Development-Basics.md), to configure the client: |
||||
|
|
||||
|
````csharp |
||||
|
context.Services.AddAuthentication() |
||||
|
.AddFacebook(facebook => |
||||
|
{ |
||||
|
facebook.AppId = "..."; |
||||
|
facebook.AppSecret = "..."; |
||||
|
facebook.Scope.Add("email"); |
||||
|
facebook.Scope.Add("public_profile"); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
> It would be a better practice to use the `appsettings.json` or the ASP.NET Core User Secrets system to store your credentials, instead of a hard-coded value like that. Follow the [Microsoft's document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to learn the user secrets usage. |
||||
@ -0,0 +1,30 @@ |
|||||
|
# 社交/外部登录 |
||||
|
|
||||
|
## ASP.NET Core MVC / Razor Pages UI |
||||
|
|
||||
|
[帐户模块](../Modules/Account.md)已配置为开箱即用的处理社交或外部登录. 你可以按照ASP.NET Core文档向你的应用程序添加社交/外部登录提供程序. |
||||
|
|
||||
|
### 示例: Facebook 认证 |
||||
|
|
||||
|
按照[ASP.NET Core Facebook集成文档](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/social/facebook-logins)向你应用程序添加Facebook登录. |
||||
|
|
||||
|
#### 添加NuGet包 |
||||
|
|
||||
|
添加[Microsoft.AspNetCore.Authentication.Facebook]包到你的项目. 基于你的架构,可能是 `.Web`,`.IdentityServer`(对于分层启动)或 `.Host` 项目. |
||||
|
|
||||
|
#### 配置提供程序 |
||||
|
|
||||
|
在你模块的 `ConfigureServices` 方法中使用 `.AddFacebook(...)` 扩展方法来配置客户端: |
||||
|
|
||||
|
````csharp |
||||
|
context.Services.AddAuthentication() |
||||
|
.AddFacebook(facebook => |
||||
|
{ |
||||
|
facebook.AppId = "..."; |
||||
|
facebook.AppSecret = "..."; |
||||
|
facebook.Scope.Add("email"); |
||||
|
facebook.Scope.Add("public_profile"); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
> 最佳实践是使用 `appsettings.json` 或ASP.NET Core用户机密系统来存储你的凭据,而不是像这样硬编码值. 请参阅[微软](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins)文档了解如何使用用户机密. |
||||
@ -0,0 +1,70 @@ |
|||||
|
# React Native入门 |
||||
|
|
||||
|
ABP平台提供了[React Native](https://reactnative.dev/)模板用于开发移动应用程序. |
||||
|
|
||||
|
当你按照[入门文档](Getting-Started.md)中所述**创建新应用程序**时,解决方案默认将React Native应用程序包含在 `react-native` 文件夹中. |
||||
|
|
||||
|
## 配置你的本地IP地址 |
||||
|
|
||||
|
运行在Android模拟器或真机上的React Native应用程序无法连接到 `localhost` 上的后.要修复此问题,需要在本地IP上运行后端. |
||||
|
|
||||
|
{{ if Tiered == "No"}} |
||||
|
 |
||||
|
|
||||
|
* 打开 `.HttpApi.Host` 文件夹下的 `appsettings.json` 文件. 将 `SelfUrl` 和 `Authority` 属性的 `localhost` 替换为你本地的IP地址. |
||||
|
* 打开 `.HttpApi.Host/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. |
||||
|
|
||||
|
{{ else if Tiered == "Yes" }} |
||||
|
|
||||
|
 |
||||
|
|
||||
|
* 打开 `.IdentityServer` 文件夹下的 `appsettings.json` 文件. 将 `SelfUrl` 属性的 `localhost` 替换为你本地的IP地址. |
||||
|
* 打开 `.IdentityServer/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. |
||||
|
* 打开 `.HttpApi.Host` 文件夹下的 `appsettings.json` 文件. 将 `Authority` 属性的 `localhost` 替换为你本地的IP地址. |
||||
|
* 打开 `.HttpApi.Host/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
按照**运行HTTP API Host (服务端口)**那样运行后端. |
||||
|
|
||||
|
> React Native应用程序不信任自动生成的.NET HTTPS证书,你可以在开发期间使用HTTP. |
||||
|
|
||||
|
在 `react-native` 文件夹打开命令行终端,输入 `yarn` 命令(我们推荐使用[yarn](https://yarnpkg.com/)包管理, `npm install` 在大多数情况下也可以工作). |
||||
|
|
||||
|
```bash |
||||
|
yarn |
||||
|
``` |
||||
|
|
||||
|
* 打开 `react-nativer` 文件夹下的 `Environment.js` 文件. 将 `apiUrl` 和 `issuer` 属性的 `localhost` 替换为你本地的IP地址: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
{{ if Tiered == "Yes" }} |
||||
|
|
||||
|
> 确保 `issuer` 与正在运行的 `.IdentityServer` 项目匹配, `apiUrl` 与正在运行的 `.HttpApi.Host` 项目匹配. |
||||
|
|
||||
|
{{else}} |
||||
|
|
||||
|
> 确保 `issuer` 和 `apiUrl` 与正在运行的 `.HttpApi.Host` 项目匹配 |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
等到所有node模块加载成功, 执行 `yarn start` (或 `npm start`) 命令: |
||||
|
|
||||
|
```bash |
||||
|
yarn start |
||||
|
``` |
||||
|
|
||||
|
等待Expo CLI启动后Expo CLI在 `http://localhost:19002/` 地址要开管理页面. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
在上面的管理界面中,可以通过使用[Expo Client](https://expo.io/tools#client)扫描二维码,使用Android模拟器,iOS模拟器或真机来启动应用程序. |
||||
|
|
||||
|
> 请参阅expo.io上的[Android Studio模拟器](https://docs.expo.io/workflow/android-simulator/)和[iOS模拟器文档](https://docs.expo.io/workflow/ios-simulator/). |
||||
|
|
||||
|
 |
||||
|
|
||||
|
输入用户名 **admin**,密码 **1q2w3E*** 登录到应用程序. |
||||
|
|
||||
|
应用程序已经启动并执行,你可以基于该启动模板开发应用程序. |
||||
@ -1,3 +1,5 @@ |
|||||
# 身份管理模块 |
# 身份管理模块 |
||||
|
|
||||
|
身份模块基于Microsoft Identity 库用于管理[组织单元](Organization-Units.md), 角色, 用户和他们的权限. |
||||
|
|
||||
参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善. |
参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善. |
||||
@ -0,0 +1,47 @@ |
|||||
|
# 组织单元管理 |
||||
|
|
||||
|
组织单元(OU)是"身份模块"的一部分,用于**对用户和实体进行分层分组**. |
||||
|
|
||||
|
### OrganizationUnit 实体 |
||||
|
|
||||
|
OU由 **OrganizationUnit** 实体表示. 实体有以下基本属性: |
||||
|
|
||||
|
- **TenantId**: OU租户的ID,为null代表是宿主OU. |
||||
|
- **ParentId**: OU的父亲Id,为null为根OU. |
||||
|
- **Code**: 租户唯一的分层字符串代码. |
||||
|
- **DisplayName**: OU的显示名称. |
||||
|
|
||||
|
OrganizationUnit实体的主键(Id)是 **Guid** 类型,派生自[**FullAuditedAggregateRoot**](../Entities.md)类. |
||||
|
|
||||
|
#### Organization 树 |
||||
|
|
||||
|
因为OU可以有父亲,租户所有的OU是一个**树**结构. 树有一些规则: |
||||
|
|
||||
|
- 可以有多个根(`ParentId` 为 `null`). |
||||
|
- OU的第一级子级数有限制(面说明的固定OU代码单位长度). |
||||
|
|
||||
|
#### OU Code |
||||
|
|
||||
|
OU代码由OrganizationUnit Manager自动生成和维护. 看起来像这样的字符串: |
||||
|
|
||||
|
"**00001.00042.00005**" |
||||
|
|
||||
|
此代码可用于轻松查询数据库中OU的所有子级(递归). 代码有一些规则: |
||||
|
|
||||
|
- 必须[租户](../Multi-Tenancy.md)**唯一**的. |
||||
|
- 同一OU的所有子代均以**父OU的代码开头**. |
||||
|
- 它是**固定长度**的,并且基于树中OU的级别,如示例中所示. |
||||
|
- 虽然OU代码是唯一的,但是如果移动OU,它是**可更改的**. |
||||
|
- 你必须通过Id而不是代码引用OU. |
||||
|
|
||||
|
### OrganizationUnit Manager |
||||
|
|
||||
|
可以注入 **OrganizationUnitManager** 管理OU. 常见的用例有: |
||||
|
|
||||
|
- 创建,更改或删除OU. |
||||
|
- 在OU树中移动OU. |
||||
|
- 获取有关OU树及其项的信息 |
||||
|
|
||||
|
#### 多租户 |
||||
|
|
||||
|
`OrganizationUnitManager` 设置为一次性为 **单个租户** 工作,默认是 **当前租户**. |
||||
@ -0,0 +1 @@ |
|||||
|
TODO.. |
||||
@ -0,0 +1,255 @@ |
|||||
|
# Web应用程序开发教程 - 第三章: 集成测试 |
||||
|
````json |
||||
|
//[doc-params] |
||||
|
{ |
||||
|
"UI": ["MVC","NG"], |
||||
|
"DB": ["EF","Mongo"] |
||||
|
} |
||||
|
```` |
||||
|
{{ |
||||
|
if UI == "MVC" |
||||
|
UI_Text="mvc" |
||||
|
else if UI == "NG" |
||||
|
UI_Text="angular" |
||||
|
else |
||||
|
UI_Text="?" |
||||
|
end |
||||
|
if DB == "EF" |
||||
|
DB_Text="Entity Framework Core" |
||||
|
else if DB == "Mongo" |
||||
|
DB_Text="MongoDB" |
||||
|
else |
||||
|
DB_Text="?" |
||||
|
end |
||||
|
}} |
||||
|
|
||||
|
## 关于本教程 |
||||
|
|
||||
|
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: |
||||
|
|
||||
|
* **{{DB_Text}}** 做为ORM提供程序. |
||||
|
* **{{UI_Value}}** 做为UI框架. |
||||
|
|
||||
|
本教程分为以下部分: |
||||
|
|
||||
|
- [Part 1: 创建服务端](Part-1.md) |
||||
|
- [Part 2: 图书列表页面](Part-2.md) |
||||
|
- [Part 3: 创建,更新和删除图书](Part-3.md) |
||||
|
- **Part 4: 集成测试**(本章) |
||||
|
- [Part 5: 授权](Part-5.md) |
||||
|
- [Part 6: 作者: 领域层](Part-6.md) |
||||
|
- [Part 7: 作者: 数据库集成](Part-7.md) |
||||
|
- [Part 8: 作者: 应用服务层](Part-8.md) |
||||
|
- [Part 9: 作者: 用户页面](Part-9.md) |
||||
|
- [Part 10: 图书到作者的关系](Part-10.md) |
||||
|
|
||||
|
## 下载源码 |
||||
|
|
||||
|
本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合: |
||||
|
|
||||
|
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) |
||||
|
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) |
||||
|
|
||||
|
## 解决方案中的测试项目 |
||||
|
|
||||
|
这一部分涵盖了 **服务器端** 测试. 解决方案中有多个测试项目: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: |
||||
|
|
||||
|
* [xunit](https://xunit.github.io/) 作为主测试框架. |
||||
|
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库. |
||||
|
* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库. |
||||
|
|
||||
|
{{if DB=="EF"}} |
||||
|
|
||||
|
> 测试项目配置为使用 **SQLite内存** 作为数据库. 创建一个单独的数据库实例并使用数据种子系统进行初始化种子数据,为每个测试准备一个新的数据库. |
||||
|
|
||||
|
{{else if DB=="Mongo"}} |
||||
|
|
||||
|
> **[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)**库用于模拟MongoDB数据库. 创建一个单独的数据库实例并使用数据种子系统进行初始化种子数据,为每个测试准备一个新的数据库. |
||||
|
|
||||
|
{{end}} |
||||
|
|
||||
|
## 添加测试数据 |
||||
|
|
||||
|
如果你已经按照[第一部分](Part-1.md)中的描述创建了数据种子贡献者,则相同的数据也在测试中可用. 因此你可以跳过此部分. 如果你尚未创建种子贡献者,可以使用 `BookStoreTestDataSeedContributor` 来为要在以下测试中使用的相同数据提供种子. |
||||
|
|
||||
|
## 测试 BookAppService |
||||
|
|
||||
|
在 `Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: |
||||
|
|
||||
|
````csharp |
||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Acme.BookStore.Books |
||||
|
{ {{if DB=="Mongo"}} |
||||
|
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} |
||||
|
public class BookAppService_Tests : BookStoreApplicationTestBase |
||||
|
{ |
||||
|
private readonly IBookAppService _bookAppService; |
||||
|
|
||||
|
public BookAppService_Tests() |
||||
|
{ |
||||
|
_bookAppService = GetRequiredService<IBookAppService>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Get_List_Of_Books() |
||||
|
{ |
||||
|
//Act |
||||
|
var result = await _bookAppService.GetListAsync( |
||||
|
new PagedAndSortedResultRequestDto() |
||||
|
); |
||||
|
|
||||
|
//Assert |
||||
|
result.TotalCount.ShouldBeGreaterThan(0); |
||||
|
result.Items.ShouldContain(b => b.Name == "1984"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查. |
||||
|
* 我们可以安全地检查 "1984" 这本书的名称,因为我们知道这本书可以在数据库中找到,我们已将其添加到种子数据中. |
||||
|
|
||||
|
新增测试方法,用以测试创建一个**合法**book实体的场景: |
||||
|
|
||||
|
````csharp |
||||
|
[Fact] |
||||
|
public async Task Should_Create_A_Valid_Book() |
||||
|
{ |
||||
|
//Act |
||||
|
var result = await _bookAppService.CreateAsync( |
||||
|
new CreateUpdateBookDto |
||||
|
{ |
||||
|
Name = "New test book 42", |
||||
|
Price = 10, |
||||
|
PublishDate = System.DateTime.Now, |
||||
|
Type = BookType.ScienceFiction |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
//Assert |
||||
|
result.Id.ShouldNotBe(Guid.Empty); |
||||
|
result.Name.ShouldBe("New test book 42"); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
新增测试方法,用以测试创建一个非法book实体失败的场景: |
||||
|
|
||||
|
````csharp |
||||
|
[Fact] |
||||
|
public async Task Should_Not_Create_A_Book_Without_Name() |
||||
|
{ |
||||
|
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () => |
||||
|
{ |
||||
|
await _bookAppService.CreateAsync( |
||||
|
new CreateUpdateBookDto |
||||
|
{ |
||||
|
Name = "", |
||||
|
Price = 10, |
||||
|
PublishDate = DateTime.Now, |
||||
|
Type = BookType.ScienceFiction |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
exception.ValidationErrors |
||||
|
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常. |
||||
|
|
||||
|
最终的测试类如下所示: |
||||
|
|
||||
|
````csharp |
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Volo.Abp.Validation; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Acme.BookStore.Books |
||||
|
{ {{if DB=="Mongo"}} |
||||
|
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} |
||||
|
public class BookAppService_Tests : BookStoreApplicationTestBase |
||||
|
{ |
||||
|
private readonly IBookAppService _bookAppService; |
||||
|
|
||||
|
public BookAppService_Tests() |
||||
|
{ |
||||
|
_bookAppService = GetRequiredService<IBookAppService>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Get_List_Of_Books() |
||||
|
{ |
||||
|
//Act |
||||
|
var result = await _bookAppService.GetListAsync( |
||||
|
new PagedAndSortedResultRequestDto() |
||||
|
); |
||||
|
|
||||
|
//Assert |
||||
|
result.TotalCount.ShouldBeGreaterThan(0); |
||||
|
result.Items.ShouldContain(b => b.Name == "1984"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Create_A_Valid_Book() |
||||
|
{ |
||||
|
//Act |
||||
|
var result = await _bookAppService.CreateAsync( |
||||
|
new CreateUpdateBookDto |
||||
|
{ |
||||
|
Name = "New test book 42", |
||||
|
Price = 10, |
||||
|
PublishDate = System.DateTime.Now, |
||||
|
Type = BookType.ScienceFiction |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
//Assert |
||||
|
result.Id.ShouldNotBe(Guid.Empty); |
||||
|
result.Name.ShouldBe("New test book 42"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Not_Create_A_Book_Without_Name() |
||||
|
{ |
||||
|
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () => |
||||
|
{ |
||||
|
await _bookAppService.CreateAsync( |
||||
|
new CreateUpdateBookDto |
||||
|
{ |
||||
|
Name = "", |
||||
|
Price = 10, |
||||
|
PublishDate = DateTime.Now, |
||||
|
Type = BookType.ScienceFiction |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
exception.ValidationErrors |
||||
|
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
恭喜你, **绿色图标**表示测试已成功通过! |
||||
|
|
||||
|
## 下一章 |
||||
|
|
||||
|
查看本教程的[下一章](Part-5.md). |
||||
@ -0,0 +1 @@ |
|||||
|
TODO.. |
||||
@ -0,0 +1 @@ |
|||||
|
TODO.. |
||||
@ -0,0 +1 @@ |
|||||
|
TODO.. |
||||
@ -0,0 +1 @@ |
|||||
|
TODO.. |
||||
@ -0,0 +1 @@ |
|||||
|
TODO.. |
||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 33 KiB |
@ -0,0 +1,205 @@ |
|||||
|
# 轻松取消将你的观察取消订阅 |
||||
|
|
||||
|
`SubscriptionService` 是一个实用工具服务,它提供了一个简单的取消订阅Angular组件和指令中的RxJS可观察对象的功能. 请参见[为什么在实例销毁时要取消订阅可观察对象](https://angular.io/guide/lifecycle-hooks#cleaning-up-on-instance-destruction). |
||||
|
|
||||
|
## 入门 |
||||
|
|
||||
|
你必须在组件或指令级别提供 `SubscriptionService`,因为它没有在**根中提供**,而且它与组件/指令的生命周期同步. 只有在此之后,您才能注入并开始使用它。 |
||||
|
|
||||
|
```js |
||||
|
import { SubscriptionService } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
providers: [SubscriptionService], |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
count$ = interval(1000); |
||||
|
|
||||
|
constructor(private subscription: SubscriptionService) { |
||||
|
this.subscription.addOne(this.count$, console.log); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
由 `count$` 发出的值将被记录下来,直到组件被销毁. 你不必手动退订. |
||||
|
|
||||
|
> 请不要尝试使用单例 `SubscriptionService`. 这是行不通的. |
||||
|
|
||||
|
## 用法 |
||||
|
|
||||
|
## 如何订阅可观察对象 |
||||
|
|
||||
|
你可以传递 `next` 函数和 `error` 函数. |
||||
|
|
||||
|
|
||||
|
|
||||
|
```js |
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
providers: [SubscriptionService], |
||||
|
}) |
||||
|
class DemoComponent implements OnInit { |
||||
|
constructor(private subscription: SubscriptionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
const source$ = interval(1000); |
||||
|
const nextFn = value => console.log(value * 2); |
||||
|
const errorFn = error => { |
||||
|
console.error(error); |
||||
|
return of(null); |
||||
|
}; |
||||
|
|
||||
|
this.subscription.addOne(source$, nextFn, errorFn); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
或者,你可以传递一个观察者. |
||||
|
|
||||
|
```js |
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
providers: [SubscriptionService], |
||||
|
}) |
||||
|
class DemoComponent implements OnInit { |
||||
|
constructor(private subscription: SubscriptionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
const source$ = interval(1000); |
||||
|
const observer = { |
||||
|
next: value => console.log(value * 2), |
||||
|
complete: () => console.log('DONE'), |
||||
|
}; |
||||
|
|
||||
|
this.subscription.addOne(source$, observer); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`addOne` 方法返回单个订阅以便你以后使用它. 有关详细信息,请参见下面的主题. |
||||
|
|
||||
|
### 实例销毁之前如何退订 |
||||
|
|
||||
|
有两种方法可以做到这一点. 如果你不想再次订阅.可以使用 `closeAll` 方法. |
||||
|
|
||||
|
```js |
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
providers: [SubscriptionService], |
||||
|
}) |
||||
|
class DemoComponent implements OnInit { |
||||
|
constructor(private subscription: SubscriptionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.subscription.addOne(interval(1000), console.log); |
||||
|
} |
||||
|
|
||||
|
onSomeEvent() { |
||||
|
this.subscription.closeAll(); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
这将清除所有订阅,你将无法再次订阅. 如果你打算添加另一个订阅,可以使用`reset`方法. |
||||
|
|
||||
|
```js |
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
providers: [SubscriptionService], |
||||
|
}) |
||||
|
class DemoComponent implements OnInit { |
||||
|
constructor(private subscription: SubscriptionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.subscription.addOne(interval(1000), console.log); |
||||
|
} |
||||
|
|
||||
|
onSomeEvent() { |
||||
|
this.subscription.reset(); |
||||
|
this.subscription.addOne(interval(1000), console.warn); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 如何取消单个订阅 |
||||
|
|
||||
|
有时你可能需要取消订阅特定的订阅,但保留其他订阅. 在这种情况下,你可以使用 `closeOne` 方法. |
||||
|
|
||||
|
```js |
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
providers: [SubscriptionService], |
||||
|
}) |
||||
|
class DemoComponent implements OnInit { |
||||
|
countSubscription: Subscription; |
||||
|
|
||||
|
constructor(private subscription: SubscriptionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.countSubscription = this.subscription.addOne( |
||||
|
interval(1000), |
||||
|
console.log |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
onSomeEvent() { |
||||
|
this.subscription.closeOne(this.countSubscription); |
||||
|
console.log(this.countSubscription.closed); // true |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 如何从跟踪的订阅中删除单个订阅 |
||||
|
|
||||
|
你可能需要控制特定的订阅. 在这种情况下你可以使用 `removeOne` 方法将其从跟踪的订阅中删除. |
||||
|
|
||||
|
```js |
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
providers: [SubscriptionService], |
||||
|
}) |
||||
|
class DemoComponent implements OnInit { |
||||
|
countSubscription: Subscription; |
||||
|
|
||||
|
constructor(private subscription: SubscriptionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.countSubscription = this.subscription.addOne( |
||||
|
interval(1000), |
||||
|
console.log |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
onSomeEvent() { |
||||
|
this.subscription.removeOne(this.countSubscription); |
||||
|
console.log(this.countSubscription.closed); // false |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 如何检查是否全部取消订阅 |
||||
|
|
||||
|
使用 `isClosed` 检查 `closeAll` 是否被调用. |
||||
|
|
||||
|
```js |
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
providers: [SubscriptionService], |
||||
|
}) |
||||
|
class DemoComponent implements OnInit { |
||||
|
constructor(private subscription: SubscriptionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.subscription.addOne(interval(1000), console.log); |
||||
|
} |
||||
|
|
||||
|
onSomeEvent() { |
||||
|
console.log(this.subscription.isClosed); // false |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## 下一步是什么? |
||||
|
|
||||
|
- [ListService](./List-Service.md) |
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 19 KiB |
@ -0,0 +1,3 @@ |
|||||
|
# Dynamic JavaScript HTTP API Proxies |
||||
|
|
||||
|
TODO |
||||
@ -0,0 +1,38 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.Reflection; |
||||
|
|
||||
|
namespace Volo.Abp.Http.Modeling |
||||
|
{ |
||||
|
public static class ApiTypeNameHelper |
||||
|
{ |
||||
|
public static string GetTypeName(Type type) |
||||
|
{ |
||||
|
if (TypeHelper.IsDictionary(type, out var keyType, out var valueType)) |
||||
|
{ |
||||
|
return $"{{{GetTypeName(keyType)}:{GetTypeName(valueType)}}}"; |
||||
|
} |
||||
|
|
||||
|
if (TypeHelper.IsEnumerable(type, out var itemType, includePrimitives: false)) |
||||
|
{ |
||||
|
return $"[{GetTypeName(itemType)}]"; |
||||
|
} |
||||
|
|
||||
|
return TypeHelper.GetFullNameHandlingNullableAndGenerics(type); |
||||
|
} |
||||
|
|
||||
|
public static string GetSimpleTypeName(Type type) |
||||
|
{ |
||||
|
if (TypeHelper.IsDictionary(type, out var keyType, out var valueType)) |
||||
|
{ |
||||
|
return $"{{{GetSimpleTypeName(keyType)}:{GetSimpleTypeName(valueType)}}}"; |
||||
|
} |
||||
|
|
||||
|
if (TypeHelper.IsEnumerable(type, out var itemType, includePrimitives: false)) |
||||
|
{ |
||||
|
return $"[{GetSimpleTypeName(itemType)}]"; |
||||
|
} |
||||
|
|
||||
|
return TypeHelper.GetSimplifiedName(type); |
||||
|
} |
||||
|
} |
||||
|
} |
||||