【Angular基礎講座】動的/静的ウェブサイト内のページネイション作成 〜 パスパラメータ/クエリパラメータ付きのURLアドレスからルーティングする


2020/09/29

ウェブページのコンテンツが増えてくると、一つのページのコンテンツの全てを載せていてはとても大きなファイルになるため、リストでコンテンツを小分けしたページ分割を検討していく必要があります。

おそらくそのような場合、
/home/hoge/piyoといったようなパスパラメータか、/home?param1=hoge&param2=piyoのようなクエリパラメータを使用したURLアドレスを設定して小分けしたページへナビゲーションすることになります。

今回はAngularでの動的・静的ウェブページのURLパラメータによるルーティングをどう実現するのか検討してみます。


TL;DR

Angularで動的なウェブページをページング処理する際には、クエリパラメータを用いたルーティングを用いた方が実装しやすいです。

その際には
ActivatedRouteクラスのqueryParamsを利用します。

            
            // `/home?param1=hoge&param2=piyo`にアクセスしたときのparam1とparam2を取得
constructor(private route: ActivatedRoute) {
    const param1: string = this.route.snapshot.queryParamMap.get('param1'); // hoge
    const param2: string = this.route.snapshot.queryParamMap.get('param2'); // piyo
}
        
静的なウェブページは、各階層のindex.htmlにアクセスさせる必要があります。

そこでパス変数(パスに
:を使って、その後ろにパラメータ名を付けて使うとコンポーネント側でパラメータを参照できる)を利用できるようにする修正を行いたい場合があります。

この場合、
ActivatedRouteクラスのsnapshotparamsプロパティを使って実装するようにします。

            
            // `/home/:param1/:param2`とパスパラメータを定義して、
// 例えば/home/hoge/piyoを取得
constructor(private route: ActivatedRoute) {
    const param1: string = this.route.snapshot.paramMap.get('param1'); // hoge
    const param2: string = this.route.snapshot.paramMap.get('param2'); // piyo
}
        


動的なサイトのページング処理

Angularを使った動的なページネーションは主に、リクエストしたurl文字例のサフィックスからパラメータを読み取り、内部のjsスクリプトがコンポーネントを適切にhtmlをレンダリングして描画する処理を行っています。

合同会社タコスキングダム|蛸壺の技術ブログ

レンダリングの作業をクライアント側のブラウザが行うか、サーバーサイドからレンダリングして返されるかの違いはあります。

基本的には同一のルート(ここでは
/home)にその場限りの動的なDOMが生成されていて、結果としてページが切り替わってみえます。

app-routingモジュール

まず最初にプロジェクトのapp-routing.module.tsを確認しておきましょう。

この記事では割愛しますが、もしまだプロジェクトにapp-routing.module.tsを実装していない場合には、angular公式ガイドの
ルーティングを使ったアプリ内ナビゲーションの追加を参照して導入してみてください。

今回は以下のようなルーティング構造でページング処理を行います。

            
            import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const routes: Routes = [
    { path: 'error', component: PageNotFoundComponent },
    { path: 'home', component: HomeComponent },
    { path: '', redirectTo: '/home', pathMatch: 'full' },
    { path: '**', redirectTo: '/error', pathMatch: 'full' }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }
        


動的なサイトのページング処理

今回はコンテンツを10件ごとに小分けして、1つのページで区切るようにしようと思います。

以下は、例えば基点となるHomeComponent(
/home相当)に対して、1ページ目は/home?page=1、2ページ目は/home?page=2...というクエリパラメータのルールでページング処理を実装したコード例です。

            
            import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
    selector: 'app-root',
    template: `
        <div>
            <ul>
                <ng-container *ngFor="let article of articleList | slice:startPos:startPos+listLength">
                    <li>
                        <a href="{{article.url}}">{{article.title}}</a>
                        <p>{{article.subtitle}}</p>
                    </li>
                </ng-container>
            </ul>
            <ng-container *ngIf="articleList.length === 0">
                <div>
                    <p>このカテゴリーにはまだ記事が有りません。</p>
                </div>
            </ng-container>
        </div>
    `,
    styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
    startPos: number = 0; // リストの最初の要素番号
    listLength: number = 10; // リストへの最大表示数

    articleList: any[] = [
        {
            url: '/blog/1',
            title: 'Hogeの日記',
            subtitle: '今日はお肉の日でした。',
        },
        {
            url: '/blog/2',
            title: 'Piyoの日記',
            subtitle: '今日はおさかなの日でした。',
        },
        //...中略
    ];

    constructor(
        private route: ActivatedRoute
    ) { }

    ngOnInit() {
        // リスト表示に必要なページ数の更新
        const totalPages: number = Math.floor(this.articleList.length / this.listLength) + 1;

        // リクエストされたルートのクエリパラメータを取得
        let pageId: number = parseInt(this.route.snapshot.queryParamMap.get('page'), 10); // 1,2,3...

        if (!pageId || pageId <= 0) {
            // 1より小さい値はとらない
            pageId = 1;
        } else if (pageId <= totalPages) {
            // そのページの描画開始位置を算出
            this.startPos = this.listLength * (pageId - 1);
        } else {
            // 最大のページ数を越えないようにする
            this.startPos = this.listLength * (totalPages - 1);
        }
    }
}
        
html部分を見ていただくと、ngForを使ってリストのメンバー要素を繰り返して、DOMを生成しています。

sliceパイプを使うと、javascriptを用いなくとも簡単に指定した要素分だけを抽出することができます。

ちなみに
sliceは配列を内部で扱うのでゼロからはじまるインデックス位置になります。

ページ毎に、コンテンツリストの最初の位置と描画長までを取り出しています。

さて今回の実装では、
this.route.snapshot.queryParamMap.get('page')の部分がクエリパラメータから読み取ったpage=*に当たります。

何ページ目かを読み取った後にDOMを生成しなければならないので、クエリパラメータの読み取りは
constructorngOnInitに記述する必要があります。

クエリパラメータを用いたページの遷移

ページをクエリパラメータから生成・表示させるロジックは上記の通りですが、プログラム側からアクセスさせることもできます。

Angularでクエリパラメータを使ったナビゲーションの方法として、Componentのtsソース側に実装する場合には下記のようになります。

            
            // /home?page=1
this.router.navigate(
    ['/home'],
    {
        queryParams: {
            page: 1
        }
    }
);
        
htmlコード内で利用する場合には、routerLink属性を使うことで同様のことが可能になります。

            
            <!-- /home?page=1 -->
<a [routerLink]="['/home']" [queryParams]="{ page: 1 }"></a>
        

静的なサイトのページング処理

先ほどの動的なページネーションとは違い、静的なウェブサイトでは、サーバー側に予めレンダリングしておいたindex.htmlを階層構造ごとに取り貯めておく必要があります。

このときリクエストされたurl文字列に応じた
index.htmlをクライアントに送り返します。

合同会社タコスキングダム|蛸壺の技術ブログ

動的なページネーションではユーザーがurlをリクエストしてからその場限りのページがレンダリングされることで動作しています。

なので、ユーザーが
/homeにアクセスした時には/home/index.htmlにルーティングされ、/home/1になら/home/1/index.htmlといった具合に、静的なホスティングサイトは必ず実体のあるhtmlファイルがサーバー側から返される必要があります。

angularにおける静的なサイトのページング処理の方法と比べて、上記までの動的なページングよりも複雑です。

プロジェクトをビルドするときに、angularのコンパイラにどのルートをプリレンダリングしておくのか明示に教えてあげるという一手間が必要になります。

以下では簡単にプリレンダリングしたhtmlでのページネーションの実装手順を解説します。

app-routingモジュールの修正

angularのルーティングにおいて、パスパラメータを使うためには、:から始まる変数を定義する必要があります。

            
            import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const routes: Routes = [
    { path: 'error', component: PageNotFoundComponent },
    { path: 'home', component: HomeComponent },
    { path: 'home/:page', component: HomeComponent }, // 👈追加
    { path: '', redirectTo: '/home', pathMatch: 'full' },
    { path: '**', redirectTo: '/error', pathMatch: 'full' }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }
        
ここでは、home/以下にpageというパスパラメータを設定しておきましょう。

サーバーサイドレンダリング(SSR)

通常angularのSSRには、Angular Universalを用いることになります。

こちらで紹介されているプリレンダリングページへのルーティングの手順に従うと、

            
            1. ビルド時のコマンドのオプション--routesに指定のパスパラメータを追加する
2. angular.jsonのprerenderのroutesフィールドにパスパラメータを追加する
3. ビルドコマンドのオプション--routesFileを使って、
    dynamic-routes.txtを指定し、このテキストにパスを追加する
4. angular.jsonのprerenderのroutesFileフィールドに、
    dynamic-routes.txtのパスを追加し、ビルド時に読み込ませる
        
の4つの方法のどれかで、静的なウェブサイトのページング処理を行うことが可能です。

とはいえこのやり方ですと、angularのビルドコマンドを直接利用している力技になります。

そこで、
angular-prerenderというツールを利用した方がもう少しマイルドにCLIでビルド管理が出来ます。

angular-prerenderはAngular Universalで直にビルドオプションを弄るよりは随分とスマートにコンパイル処理を行ってくれます。

コマンドオプション
--parameter-valuesを使うことで、定義していたパスパラメータを自動で判別して、指定されたパス直下にindex.htmlを生成します。

今回の例では、
:pageというパスパラメータをapp-routingモジュールに定義しましたので、angular-prerenderは自動でこれを捉えてビルドをしてくれます。

            
            $ npx angular-prerender --parameter-values '{":page":["1","2","3","4","5","6"]}'
/home/1
/home/2
/home/3
/home/4
/home/5
/home/6
        
もちろん:pageに指定するサフィックスの値までは自動化できません。

全てのビルド作業を自動化したい場合はシェルスクリプト等でCLIを使ったカスタマイズをする必要があります。

静的なサイトのページング処理

最後にパスパラメータを適切に処理できるように、先程のHomeComponentクラスを手直しします。

            
            import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
    selector: 'app-root',
    template: `
        <div>
            <ul>
                <ng-container *ngFor="let article of articleList | slice:startPos:startPos+listLength">
                    <li>
                        <a href="{{article.url}}">{{article.title}}</a>
                        <p>{{article.subtitle}}</p>
                    </li>
                </ng-container>
            </ul>
            <ng-container *ngIf="articleList.length === 0">
                <div>
                    <p>このカテゴリーにはまだ記事が有りません。</p>
                </div>
            </ng-container>
        </div>
    `,
    styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
    startPos: number = 0; // リストの最初の要素番号
    listLength: number = 10; // リストへの最大表示数

    articleList: any[] = [
        {
            url: '/blog/1',
            title: 'Hogeの日記',
            subtitle: '今日はお肉の日でした。',
        },
        {
            url: '/blog/2',
            title: 'Piyoの日記',
            subtitle: '今日はおさかなの日でした。',
        },
        //...中略
    ];

    constructor(
        private route: ActivatedRoute
    ) { }

    ngOnInit() {
        // リスト表示に必要なページ数の更新
        const totalPages: number = Math.floor(this.articleList.length / this.listLength) + 1;

        // リクエストされたルートのパスパラメータを取得
        let pageId: number = parseInt(this.route.snapshot.paramMap.get('page'), 10); // 1,2,3...

        if (!pageId || pageId <= 0) {
            // 1より小さい値はとらない
            pageId = 1;
        } else if (pageId <= totalPages) {
            // そのページの描画開始位置を算出
            this.startPos = this.listLength * (pageId - 1);
        } else {
            // 最大のページ数を越えないようにする
            this.startPos = this.listLength * (totalPages - 1);
        }
    }
}
        
見ていただくと分かる通り、this.route.snapshot.paramMapに変わっただけですが、これでプリレンダリングに対応したページネーションを実装できるようになりました。


まとめ

今回はAngularを用いた静的・動的なウェブページのページング処理のための簡単な概論として記事にしてみました。

Angular Universalの登場から直近まででSSR・プリレンダリングの技術は大分使いものになって来た印象です。

ただ、まだまだ不安定な要素も多いため今後もより使いやすいものに開発が進んでいくと思われます。


参考サイト

Angularで「ページング」処理を実装するには?(ngFor/slice)

Prerender Angular Application using Angular Universal Prerenderer


Angularの学び方

主要なJavascriptのフレームワークの一角であるAngularは、現在Google主導で開発が進められている玄人フロントエンジニア向けのフレームワークです。

Angularを使えばかなり高度なWebアプリも自由に作成できるのですが、やはり初心者が独学で勉強しようとおもうとかなりの知識が必要でAngularは挫折しやすいと言われています。

特にAngularを使う上で注意が必要なのは半年に一回のペースでプログラムのメジャーバージョンが繰り上がるので、他のフレームワークと比べても格段に技術を追っかけるスピードが早いことも躓く人が多い一因になっているとも思います。

Udemyの動画講座では、基礎からじっくり学べるためより実践的なスキルを身につけることが可能です。以下の講座ではAngular初心者向けに簡単なプロジェクトの作成まで学ぶことができます。

より実践的にフロントエンジニアとして重宝されるためにはSPA(シングルページアプリケーション)をある程度作成できるスキルが必要になりますので、さらに学びたい方は以下のような講座も用意されています。

他にもAngularプログラミングの習得度に応じた講座も多数用意されているので、必要に応じて試されてはいかがでしょうか。