Monaca × NCMBでマンガビューワーアプリを作る

NCMBにはいくつかの機能がありますが、データベースのように使えるデータストアや写真などを保存しておくファイルストアはよく使われる機能になります。

今回はそのデータストアとファイルストアを組み合わせて、マンガビューワーアプリアプリを作ります。まず初回となる今回は、アプリの概要とNCMBの初期化を行います。

## 完成版のコード

作成したデモアプリのコードは[NCMBMania/Monaca_Comic: MonacaとNCMBを使ったマンガアプリのサンプルです](https://github.com/NCMBMania/Monaca_Comic)にアップロードしてあります。実装時の参考にしてください。

## ベースについて

今回はMonacaを利用し、UIフレームワークとしてFramework7を用いています。その際のベースとして[NCMBMania/MonacaFramework7Base: Monaca × Framework7 × NCMBのベースアプリです。](https://github.com/NCMBMania/MonacaFramework7Base)を利用しています。執筆時点でのFramework7のバージョンは7.0.2です。

このベースアプリにFramework7のUI部分（NCMB部分除く）を実装したハンズオンテンプレートが下記になります。まず、これをダウンロードして、Monaca IDEにてアップロードします。

[NCMBMania/Monaca_Comic_Handson: Monacaを使ったマンガアプリのハンズオンデータです](https://github.com/NCMBMania/Monaca_Comic_Handson)

## 画面について

今回のアプリは以下の2つの画面を持っています。

- 書籍一覧画面
- 書籍閲覧画面

### 書籍一覧画面

![FireShot Capture 282 - 20220506185731 Monaca × Framework7 × NCMB - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/ababca13-040c-ea76-a034-e0b4b224098c.jpeg)


`pages/list.html` として作成します。データストアから書籍データを取得し、一覧表示します。

### 書籍閲覧画面

![FireShot Capture 282 - 20220506185729 Monaca × Framework7 × NCMB - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/457f9e90-50d4-2db3-ebe4-86c5a5bfceec.jpeg)


`pages/view.html` として作成します。一覧画面で選択された書籍の画像データを取得し、マンガビューワーで表示します。

## 利用するライブラリ

今回は以下のライブラリ・SDKを利用しています。

- NCMB SDK
- [laymic](https://github.com/dettalant/laymic)
- Framework7

NCMB SDKとFramework7は上記ベースアプリに元々含まれています。laymicはWebベースのマンガビューワーになります。

### laymicのインストール（実装済み）

laymicをGitHubのリポジトリからダウンロードし、distの中身を `www/assets/laymic` 以下に保存します。そして `www/index.html` にて読み込みます。

```html
<!-- 記述済み -->
<link rel="stylesheet" href="assets/laymic/laymic.min.css">
<script src="assets/laymic/laymic.iife.min.js"></script>
```

## ルーティングの設定（実装済み）

今回の2画面に合わせてルーティングを変更します。 `js/routes.js` を以下のように修正します。

```js
// 記述済み
const routes = [
  {
    path: '/',
    url: './index.html',
  },
  // コミック一覧画面
  {
    path: '/comics/',
    componentUrl: './pages/list.html',
  },
  // コミック詳細画面
  {
    path: '/comics/:key',
    componentUrl: './pages/view.html',
  },
  {
    path: '(.*)',
    url: './pages/404.html',
  },
];
```

## index.htmlの修正

index.htmlでは初期表示でコミック一覧画面を読み込むように修正します。まだ `pages/list.html` はないので、画面が真っ白になってしまいます。

```html
<!-- 記述済み -->
<div id="app">
	<div class="views tabs safe-areas">
      <div id="view-home" class="view view-main view-init" data-url="/comics/">
      </div>
    </div>
  </div>
</div>
```

## NCMB SDKの初期化

NCMBのWebサイトでアプリを作成し、アプリケーションキーとクライアントキーを取得します。そして `www/js/config.json` を開いてそれぞれのキーを記述します。

```json
{
	"applicationKey": "ここにアプリケーションキー",
	"clientKey": "ここにクライアントキー"
}
```

この `config.json` は `js/app.js` にて読み込まれ、NCMBを初期化処理に利用されます。

```js
// 記述済み
// NCMBの初期化用
const event = window.cordova ? 'deviceready' : 'DOMContentLoaded';
document.addEventListener(event, async (e) => {
  const config = await (await fetch('./js/config.json')).json();
  window.ncmb = new NCMB(config.applicationKey, config.clientKey);
  window.app = new Framework7({
    name: 'My App', // App name
    theme: 'auto', // Automatic theme detection
    el: '#app', // App root element
    // App store
    store: store,
    // App routes
    routes: routes,
  });
});
```

これでNCMBが初期化され、利用準備が整いました。

## データの用意

### マンガデータ

今回はCCなどで公開されている以下のマンガデータを利用しました。ありがとうございます。

- [ブラックジャックによろしく](https://densho810.com/free/)
- [ダーウィン事変](https://afternoon.kodansha.co.jp/news/5183.html)
- [聖☆おにいさん](http://morningmanga.com/st023cc/)

これらのデータをダウンロードし（一部はPDFから画像にした上で）、ページ毎に連番で保存します。たとえばブラックジャックによろしくであれば、bj_1.jpeg・bj_2.jpeg…といった具合です。

そうしたデータをまとめてNCMBのファイルストアにアップロードします。

![FireShot Capture 276 - 20220506155402 ニフクラ mobile backend - console.mbaas.nifcloud.com.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/d4b1145c-30bb-16d6-0245-363850060434.jpeg)

※ ハンズオン用に画像データを以下にアップロードしてあります。ダウンロード、展開してください。

[images.zip](https://github.com/NCMBMania/Monaca_Comic_Handson/releases/download/1.0/images.zip)

### データストア

次にデータストアでComicクラスを作成します。フィールドは次のように作成します。

| フィールド名 | 型 | 意味 |
|-----|-----|-----|
| title | 文字列 | マンガのタイトル |
| key | 文字列 | 画像ファイル名のプリフィックス（bj_など） |

そしてマンガデータを作成します。

![FireShot Capture 277 - 20220506155412 ニフクラ mobile backend - console.mbaas.nifcloud.com.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/9a75621f-5974-d6c5-c21f-775cee16418d.jpeg)

以下は例です。

| title | key |
|---------|---------|
| ブラックジャックによろしく | bj_ |
| ダーウィン事変 | darwinsincident_ |
| 聖☆おにいさん | sym001_p01 |

## 一覧画面の作成

`www/pages/list.html` を作成します。HTMLを含めた基本形は次の通りです。

```html
<!-- 記述済み -->
<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner sliding">
        <div class="title">書籍一覧</div>
      </div>
    </div>
    <div class="page-content">
      <div class="list links-list">
      </div>
    </div>
  </div>
</template>
<script>
  export default (props, { $onMounted, $update }) => {
		// ここにJavaScriptを記述
    return $render;
  };
</script>
```

### 書籍データを取得する

JavaScriptで書籍データを取得する処理を記述します。これは `$onMounted` 関数内で記述します。この関数はDOMのマウントが終わった際に呼ばれます。

```js
// 要記述
let comics = [];
// DOMがマウントされたタイミングで実行
$onMounted(async () => {
	// 1. コミックデータを取得する
	const Comic = ncmb.DataStore('Comic');
	comics = await Comic
		.order('title')
		.fetchAll();
	// 取得したら表示更新
	$update();
});
```

変数 `comics` の中にデータが入った段階で `$update` 関数を呼ぶことで、HTML側に表示を反映できます。 `div.list` の中に一覧表示を追加します。

```html
<!-- 記述済み -->
<div class="list links-list">
	<!-- ここに追加（ここから） -->
	<ul>
		${ comics.map(comic => $h`
			<li><a href="/comics/${comic.key}?title=${comic.title}">${comic.title}</a></li>
		`)}
	</ul>
	<!-- ここに追加（ここまで） -->
</div>
```

これでデータの取得と、画面への反映が完了します。

![FireShot Capture 282 - 20220506185731 Monaca × Framework7 × NCMB - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/9bacc99b-c5b3-c4b3-9126-02ae5e5dbc0e.jpeg)

## ビューワー画面の実装

`www/pages/view.html` を作成します。HTMLを含めた基本形は次の通りです。

```html
<!-- 記述済み -->
<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner sliding">
        <div class="left">
          <a href="#" class="link back">
            <i class="icon icon-back"></i>
            <span class="if-not-md">戻る</span>
          </a>
        </div>
        <div class="title">${ title }</div>
      </div>
    </div>
    <div class="page-content">
			<div class="laymic" data-viewer-id="laymic">
			</div>
		</div>
  </div>
</template>
<script>
  export default (props, { $f7route, $on, $f7router, $update, $tick }) => {
		// ここにJavaScriptを記述
    return $render;
  };
</script>
```

### 変数を準備

まず画面で表示するための変数を準備します。基本的に一覧画面から送られてきた変数を受け取るだけです。

```js
// 記述済み
const { key } = props; // 検索キー
const { title }  = $f7route.query; // 画面表示用のマンガタイトル
let pages = []; // マンガ画像が入る配列
```

### 画面表示処理

画面表示時のイベントでは、以下の処理を行います。

1. ローディングアイコンの表示
1. ファイルストアからの画像取得
1. マンガビューワーの立ち上げ
1. ローディングアイコンの非表示

コードで書くと、次のようになります。

```js
// 記述済み
// 画面を表示する前に実行されるイベント
$on('page:beforein', async () => {
	// ローディング表示
	app.preloader.show();
	// ファイルストアのデータを取得
	const blobs = await getPage();
	// ファイルストアのデータをblob形式に変換
	pages = blobs.map(p => URL.createObjectURL(p));
	// 表示更新
	$update();
	// 描画完了を待つ
	await $tick();
	showLaymic();
	// ローディングを消す
	app.preloader.hide();
});
```

#### ファイルストアからの画像取得

ファイルストアからは、先ほどの `key` を使って取得対象の画像を絞り込みます。また、ファイル名ごとに並べることでページ番号順に取得しています。

ファイルストアを検索しただけではデータのダウンロードまでは行っていませんので、さらに `ncmb.File.download` を使ってダウンロード処理も行っています。

```js
// 要記述
// ファイルストアからデータを取得する関数
const getPage = async () => {
	// 1. ファイル名のキーで検索
	const files = await ncmb.File
		.regularExpressionTo('fileName', `^${key}.*`)
		.order('fileName', false)
		.fetchAll();
	// 2. 写真データをblob形式でダウンロード
	const promises = files.map(f => ncmb.File.download(f.fileName, 'blob'));
	// Promiseで処理を返す
	return Promise.all(promises);
};
```

#### マンガビューワーの立ち上げ

マンガビューワー[laymic](https://github.com/dettalant/laymic)の立ち上げ処理は次のようになります。注意点としては、Framework7の画面が邪魔をしてビューワーが後ろに開いてしまうことです。そのため、今回はlaymicが開いた段階で一旦Framework7全体を非表示にし、laymicを閉じた段階で表示を戻すと言ったことを行っています。

```js
// 記述済み
// 電子書籍を表示する処理
const showLaymic = () => {
	// Laymicの表示
	const applicator = new laymic.LaymicApplicator(".laymic", {
		pageWidth: 690,
		pageHeight: 976,
	});
	applicator.open('laymic');
	// Framework7自体を一旦隠す
	app.el.style.display = 'none';
	// 電子書籍を閉じる際にFramework7を表示する
	$('.laymic_close').on('click', e => {
		app.el.style.display = 'block';
		// 一覧ページに戻る
		$f7router.back();
	});
}
```

#### 画面描画内容

`pages` 変数にはblob:ではじまるURLが配列になっています。これを画面上に表示します。

```html
<!-- 記述済み -->
<div class="laymic" data-viewer-id="laymic">
	<!-- 追記（ここから）-->
	${ pages.map(page => $h`
		<img data-src="${page}" />
	`)}
	<!-- 追記（ここまで）-->
</div>
```

さらに標準のCSP（Content-Security-Policy）ではblobが使えないので `www/index.html` を修正します。

```html
<!-- 記述済み -->
<meta http-equiv="Content-Security-Policy" content="default-src * 'self' 'unsafe-inline' 'unsafe-eval' data: gap: content: blob:"> <!-- 最後に blob: を追加-->
```

これでマンガビューワーが立ち上がるでしょう。

![FireShot Capture 282 - 20220506185729 Monaca × Framework7 × NCMB - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/5561522e-145f-b7b8-8ad4-c9acd52e1507.jpeg)