Abp Vnext Vue3 的版本實現
Abp Vnext Pro 的 Vue3 實現版本 開箱即用的中後台前端/設計解決方案
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
開始
系統功能
- [x] 用戶管理
- [x] 角色管理
- [x] 審計日誌
- [x] 後台任務
- [x] 集成事件
- [x] IdentityServer4
- [x] 客戶端管理
- [x] Api 資源管理
- [x] ApiScope 管理
- [x] Identity 資源管理
- [x] SinglaR 消息通知
- [x] 多語言
- [x] FreeSql
- [x] 數據字典(UI 暫時沒有)
- [x] 容器化部署
- [x] 單元測試
- [x] ES 日誌
- [x] Setting 管理
- [x] 多租戶
- [ ] 組織機構
項目結構
後端
.
├── Directory.Build.props nuget 版本控制
├── frameworks # 公共模組
│ ├── CAP # dotnetcore.cap
│ └── Extensions # 自定義擴展
├── gateways # 網關
├── modules # 模組
│ ├── DataDictionaryManagement # 數據字典
│ └── NotificationManagement # 通知服務
├── services # 公共靜態資源目錄
│ ├── host # 啟動模組
│ ├── CompanyName.ProjectName.HttpApi.Host # admin ui host
│ └── CompanyName.ProjectName.IdentityServer # IdentityServer host
│ ├── src # 源碼
│ └── CompanyName.ProjectName.DbMigrator # 遷移控制台程式
│ └── test # 單元測試
前端
.
├── _nginx # docker 打包
├── build # 打包腳本相關
│ ├── config # 配置文件
│ ├── generate # 生成器
│ ├── script # 腳本
│ └── vite # vite配置
├── mock # mock文件夾
├── public # 公共靜態資源目錄
├── src # 主目錄
│ ├── api # 介面文件
│ ├── assets # 資源文件
│ │ ├── icons # icon sprite 圖標文件夾
│ │ ├── images # 項目存放圖片的文件夾
│ │ └── svg # 項目存放svg圖片的文件夾
│ ├── components # 公共組件
│ ├── design # 樣式文件
│ ├── directives # 指令
│ ├── enums # 枚舉/常量
│ ├── hooks # hook
│ │ ├── component # 組件相關hook
│ │ ├── core # 基礎hook
│ │ ├── event # 事件相關hook
│ │ ├── setting # 配置相關hook
│ │ └── web # web相關hook
│ ├── layouts # 布局文件
│ │ ├── default # 默認布局
│ │ ├── iframe # iframe布局
│ │ └── page # 頁面布局
│ ├── locales # 多語言
│ ├── logics # 邏輯
│ ├── main.ts # 主入口
│ ├── router # 路由配置
│ ├── services # Nswag生成的代理
│ │ ├── ServiceProxies.ts # Nswag生成的代理
│ │ ├── ServiceProxyBase.ts # Nswag生成的代理攔截器
│ ├── settings # 項目配置
│ │ ├── componentSetting.ts # 組件配置
│ │ ├── designSetting.ts # 樣式配置
│ │ ├── encryptionSetting.ts # 加密配置
│ │ ├── localeSetting.ts # 多語言配置
│ │ ├── projectSetting.ts # 項目配置
│ │ └── siteSetting.ts # 站點配置
│ ├── store # 數據倉庫
│ ├── utils # 工具類
│ └── views # 頁面
├── test # 測試
│ └── server # 測試用到的服務
│ ├── api # 測試伺服器
│ ├── upload # 測試上傳伺服器
│ └── websocket # 測試ws伺服器
├── types # 類型文件
├── vite.config.ts # vite配置文件
└── windi.config.ts # windcss配置文件
運行項目前提
-
Mysql
docker run --name mymysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=1q2w3E* -d mysql:5.7 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
-
Redis
docker run --name myredis -p 6379:6379 -d redis:latest redis-server
-
RabbitMq 非必須
-
appsetting.development.json-> CAP:Enabled 設置為 false
docker run -d --name myrabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 rabbitmq:management
-
ELK 非必須
-
appsetting.development.json-> LogToElasticSearch:Enabled 設置為 false
-
安裝 Node.js, Npm Or Yarn
獲取項目
- 直接 clone 項目
git clone //github.com/WangJunZzz/abp-vnext-pro.git
OR
- 下載程式碼生成器
git clone //github.com/WangJunZzz/abp-vnext-pro-gui.git
- 下載程式碼生成生成器之後,輸入自己想要的項目名稱生成程式碼即可
啟動
- 修改 HttpApi.Host-> appsettings.development.json 的資料庫連接字元串,Redis, RabbitMq,Es 地址即可(如果沒有 es 也可以運行,只是前端 es 日誌頁面無法使用而已,不影響後端項目啟動)
- 修改 IdentityServer-> appsettings.development.json 資料庫連接字元串
- 修改 DbMigrator-> appsettings.json 資料庫連接字元串
- 運行 DbMigrator 生成資料庫
- 啟動 HttpApi.Host 和 IdentityServer
- 前端 yarn 之後,執行 npm run dev 啟動
配置說明
- HttpApi.Host-> appsettings.development.json
{
// Serilog 日誌配置,生成環境修改日誌級別
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"Volo.Abp": "Information",
"Hangfire": "Information",
"DotNetCore.CAP": "Information",
"Serilog.AspNetCore": "Information"
}
}
},
// 跨域設置
"App": {
"CorsOrigins": "//*.ProjectName.com,//localhost:4200,//localhost:3100"
},
// 資料庫連接字元串,修改為你本地的mysql地址
"ConnectionStrings": {
"Default": "Data Source=localhost;Database=CompanyNameProjectNameDB;uid=root;pwd=1q2w3E*;charset=utf8mb4;Allow User Variables=true;AllowLoadLocalInfile=true"
},
// Redis快取
"Cache": {
"Redis": {
"ConnectionString": "localhost",
"Password": "mypassword",
"DatabaseId": 0
}
},
// Jwt配置
"Jwt": {
"Audience": "CompanyNameProjectName",
//客戶端標識
"SecurityKey": "dzehzRz9a8asdfasfdadfasdfasdfafsdadfasbasdf=",
"Issuer": "CompanyNameProjectName",
//簽發者
"ExpirationTime": 24
//過期時間 hour
},
// 使用了Dotnetcore.cap的rabbitmq,false的情況基於記憶體
"Cap": {
"Enabled": "false",
"RabbitMq": {
"HostName": "localhost",
"UserName": "admin",
"Password": "admin"
}
},
// es日誌地址配置
"LogToElasticSearch": {
"Enabled": "true",
"ElasticSearch": {
"Url": "//es.cn",
"IndexFormat": "companyname.projectname.development",
"UserName": "elastic",
"Password": "aVVhjQ95RP7nbwNy",
"DashboardIndex": "companyname.projectname"
}
},
// identityserver地址
"HttpClient": {
"Sts": {
"Url": "//localhost:44354"
}
},
// Consul 服務發現和治理
"Consul": {
"Enabled": false,
"Host": "//localhost:8500",
"Service": "Project-Service"
}
}
- IdentityServer-> appsettings.development.json
{
"App": {
"SelfUrl": "//localhost:44354",
"ClientUrl": "//localhost:4200",
"CorsOrigins": "//*.ProjectName.com,//localhost:4200,//localhost:44307,//localhost:44315",
"RedirectAllowedUrls": "//localhost:4200,//localhost:44307"
},
// mysql連接字元串
"ConnectionStrings": {
"Default": "Data Source=localhost;Database=CompanyNameProjectNameDB;uid=root;pwd=1q2w3E*;charset=utf8mb4;Allow User Variables=true;AllowLoadLocalInfile=true"
},
// Redis
"Redis": {
"Configuration": "localhost,password=mypassword"
}
}
- DbMigrator-> appsettings.json
// 遷移資料庫
"ConnectionStrings": {
"Default": "Data Source=localhost;Database=CompanyNameProjectNameDB;uid=root;pwd=1q2w3E*;charset=utf8mb4;Allow User Variables=true;AllowLoadLocalInfile=true"
}
前端
- 前端採用 TypeScript,所有的類型動態生成 NSwag
- 後端 api 統一使用 Post
- 定義 api 格式
// 一定要打Tags,因為前端會根據這個生成代理類
// 建議參數都封裝為一個Input
[SwaggerOperation(summary: "登錄", Tags = new[] {"Account"})]
public Task<LoginOutput> LoginAsync(LoginInput input)
{
return _loginAppService.LoginAsync(input);
}
-
在前端目錄下配置代理的地址
- nswag->nswag.json
"documentGenerator": {
"fromDocument": {
"url": "//localhost:44315/swagger/v1/swagger.json", // 代理地址,只有生成的時候用,不區分環境
}
}
- 如果介面參數或者返回值有改變,需要重新生成代理,執行:
npm run nswag
-
前端多環境,.env.development 和.env.production
- 介面地址配置 VITE_API_URL
- IdentityServer 地址配置 VITE_AUTH_URL
-
許可權配置
-
菜單許可權
- src/router/routes
policy 欄位匹配後端的許可權名稱
- 按鈕許可權
v-auth=”‘AbpIdentity.Users.Delete'”
- src/router/routes
健康檢查
模組
用戶管理
- 提供原始登錄和第三方登錄(IdentityServer4),默認用戶名密碼:admin 1q2w3*
角色管理
- 許可權定義(Application.Contracts 層)
- Abp 會自動掃描繼承 PermissionDefinitionProvider
- 文檔 Abp 官方
- 在 Http.Api 的 Controller 打上 Authorize
設置管理
消息通知
- 消息類型,發送給指定人和廣播消息
- 發送消息到前端,通過集成事件和 RabbitMq
- 注入 NotificationManager 發送消息,
/// <summary>
/// 發送普通文本消息
/// </summary>
/// <returns></returns>
/// <exception cref="NotificationManagementDomainException"></exception>
public async Task<Notification> SendCommonTextAsync(string title, string content, List<Guid> receiveIds)
{
if (receiveIds is {Count: 0})
{
throw new NotificationManagementDomainException("消息接收人不能為空");
var senderId = Guid.Empty;
if (_currentUser?.Id != null)
{
senderId = _currentUser.Id.Value;
var entity = new Notification(GuidGenerator.Create(), title, content, MessageType.Text, senderId);
foreach (var item in receiveIds)
{
entity.AddNotificationSubscription(GuidGenerator.Create(), item);
var notificationEto = ObjectMapper.Map<Notification, NotificationEto>(entity);
// 發送集成事件
entity.AddCreatedNotificationDistributedEvent(new CreatedNotificationDistributedEvent(notificationEto));
return entity = await _notificationRepository.InsertAsync(entity);
}
- Handler 當前事件:NotificationCreatedDistributedEventHandler
/// <summary>
/// 發送消息
/// </summary>
public async Task SendMessageAsync(string title, string content, MessageType messageType, List<string> users)
{
switch (messageType)
{
case MessageType.Text:
await SendMessageToClientByUserIdAsync(new SendNotificationDto(title, content, messageType), users);
break;
case MessageType.BroadCast:
await SendMessageToAllClientAsync(new SendNotificationDto(title, content, messageType));
break;
default:
throw new UserFriendlyException("未知的消息類型");
}
}
- 前端接受 SignalR 消息
// src/hooks/web/useSignalR.js
import * as signalR from "@microsoft/signalr";
import { useMessage } from "/@/hooks/web/useMessage";
import { useUserStoreWithOut } from "/@/store/modules/user";
export function useSignalR() {
/**
* 開始連接SignalR
*/
function startConnect(): void {
let connection = connectionsignalR();
//接收普通文本消息
connection.on("ReceiveTextMessageAsync", ReceiveTextMessageHandlerAsync);
//接收廣播消息
connection.on("ReceiveBroadCastMessageAsync", ReceiveBroadCastMessageHandlerAsync);
//開始連接
connection.start();
}
/**
* 連接signalr
*/
function connectionsignalR(): signalR.HubConnection {
const userStore = useUserStoreWithOut();
const token = userStore.getToken;
const url = (import.meta.env.VITE_WEBSOCKE_URL as string) + "/ws/signalr/notification";
const connection = new signalR.HubConnectionBuilder()
.withUrl(url, {
accessTokenFactory: () => token,
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
})
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
//重連規則:重連次數<300:間隔1s;重試次數<3000:間隔3s;重試次數>3000:間隔30s
let count = retryContext.previousRetryCount / 300;
if (count < 1) {
//重試次數<300,間隔1s
return 1000;
} else if (count < 10) {
//重試次數>300:間隔5s
return 1000 * 5;
} //重試次數>3000:間隔30s
else {
return 1000 * 30;
}
},
})
.configureLogging(signalR.LogLevel.Debug)
.build();
return connection;
}
/**
* 接收文本消息
* @param message 消息體
*/
function ReceiveTextMessageHandlerAsync(message: any) {
console.log(message);
const { notification } = useMessage();
notification.open({
message: message.title,
description: message.content,
});
}
/**
* 接收廣播消息
* @param message 消息體
*/
function ReceiveBroadCastMessageHandlerAsync(message: any) {
const { notification } = useMessage();
notification.open({
message: message.title,
description: message.content,
});
}
return { startConnect };
}
審計日誌
- 參考 Abp 官方文檔即可
ES 日誌
- 在 appsetting.development.json 設置是否開啟
"LogToElasticSearch": {
"Enabled": "false", // 如果為fasel,日誌也會寫入到本地,安裝ELK,參考上面的docker-compose
"ElasticSearch": {
"Url": "//es.cn",
"IndexFormat": "companyname.projectname.development",
"UserName": "elastic",
"Password": "aVVhjQ95RP7nbwNy",
"DashboardIndex": "companyname.projectname"
}
},
後台任務
- 定時任務
public override void OnPostApplicationInitialization(ApplicationInitializationContext context)
{
context.CreateRecurringJob();
base.OnPostApplicationInitialization(context);
}
- 延遲任務: 官方文檔
集成事件
-
在 appsetting.development.json 設置是否開啟
"Cap": {
"Enabled": "false", //如果為false 默認使用記憶體級別的隊列,否則請安裝rabbitmq
"RabbitMq": {
"HostName": "localhost",
"UserName": "admin",
"Password": "admin"
}
},
private void ConfigurationCap(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
var enabled = configuration.GetValue<bool>("Cap:Enabled", false);
if (enabled)
{
context.AddAbpCap(capOptions =>
{
capOptions.UseEntityFramework<ProjectNameDbContext>();
capOptions.UseRabbitMQ(option =>
{
option.HostName = configuration.GetValue<string>("Cap:RabbitMq:HostName");
option.UserName = configuration.GetValue<string>("Cap:RabbitMq:UserName");
option.Password = configuration.GetValue<string>("Cap:RabbitMq:Password");
});
var hostingEnvironment = context.Services.GetHostingEnvironment();
bool auth = !hostingEnvironment.IsDevelopment();
capOptions.UseDashboard(options => { options.UseAuth = auth; });
});
}
else
{
context.AddAbpCap(capOptions =>
{
capOptions.UseInMemoryStorage();
capOptions.UseInMemoryMessageQueue();
var hostingEnvironment = context.Services.GetHostingEnvironment();
bool auth = !hostingEnvironment.IsDevelopment();
capOptions.UseDashboard(options => { options.UseAuth = auth; });
});
}
}
- 發布事件
- 可參考通知模組
// 發送集成事件
entity.AddCreatedNotificationDistributedEvent(new CreatedNotificationDistributedEvent(notificationEto));
- 訂閱事件
- 可參考通知模組
/// <summary>
/// 創建消息事件處理
/// </summary>
public class
CreatedNotificationDistributedEventHandler : IDistributedEventHandler<CreatedNotificationDistributedEvent>,
ITransientDependency
{
private readonly INotificationAppService _hubAppService;
public CreatedNotificationDistributedEventHandler(INotificationAppService hubAppService)
{
_hubAppService = hubAppService;
}
public Task HandleEventAsync(CreatedNotificationDistributedEvent eventData)
{
return _hubAppService.SendMessageAsync(
eventData.NotificationEto.Title,
eventData.NotificationEto.Content,
eventData.NotificationEto.MessageType,
eventData.NotificationEto.NotificationSubscriptions.Select(e => e.ReceiveId.ToString()).ToList());
}
}
身份認證中心
- IdentityServer4
- 可重寫登錄介面 UI
租戶管理
- 提供租戶登錄和 IdentityServer4 租戶登錄方式
Ocelot 網關(可選)
- 集成 Ocelot 和 Consul
部署
Docker 方式
HttpApi.Host
-
發布 HttpApi.Host 到和 Dockerfile 同級目錄
-- publish -- Dockerfile
-
Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:5.0
# 創建目錄
RUN mkdir /app
COPY publish /app
# 設置工作目錄
WORKDIR /app
# 暴露80埠
EXPOSE 80
# 設置環境變數
ENV ASPNETCORE_ENVIRONMENT=Production
ENTRYPOINT ["dotnet", "CompanyName.ProjectName.HttpApi.Host.dll"]
- 生成 Docker 鏡像
docker build -t abp-vnext-pro-admin .
- 運行容器
docker run -itd --name abp-vnext-pro-admin -p 8011:80 abp-vnext-pro-admin
IdentityServer.Host
- 步驟同上
前端
- 打包
npm run build
- Dockerfile
FROM nginx:1.17.3-alpine as base
EXPOSE 80
COPY /_nginx/nginx.conf /etc/nginx/nginx.conf
COPY /_nginx/env.js /etc/nginx/env.js
COPY /_nginx/default.conf /etc/nginx/conf.d/default.conf
COPY /dist/ /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
- 生成 Docker 鏡像
docker build -t abp-vnext-pro-ui .
- 運行容器
docker run -itd --name abp-vnext-pro-ui -p 8012:80 abp-vnext-pro-ui
常見問題
VS 編譯項目字元串超過 256 個字元
- 把項目拷貝到磁碟根目錄 OR 使用 Rider 開發
Hangfire 和 Cap 介面載入不出來
- 這 2 個介面開啟了許可權認證,由於前端路由的非同步載入,導致路由在渲染的時候 access_token 沒有載入出來,Ctrl+F5 刷新即可