Web應用程序開發教程 - 第二章: 圖書列表頁面
關於本教程
在本系列教程中, 你將構建一個名為 Acme.BookStore 的用於管理書籍及其作者列表的基於ABP的應用程序. 它是使用以下技術開發的:
- {{DB_Text}} 做為ORM提供程序.
- {{UI_Value}} 做為UI框架.
本教程分為以下部分:
- Part 1: 創建服務端
- Part 2: 圖書列表頁面(本章)
- Part 3: 創建,更新和刪除圖書
- Part 4: 集成測試
- Part 5: 授權
- Part 6: 作者: 領域層
- Part 7: 作者: 數據庫集成
- Part 8: 作者: 應用服務層
- Part 9: 作者: 用戶頁面
- [Part 10: 圖書到作者的關系]abp-tutorial-part-10.html)
下載源碼
本教程根據你的UI 和 Database偏好有多個版,我們准備了兩種可供下載的源碼組合:
{{if UI == "MVC"}}
動態JavaScript代理
通常在 JavaScript 端通過AJAX調用HTTP API端點. 你可以使用 $.ajax 或其他工具來調用端點. 但是ABP提供了更好的方法.
ABP動態為所有API端點創建 JavaScript代理. 所以你可以像調用Javascript本地方法一樣使用任何端點.
在開發者控制台中進行測試
你可以在自己喜歡的瀏覽器的開發者控制台輕松的測試JavaScript代理. 運行應用程序,打開瀏覽器的開發者人員工具(快捷鍵通常是F12),切換到控制台選項卡,輸入以下代碼然后按回車:
acme.bookStore.books.book.getList({}).done(function (result) { console.log(result); });
acme.bookStore.books是BookAppService的命令空間轉換成小駝峰形式.book是BookAppService的約定名稱(刪除AppService后綴並且轉換為小駝峰).getList是CrudAppService基類定義的GetListAsync方法的約定名稱(刪除Async后綴並且轉換為小駝峰).{}參數將空對象發送到GetListAsync方法,該方法通常需要一個類型為PagedAndSortedResultRequestDto的對象,該對象用於將分頁和排序選項發送到服務器(所有屬性都是可選的,具有默認值. 因此你可以發送一個空對象).getList函數返回一個promise. 你可以傳遞一個回調到then(或done)函數來獲取從服務器返回的結果.
運行該代碼會產生以下輸出:

你可以看到服務端返回的 圖書列表. 你也可以在開發者人員工具的 網絡 選項卡查看客戶端到服務端的通信:

Let's create a new book using the create function:
讓我們使用 create 函數創建一本書:
acme.bookStore.books.book.create({
name: 'Foundation',
type: 7,
publishDate: '1951-05-24',
price: 21.5
}).then(function (result) {
console.log('successfully created the book with id: ' + result.id);
});
您應該在控制台中看到類似以下的消息:
successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
檢查數據庫中的 Books 表你會看到新的一行. 你可以自己嘗試使用 get, update 和 delete 函數.
我們將利用這些動態代理功能在接下來的章節來與服務器通信.
{{end}}
本地化
開始的UI開發之前,我們首先要准備本地化的文本(這是你通常在開發應用程序時需要做的).
本地化文本位於 Acme.BookStore.Domain.Shared 項目的 Localization/BookStore 文件夾下:

打開 en.json (英文翻譯)文件並更改內容,如下所示:
{
"Culture": "en",
"Texts": {
"Menu:Home": "Home",
"Welcome": "Welcome",
"LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.",
"Menu:BookStore": "Book Store",
"Menu:Books": "Books",
"Actions": "Actions",
"Close": "Close",
"Delete": "Delete",
"Edit": "Edit",
"PublishDate": "Publish date",
"NewBook": "New book",
"Name": "Name",
"Type": "Type",
"Price": "Price",
"CreationTime": "Creation time",
"AreYouSure": "Are you sure?",
"AreYouSureToDelete": "Are you sure you want to delete this item?",
"Enum:BookType:0": "Undefined",
"Enum:BookType:1": "Adventure",
"Enum:BookType:2": "Biography",
"Enum:BookType:3": "Dystopia",
"Enum:BookType:4": "Fantastic",
"Enum:BookType:5": "Horror",
"Enum:BookType:6": "Science",
"Enum:BookType:7": "Science fiction",
"Enum:BookType:8": "Poetry"
}
}
- 本地化關鍵字名稱是任意的. 你可以設置任何名稱. 對於特定的文本類型,我們更喜歡遵循一些約定:
- 為按鈕項添加
Menu:前綴. - 使用
Enum:<enum-type>:<enum-value>命名約定來本地化枚舉成員. 當您這樣做時ABP可以在某些適當的情況下自動將枚舉本地化.
- 為按鈕項添加
如果未在本地化文件中定義文本,則文本將回退到本地化鍵(作為ASP.NET Core的標准行為).
ABP本地化系統建立在ASP.NET Core標准本地化系統之上,並以多種方式進行了擴展. 有關詳細信息請參見本地化文檔.
{{if UI == "MVC"}}
創建圖書頁面
是時候創建可見的和可用的東西了! 代替經典的MVC,我們將使用微軟推薦的Razor Pages UI.
在 Acme.BookStore.Web 項目的 Pages 文件夾下創建一個名為新的 Books 的文件夾. 然后在文件夾右鍵選擇 添加 > Razor Page 菜單. 輸入名稱 Index:

打開 Index.cshtml 並把內容修改成下面這樣:
@page
@using Acme.BookStore.Web.Pages.Books
@model IndexModel
<h2>Books</h2>
Index.cshtml.cs 內容應該是:
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Acme.BookStore.Web.Pages.Books
{
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
}
將Book頁面添加到主菜單
打開 Menus 文件夾中的 BookStoreMenuContributor 類,在 ConfigureMainMenuAsync 方法的底部添加如下代碼:
context.Menu.AddItem(
new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
).AddItem(
new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/Books"
)
)
);
運行項目,使用用戶名 admin 和密碼 1q2w3E* 登錄到應用程序. 看到新菜單項已添加到頂部欄:

點擊BookStore下的Books子菜單項就會跳轉到空的圖書頁面.
圖書列表
We will use the Datatables.net jQuery library to show the book list. Datatables library completely work via AJAX, it is fast, popular and provides a good user experience.
我們將使用Datatables.netJQuery插件來顯示頁面上的表格列表. Datatables可以完全通過AJAX工作,速度快,並提供良好的用戶體驗.
Datatables插件在啟動模板中配置,因此你可以直接在任何頁面中使用它,無需在頁面中引用樣式和腳本文件.
Index.cshtml
將 Pages/Book/Index.cshtml 改成下面的樣子:
@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Web.Pages.Books
@using Microsoft.Extensions.Localization
@model IndexModel
@inject IStringLocalizer<BookStoreResource> L
@section scripts
{
<abp-script src="/Pages/Books/Index.js" />
}
<abp-card>
<abp-card-header>
<h2>@L["Books"]</h2>
</abp-card-header>
<abp-card-body>
<abp-table striped-rows="true" id="BooksTable"></abp-table>
</abp-card-body>
</abp-card>
abp-scripttag helper用於將外部的 腳本 添加到頁面中.它比標准的script標簽多了很多額外的功能.它可以處理 最小化和 版本.查看捆綁 & 壓縮文檔獲取更多信息.abp-card和abp-table是為Twitter Bootstrap的card component封裝的 tag helpers.ABP中有很多tag helpers,可以很方便的使用大多數bootstrap組件.你也可以使用原生的HTML標簽代替tag helpers.使用tag helper可以通過智能提示和編譯時類型檢查減少HTML代碼並防止錯誤.查看tag helpers 文檔.
Index.js
在 Pages/Books/ 文件夾中創建 index.js文件

index.js 的內容如下:
$(function () {
var l = abp.localization.getResource('BookStore');
var dataTable = $('#BooksTable').DataTable(
abp.libs.datatables.normalizeConfiguration({
serverSide: true,
paging: true,
order: [[1, "asc"]],
searching: false,
scrollX: true,
ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
columnDefs: [
{
title: l('Name'),
data: "name"
},
{
title: l('Type'),
data: "type",
render: function (data) {
return l('Enum:BookType:' + data);
}
},
{
title: l('PublishDate'),
data: "publishDate",
render: function (data) {
return luxon
.DateTime
.fromISO(data, {
locale: abp.localization.currentCulture.name
}).toLocaleString();
}
},
{
title: l('Price'),
data: "price"
},
{
title: l('CreationTime'), data: "creationTime",
render: function (data) {
return luxon
.DateTime
.fromISO(data, {
locale: abp.localization.currentCulture.name
}).toLocaleString(luxon.DateTime.DATETIME_SHORT);
}
}
]
})
);
});
abp.localization.getResource獲取一個函數,該函數用於使用服務器端定義的相同JSON文件對文本進行本地化. 通過這種方式你可以與客戶端共享本地化值.abp.libs.datatables.normalizeConfiguration是另一個輔助方法.不是必須的, 但是它通過為缺少的選項提供常規值來簡化數據表配置.abp.libs.datatables.createAjax是幫助ABP的動態JavaScript API代理跟Datatable的格式相適應的輔助方法.acme.bookStore.books.book.getList是動態JavaScript代理函數(上面已經介紹過了)- luxon 庫也是該解決方案中預先配置的標准庫,你可以輕松地執行日期/時間操作.
查看 Datatable文檔 了解更多配置項.
運行最終應用程序
你可以運行應用程序!該部分的最終用戶界面如下所示:

這是一個完全正常工作的服務端分頁,排序和本地化的圖書列表.
{{end}}
{{if UI == "NG"}}
安裝NPM包
注意: 本教程基於ABP Framework v3.0.3+. 如果你的項目版本較舊,請升級您的解決方案. 如果要升級現有的v2.x項目,請參閱遷移指南.
在 angular 目錄下打開命令行窗口,選擇 yarn 命令安裝NPM包:
yarn
創建圖書頁面
是時候創建可見和可用的東西了!開發ABP Angular前端應用程序時,需要使用一些工具:
- Ng Bootstrap 用做UI組件庫.
- ngx-datatable 用做 datatable 類庫.
BookModule
運行以下命令創建一個名為 BookModule 的新模塊:
yarn ng generate module book --module app --routing --route books
該命令應該產生以下的輸出:
> yarn ng generate module book --module app --routing --route books
yarn run v1.19.1
$ ng generate module book --module app --routing --route books
CREATE src/app/book/book-routing.module.ts (336 bytes)
CREATE src/app/book/book.module.ts (335 bytes)
CREATE src/app/book/book.component.html (19 bytes)
CREATE src/app/book/book.component.spec.ts (614 bytes)
CREATE src/app/book/book.component.ts (268 bytes)
CREATE src/app/book/book.component.scss (0 bytes)
UPDATE src/app/app-routing.module.ts (1289 bytes)
Done in 3.88s.
BookModule
打開 /src/app/book/book.module.ts 並使用以下內容替換:
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { BookRoutingModule } from './book-routing.module';
import { BookComponent } from './book.component';
@NgModule({
declarations: [BookComponent],
imports: [
BookRoutingModule,
SharedModule
]
})
export class BookModule { }
- 添加了
SharedModule.SharedModule導出了一些創建用戶界面所需的通用模塊. SharedModule已經導出了CommonModule,所以我們刪除了CommonModule.
路由
生成的代碼將新的路由定義放在 src/app/app-routing.module.ts 文件中,如下所示:
const routes: Routes = [
// other route definitions...
{ path: 'books', loadChildren: () => import('./book/book.module').then(m => m.BookModule) },
];
現在打開 src/app/route.provider.ts 以下替換 configureRoutes 函數:
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
{
path: '/',
name: '::Menu:Home',
iconClass: 'fas fa-home',
order: 1,
layout: eLayoutType.application,
},
{
path: '/book-store',
name: '::Menu:BookStore',
iconClass: 'fas fa-book',
order: 2,
layout: eLayoutType.application,
},
{
path: '/books',
name: '::Menu:Books',
parentName: '::Menu:BookStore',
layout: eLayoutType.application,
},
]);
};
}
RoutesService 是ABP框架提供的用於配置主菜單和路由的服務.
path路由的URL.name菜單項的名稱(參閱本地化文檔了解更多).iconClass菜單項的圖標(你可以使用默認的Font Awesome圖標).order菜單項的排序.我們定義了101,它顯示在 "Administration" 項的后面.layoutBooksModule路由的布局. 可以定義eLayoutType.application,eLayoutType.account或eLayoutType.empty.
更多信息請參閱RoutesService 文檔.
生成代理
ABP CLI提供了 generate-proxy 命令為你的服務HTTP API生成客戶端代理簡化客戶端使用服務的成本. 運行 generate-proxy 命令前你的host必須正在運行. 參閱 CLI 文檔.
在 angular 文件夾下運行以下命令:
abp generate-proxy
生成的文件如下:

BookComponent
打開 /src/app/book/book.component.ts 用以下內容替換它:
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookDto } from './models';
import { BookService } from './services';
@Component({
selector: 'app-book',
templateUrl: './book.component.html',
styleUrls: ['./book.component.scss'],
providers: [ListService],
})
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
constructor(public readonly list: ListService, private bookService: BookService) {}
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
}
- 我們注入了生成的
BookService. - 我們實現了 ListService,它是一個公用服務,提供了簡單的分頁,排序和搜索.
打開 /src/app/book/book.component.html 用以下內容替換它:
<div class="card">
<div class="card-header">
<div class="row">
<div class="col col-md-6">
<h5 class="card-title">
{%{{{ '::Menu:Books' | abpLocalization }}}%}
</h5>
</div>
<div class="text-right col col-md-6"></div>
</div>
</div>
<div class="card-body">
<ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
<ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
<ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ '::Enum:BookType:' + row.type | abpLocalization }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.publishDate | date }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.price | currency }}}%}
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
</div>
</div>
現在你可以在瀏覽器看到最終結果:

{{end}}
下一章
查看本教程的下一章.
