我正在尝试为以角度4编写的应用程序实现搜索功能。它基本上用于显示大量数据的表。我还添加了ngrx商店。 使用商店实现搜索应用程序的正确方法是什么? 目前,我每次都在清理商店,搜索查询,然后将我从异步调用收到的数据填充到后端。我在HTML中显示这些数据。异步调用是从效果文件中完成的。
答案 0 :(得分:1)
我最近使用Angular 4和@ngrx实现了搜索功能。 我这样做的方法是调度EXECUTE_SEARCH操作,将查询字符串设置到商店并触发效果。该效果触发了异步调用。当异步调用返回时,我根据结果调度了FETCH_SUCCESSFUL操作或FETCH_FAILURE操作。如果成功,我将结果设置在我的商店中。
清除商店中的结果时,实际上取决于所需的行为。我的项目,我在FETCH_SUCCESSFUL上清除了结果,替换了旧的结果。在其他用例中,在执行新搜索时清除存储结果可能是合理的(在EXECUTE_SEARCH减速器中)。
答案 1 :(得分:0)
好吧,由于我长时间没有找到这个问题的答案,我采取了一种方法来保存来自后端的数据,然后按照以下方式搜索数据:
我实现了一个搜索效果,它会触发对后端的异步调用。从后端我返回搜索结果以及他们的ID。接收数据后,此效果将触发搜索完成操作。然后在这个reducer动作中,我曾经用名称searchIds将结果的id存储在我的状态中,并且我创建了一个名为entity的状态,这个状态基本上是以id为键的数据映射。
将从后端接收的数据将被过滤,以检查它是否已经存在于商店中,如果没有,则将其附加到实体。之后我订阅了一个选择器,它基本上会查找searchIds中存在的键,并仅返回实体中的数据。由于它是一个已经将ID作为键的映射,因此基于searchIds进行搜索非常有效,而且我也不必清除已有的数据。这反过来又维持了@ngrx / store的真正目的,即缓存我收到的任何数据。
答案 2 :(得分:0)
这是一个古老的问题,但我认为它值得一个更具体的例子。
由于每个搜索基本上都是唯一的,因此我也在清除结果。但是,由于结果列表可能很长,并且我不想全部显示它们,因此我加载了所有结果(以API中配置的适当值作为顶部),但使用分页显示。
以下使用了Angular 7 + ngrx / store。
操作
import { Action } from "@ngrx/store";
import { PostsSearchResult } from "../models/posts-search-result";
export enum PostsSearchActionType {
PostsSearchResultRequested = "[View Search Results] Search Results Requested",
PostsSearchResultLoaded = "[Search Results API] Search Results Loaded",
PostsSearchResultsClear = "[View Search Results Page] Search Results Page Clear",
PostsSearchResultsPageRequested = "[View Search Results Page] Search Results Page Requested",
PostsSearchResultsPageLoaded = "[Search Results API] Search Results Page Loaded",
PostsSearchResultsPageCancelled = "[Search Results API] Search Results Page Cancelled",
}
export class PostsSearchResultsClearAction implements Action {
readonly type = PostsSearchActionType.PostsSearchResultsClear;
constructor() {
}
}
export class PostsSearchPageRequestedAction implements Action {
readonly type = PostsSearchActionType.PostsSearchResultsPageRequested;
constructor(public payload: { searchText: string }) {
}
}
export class PostsSearchRequestedAction implements Action {
readonly type = PostsSearchActionType.PostsSearchResultRequested;
constructor(public payload: { searchText: string }) {
}
}
export class PostsSearchLoadedAction implements Action {
readonly type = PostsSearchActionType.PostsSearchResultLoaded;
constructor(public payload: { results: PostsSearchResult[] }) {
}
}
export class PostsSearchResultsPageLoadedAction implements Action {
readonly type = PostsSearchActionType.PostsSearchResultsPageLoaded;
constructor(public payload: { searchResults: PostsSearchResult[] }) {
}
}
export class PostsSearchResultsPageCancelledAction implements Action {
readonly type = PostsSearchActionType.PostsSearchResultsPageCancelled;
}
export type PostsSearchAction =
PostsSearchResultsClearAction |
PostsSearchRequestedAction |
PostsSearchLoadedAction |
PostsSearchPageRequestedAction |
PostsSearchResultsPageLoadedAction |
PostsSearchResultsPageCancelledAction;
效果
只有一种效果可以在需要时加载数据。即使我使用分页显示数据,也会一次从服务器获取搜索结果。
import { Injectable } from "@angular/core";
import { Actions, Effect, ofType } from "@ngrx/effects";
import { Store, select } from "@ngrx/store";
import { AppState } from "src/app/reducers";
import { mergeMap, map, catchError, tap, switchMap } from "rxjs/operators";
import { of } from "rxjs";
import { PostsService } from "../services/posts.service";
// tslint:disable-next-line:max-line-length
import { PostsSearchRequestedAction, PostsSearchActionType, PostsSearchLoadedAction, PostsSearchPageRequestedAction, PostsSearchResultsPageCancelledAction, PostsSearchResultsPageLoadedAction } from "./posts-search.actions";
import { PostsSearchResult } from "../models/posts-search-result";
import { LoggingService } from "src/app/custom-core/general/logging-service";
import { LoadingStartedAction } from "src/app/custom-core/loading/loading.actions";
import { LoadingEndedAction } from "../../custom-core/loading/loading.actions";
@Injectable()
export class PostsSearchEffects {
constructor(private actions$: Actions, private postsService: PostsService, private store: Store<AppState>,
private logger: LoggingService) {
}
@Effect()
loadPostsSearchResults$ = this.actions$.pipe(
ofType<PostsSearchRequestedAction>(PostsSearchActionType.PostsSearchResultRequested),
mergeMap((action: PostsSearchRequestedAction) => this.postsService.searchPosts(action.payload.searchText)),
map((results: PostsSearchResult[]) => {
return new PostsSearchLoadedAction({ results: results });
})
);
@Effect()
loadSearchResultsPage$ = this.actions$.pipe(
ofType<PostsSearchPageRequestedAction>(PostsSearchActionType.PostsSearchResultsPageRequested),
switchMap(({ payload }) => {
this.logger.logTrace("loadSearchResultsPage$ effect triggered for type PostsSearchResultsPageRequested");
this.store.dispatch(new LoadingStartedAction({ message: "Searching ..."}));
return this.postsService.searchPosts(payload.searchText).pipe(
tap(_ => this.store.dispatch(new LoadingEndedAction())),
catchError(err => {
this.store.dispatch(new LoadingEndedAction());
this.logger.logErrorMessage("Error loading search results: " + err);
this.store.dispatch(new PostsSearchResultsPageCancelledAction());
return of(<PostsSearchResult[]>[]);
})
);
}),
map(searchResults => {
// console.log("loadSearchResultsPage$ effect searchResults: ", searchResults);
const ret = new PostsSearchResultsPageLoadedAction({ searchResults });
this.logger.logTrace("loadSearchResultsPage$ effect PostsSearchResultsPageLoadedAction: ", ret);
return ret;
})
);
}
减速器
These handle the dispatched actions. Each search will trigger a clear of existing information. However, each page request will used the already loaded information.
import { EntityState, EntityAdapter, createEntityAdapter } from "@ngrx/entity";
import { PostsSearchResult } from "../models/posts-search-result";
import { PostsSearchAction, PostsSearchActionType } from "./posts-search.actions";
export interface PostsSearchListState extends EntityState<PostsSearchResult> {
}
export const postsSearchAdapter: EntityAdapter<PostsSearchResult> = createEntityAdapter<PostsSearchResult>({
selectId: r => `${r.questionId}_${r.answerId}`
});
export const initialPostsSearchListState: PostsSearchListState = postsSearchAdapter.getInitialState({
});
export function postsSearchReducer(state = initialPostsSearchListState, action: PostsSearchAction): PostsSearchListState {
switch (action.type) {
case PostsSearchActionType.PostsSearchResultsClear:
console.log("PostsSearchActionType.PostsSearchResultsClear called");
return postsSearchAdapter.removeAll(state);
case PostsSearchActionType.PostsSearchResultsPageRequested:
return state;
case PostsSearchActionType.PostsSearchResultsPageLoaded:
console.log("PostsSearchActionType.PostsSearchResultsPageLoaded triggered");
return postsSearchAdapter.addMany(action.payload.searchResults, state);
case PostsSearchActionType.PostsSearchResultsPageCancelled:
return state;
default: {
return state;
}
}
}
export const postsSearchSelectors = postsSearchAdapter.getSelectors();
选择器
import { createFeatureSelector, createSelector } from "@ngrx/store";
import { PostsSearchListState, postsSearchSelectors } from "./posts-search.reducers";
import { Features } from "../../reducers/constants";
import { PageQuery } from "src/app/custom-core/models/page-query";
export const selectPostsSearchState = createFeatureSelector<PostsSearchListState>(Features.PostsSearchResults);
export const selectAllPostsSearchResults = createSelector(selectPostsSearchState, postsSearchSelectors.selectAll);
export const selectSearchResultsPage = (page: PageQuery) => createSelector(
selectAllPostsSearchResults,
allResults => {
const startIndex = page.pageIndex * page.pageSize;
const pageEnd = startIndex + page.pageSize;
return allResults
.slice(startIndex, pageEnd);
}
);
export const selectSearchResultsCount = createSelector(
selectAllPostsSearchResults,
allResults => allResults.length
);
数据源
这是必需的,因为我正在处理材料表和分页器。它也处理分页:表(实际上是数据源)请求页面,但是如果需要,效果将加载所有内容并返回该页面。当然,后续页面将不会进入服务器以获取更多数据。
import {CollectionViewer, DataSource} from "@angular/cdk/collections";
import {Observable, BehaviorSubject, of, Subscription} from "rxjs";
import {catchError, tap, take} from "rxjs/operators";
import { AppState } from "../../reducers";
import { Store, select } from "@ngrx/store";
import { PageQuery } from "src/app/custom-core/models/page-query";
import { LoggingService } from "../../custom-core/general/logging-service";
import { PostsSearchResult } from "../models/posts-search-result";
import { selectSearchResultsPage } from "../store/posts-search.selectors";
import { PostsSearchPageRequestedAction } from "../store/posts-search.actions";
export class SearchResultsDataSource implements DataSource<PostsSearchResult> {
public readonly searchResultSubject = new BehaviorSubject<PostsSearchResult[]>([]);
private searchSubscription: Subscription;
constructor(private store: Store<AppState>, private logger: LoggingService) {
}
loadSearchResults(page: PageQuery, searchText: string) {
this.logger.logTrace("SearchResultsDataSource.loadSearchResults started for page ", page, searchText);
this.searchSubscription = this.store.pipe(
select(selectSearchResultsPage(page)),
tap(results => {
// this.logger.logTrace("SearchResultsDataSource.loadSearchResults results ", results);
if (results && results.length > 0) {
this.logger.logTrace("SearchResultsDataSource.loadSearchResults page already in store ", results);
this.searchResultSubject.next(results);
} else {
this.logger.logTrace("SearchResultsDataSource.loadSearchResults page not in store and dispatching request ", page);
this.store.dispatch(new PostsSearchPageRequestedAction({ searchText: searchText}));
}
}),
catchError(err => {
this.logger.logTrace("loadSearchResults failed: ", err);
return of([]);
})
)
.subscribe();
}
connect(collectionViewer: CollectionViewer): Observable<PostsSearchResult[]> {
this.logger.logTrace("SearchResultsDataSource: connecting data source");
return this.searchResultSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
console.log("SearchResultsDataSource: disconnect");
this.searchResultSubject.complete();
}
}
组件代码
搜索结果组件接收了搜索词作为查询参数,并转向数据源来加载相应的页面。
import { Component, OnInit, ViewChild, OnDestroy, AfterViewInit } from "@angular/core";
import { Store, select } from "@ngrx/store";
import { AppState } from "src/app/reducers";
import { PostsSearchResultsClearAction } from "../../store/posts-search.actions";
import { ActivatedRoute, Router, ParamMap } from "@angular/router";
import { tap, map } from "rxjs/operators";
import { environment } from "../../../../environments/environment";
import { MatPaginator } from "@angular/material";
import { SearchResultsDataSource } from "../../services/search-results.datasource";
import { LoggingService } from "src/app/custom-core/general/logging-service";
import { PageQuery } from "src/app/custom-core/models/page-query";
import { Subscription, Observable } from "rxjs";
import { selectSearchResultsCount, selectAllPostsSearchResults } from "../../store/posts-search.selectors";
@Component({
// tslint:disable-next-line:component-selector
selector: "posts-search-results",
templateUrl: "./posts-search-results.component.html",
styleUrls: ["./posts-search-results.component.css"]
})
export class PostsSearchResultsComponent implements OnInit, OnDestroy, AfterViewInit {
appEnvironment = environment;
searchResultCount$: Observable<number>;
dataSource: SearchResultsDataSource;
displayedColumns = ["scores", "searchResult", "user"];
searchText: string;
searchSubscription: Subscription;
@ViewChild(MatPaginator) paginator: MatPaginator;
constructor(private store: Store<AppState>,
private route: ActivatedRoute,
private logger: LoggingService) {
console.log("PostsSearchResultsComponent constructor");
}
ngOnInit() {
console.log("PostsSearchResultsComponent ngOnInit");
this.dataSource = new SearchResultsDataSource(this.store, this.logger);
const initialPage: PageQuery = {
pageIndex: 0,
pageSize: 10
};
// request search results based on search query text
this.searchSubscription = this.route.paramMap.pipe(
tap((params: ParamMap) => {
this.store.dispatch(new PostsSearchResultsClearAction());
this.searchText = <string>params.get("searchText");
console.log("Started loading search result with text", this.searchText);
this.dataSource.loadSearchResults(initialPage, this.searchText);
})
).subscribe();
// this does not work due to type mismatch
// Type 'Observable<MemoizedSelector<object, number>>' is not assignable to type 'Observable<number>'.
// Type 'MemoizedSelector<object, number>' is not assignable to type 'number'.
this.searchResultCount$ = this.store.pipe(
select(selectSearchResultsCount));
}
ngOnDestroy(): void {
console.log("PostsSearchResultsComponent ngOnDestroy called");
if (this.searchSubscription) {
this.searchSubscription.unsubscribe();
}
}
loadQuestionsPage() {
const newPage: PageQuery = {
pageIndex: this.paginator.pageIndex,
pageSize: this.paginator.pageSize
};
this.logger.logTrace("Loading questions for page: ", newPage);
this.dataSource.loadSearchResults(newPage, this.searchText);
}
ngAfterViewInit() {
this.paginator.page.pipe(
tap(() => this.loadQuestionsPage())
)
.subscribe();
}
// TODO: move to a generic place
getTrimmedText(text: string) {
const size = 200;
if (!text || text.length <= size) {
return text;
}
return text.substring(0, size) + "...";
}
}
组件标记
<h2>{{searchResultCount$ | async}} search results for <i>{{searchText}} </i></h2>
<mat-table [dataSource]="dataSource">
<ng-container matColumnDef="scores">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let result">
<div class="question-score-box small-font">
{{result.votes}}<br /><span class="small-font">score</span>
</div>
<div [ngClass]="{'answer-count-box': true, 'answer-accepted': result.isAnswered}" *ngIf="result.postType == 'question'">
{{result.answerCount}}<br /><span class="small-font" *ngIf="result.answerCount == 1">answer</span><span class="small-font" *ngIf="result.answerCount != 1">answers</span>
</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="searchResult">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let result">
<a [routerLink]="['/posts', result.questionId]" [routerLinkActive]="['link-active']" id="questionView"
[innerHTML]="'Q: ' + result.title" *ngIf="result.postType == 'question'">
</a>
<a [routerLink]="['/posts', result.questionId]" [routerLinkActive]="['link-active']" id="questionView"
[innerHTML]="'A: ' + result.title" *ngIf="result.postType == 'answer'">
</a>
<span class="medium-font">{{getTrimmedText(result.body)}}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="user">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let result">
<div class="q-list-user-details">
<span class="half-transparency">
{{result.postType == 'question' ? 'Asked' : 'Added'}} on {{result.createDateTime | date: 'mediumDate'}}
<br />
</span>
<a [routerLink]="['/users', result.creatorSoUserId]" [routerLinkActive]="['link-active']" id="addedByView">
{{result.creatorName}}
</a>
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<mat-paginator #paginator
[length]="searchResultCount$ | async"
[pageIndex]="0"
[pageSize]="10"
[pageSizeOptions]="[5, 10, 25, 100]">
</mat-paginator>
<!-- <hr/> -->
<div *ngIf="!appEnvironment.production">
{{(dataSource?.searchResultSubject | async) | json}}
</div>
有很多代码,我认为可以对其进行改进,但是拥有一个惯用的ngrx代码来搜索SPA中的内容是一个好的开始。