HSP3Dish.jsでセーブ

目次

はじめに

 この記事では、HSP3.7β9 HSP3Dish.js の内容について記載しています。 今後のバージョンアップで変更される可能性がある点に注意してください。

セーブ機能がほしい

 ゲームやアプリには、保存機能はやっぱりほしいですよね。 ゲームでセーブできないのは不便です。

 もちろんHSP3にもデータを保存/読込する命令が用意されています。(bsave/bload, notesave/noteload, bmpsave/picload) これらの命令を使うと、Windowsの場合データはファイルに保存されて簡単に利用できます。

 ところがHSP3Dish.jsを使用した場合は、ファイルへの出力ができません。 HSP3Dish.jsはブラウザのJavaScriptで動作しています。 ウェブブラウザで動くJavaScriptは、基本的に閲覧者側のファイルシステムにアクセスすることはできません。 またサーバー側にファイルを置くことも簡単にはできません。(File APIの話は、今回はしません。)

 しかしHSP3Dish.jsでも保存機能は必要です。 というわけで、ウェブアプリでのデータの保存方法、HSP3Dish.jsで保存機能を実装する方法について調べてみました。

ウェブアプリでのデータ保存方法

 ウェブブラウザでデータ保存が出来そうな方法をいくつか調べてみました。 多少の間違いはあるかもしれないので、正確な情報はご自身で確認してください。

Cookie

 Webサイト内でユーザーが入力した情報などを保存するために使ったりするものです。次のような特徴があります。

  • 保存場所:ブラウザ内(デバイスのストレージ)
  • 容量:最大 4KB
  • 保存できるのは文字列のみ
  • サーバー側からも参照できる
  • 有効期限があり、時間経過などで削除される。

IndexedDB

 Webブラウザに実装されているデータを保存するデータベースの機能です。 IndexedDB API の機能で、ブラウザを閉じてもデータは消えません。 有効期限なしでデータを保存し、JavaScript によるクリア、もしくは、ブラウザーに保存したデータのクリアによってのみクリアされます。 次のような特徴があります。

  • 保存場所:ブラウザ内(デバイスのストレージ)
  • 容量:ストレージの空き容量の10%
  • JavaScriptだけで読み書きが出来る。だいぶめんどくさい。
  • テキスト以外にもバイナリデータを保存できる
  • 同一ドメイン内ならデータを共有できる。ドメインが異なるとアクセスできない。
  • オフラインでも読み書きできる。

localStorage

 Webブラウザが管理するデータの保存領域です。 Web Storage APIの一部で、ブラウザを閉じてもデータは消えません。 有効期限なしでデータを保存し、JavaScript によるクリア、もしくは、ブラウザーキャッシュ/ローカルに保存したデータのクリアによってのみクリアされます。 次のような特徴があります。

  • 保存場所:ブラウザ内(デバイスのストレージ)
  • 容量:最大 5MB
  • JavaScriptだけで読み書きが出来る。簡単。
  • テキストしか保存できない。
  • 同一ドメイン内ならデータを共有できる。ドメインが異なるとアクセスできない。
  • オフラインでも読み書きできる。

サーバー上のDB(データベース)

 サーバー上にDBとAPIサーバーまたはCGIを置く。 ブラウザ側からAPIを実行して、サーバー上のDBにデータを保存します。 掲示板やゲームのスコアランキングなどで見られる昔からある方法です。 サーバー側での準備が必要なので大変。データはサーバー上に保存されるので消えたり改ざんされる心配がない。 次のような特徴があります。

  • 保存場所:サーバー上のDB
  • 読み書きにはDB、APIやCGIを置けるサーバーと、JavaScriptが必要。すごく大変。
  • 保存時と違うデバイスを使っていても、保存したデータを利用できる。
  • ランキングなどのように、ユーザーデータを公開可能。

Firebase

 Google が提供するモバイルや Web アプリケーションの開発プラットフォーム。 BaaSと呼ばれるサービスの一種。データベース管理やファイルの蓄積、ユーザー認証などのよく使う機能が提供されています。 サーバー上にDBを置く方法を、使いやすくしてくれているサービスです。 次のような特徴があります。

  • 保存場所:Firebase(Googleのサーバー)
  • 容量:プランによる(無料版あり)
  • Googleアカウントが必要。
  • 個人開発の小規模なものなら無料枠でも十分かも。
  • プッシュ通知機能を付ける際にも利用できる。
  • 保存時と違うデバイスを使っていても、保存したデータを利用できる。
  • ランキングなどのように、ユーザーデータを公開可能。

保存方法の比較

 いっぱい書いてあってなんだか分からないので、表にまとめてみます。

CookieIndexedDBlocalStorageFirebaseなど
容量制限4KBストレージの10%5MBサーバー側で管理(プランによる)
データ形式文字列オブジェクト文字列だいたいなんでも
有効期限有効期限の設定あり手動で消すまで手動で消すまでサーバー側で管理
データアクセスの範囲同一ドメイン内同一ドメイン内同一ドメイン内インターネット経由でどこからでも
保存する用途の例入力したログインIDなどゲームのセーブデータ、大量のアイテムデータアプリの設定、ゲーム進行状況スコアランキング

 Cookieはゲームのセーブに向いてないですね。一番便利そうなのはFirebaseですが、HSP3の外でやる作業が多そうです。HSP3側での実装もJavaScriptの準備が多くて大変そうです。 ローカルに保存される方法の中では、IndexedDBは容量も大きくデータの種類も選ばないので使い勝手がよさそうです。

 調べてみるとHSP3.7β9のHSP3Dish.jsでは、IndexedDBとlocalStorageが使用できるそうです。というわけで、ここからはこの2つのHSP3Dish.jsでの実装をやってみます。

IndexedDB

 HSP3Dish.jsではマニュアルには記述はありませんが、 実装されている機能からの推測としてIndexedDBへの対応が検討されているようです。

 HSP3Dish.jsで保存機能を実装している方々からの情報を頼りにやり方を調べてみました。 マニュアル記載外の機能なので、真似する場合は自己責任でお願いします。

IndexedDB:作例と設置方法

 こちらが作例です。 作例:./dish/save01.html

HSP3

; スマホ向けHSP3Dishのサンプル
; 保存のテスト
; indexedDb
#include "hsp3dish.as"

strVar = ""
intVar = 0
dblVar = 0.0
dim arrVar, 100
buf_note = ""
msg = ""

objsize 200
strObjStr = "文字列"     : input strObjStr, , , 128 : idObjStr = stat
strObjInt = "1234"       : input strObjInt, , , 128 : idObjInt = stat
strObjDbl = "3.14"       : input strObjDbl, , , 128 : idObjDbl = stat
strObjArr = "10, 20, 30" : input strObjArr, , , 128 : idObjArr = stat

button gosub "保存", *l_save_HSP_SYNC_DIR
button gosub "読込", *l_load_HSP_SYNC_DIR
button gosub "削除", *l_clear_HSP_SYNC_DIR
button gosub "コピー", *l_copy_HSP_SYNC_DIR
button gosub "note保存", *l_save_note
button gosub "note読込", *l_load_note
button gosub "dir", *l_dir
redraw 1

;	メインループ
*main
	redraw 1 : await 16 : redraw 0 : color 255, 255, 255 : boxf : color : pos 0,0

	; note
	pos 0,260
	mes "" + buf_note
	mes "" + msg

	goto *main

stop

;	HSP_SYNC_DIR 保存
*l_save_HSP_SYNC_DIR
	; 入力ボックス内の文字列をそれぞれのデータ型に変換
	strVar = strObjStr		; 文字列
	intVar = int(strObjInt)	; 整数
	dblVar = double(strObjDbl)	; 実数
	split strObjArr, ",", b	; 整数配列
	n = stat
	if n > 0 {
		dim arrVar, n
		repeat n
			arrVar(cnt) = int(b(cnt))
		loop
	} else {
		dim arrVar, 1
	}

	; 保存
	bsave "/save/strVar", strVar
	bsave "/save/intVar", intVar
	bsave "/save/dblVar", dblVar
	bsave "/save/arrVar", arrVar
	devcontrol "syncfs"
	return

;	HSP_SYNC_DIR 読み込み
*l_load_HSP_SYNC_DIR
	fname = "/save/strVar"
	exist fname
	if strsize > 0 {
		strVar = ""
		bload fname, strVar
		objprm idObjStr, strVar
	}

	fname = "/save/intVar"
	exist fname
	if strsize > 0 {
		intVar = 0
		bload fname, intVar
		objprm idObjInt, "" + intVar
	}

	fname = "/save/dblVar"
	exist fname
	if strsize > 0 {
		dblVar = 0.0
		bload fname, dblVar
		objprm idObjDbl, "" + dblVar
	}

	fname = "/save/arrVar"
	exist fname
	if strsize > 0 {
		arrVar = 0,0,0
		bload fname, arrVar
		s = strf("%d, %d, %d", arrVar(0), arrVar(1), arrVar(2))
		objprm idObjArr, s
	}
	redraw 1
	return

;	HSP_SYNC_DIR DB削除
*l_clear_HSP_SYNC_DIR
	; #Error 12 --> File I/O error
	fname = "/save/strVar"
	exist fname
	if strsize > 0 {
		strVar = ""
		delete fname
		objprm idObjStr, strVar
	}
	devcontrol "syncfs"
	
	; #Error 12 --> File I/O error
	; delete "/save"
	return

;	HSP_SYNC_DIR コピー
*l_copy_HSP_SYNC_DIR
	bcopy "/save/strVar", "/save/strVar_copy"
	bcopy "/save/intVar", "/save/intVar_copy"
	bcopy "/save/dblVar", "/save/dblVar_copy"
	bcopy "/save/arrVar", "/save/arrVar_copy"
	devcontrol "syncfs"
	return

;	note保存
*l_save_note
	notesel buf_note
	noteadd "note系命令のテスト"
	noteadd "行の追加テスト"
	notesave "/save/hsp_note"
	devcontrol "syncfs"

	buf_note = "notesave...ok"
	return

;	note読み込み
*l_load_note
	buf_note = ""
	fname = "/save/hsp_note"
	exist fname
	if strsize > 0 {
		notesel buf_note
		noteload fname
	}
	return

; dirlist のテスト
*l_dir
	s = ""
	msg = ""
	dirlist s, "*"
	msg = s
	return

HSPスクリプトエディタのメニューから ツール>HSP3Dish / Cソース変換 をクリックして、HSP3Dish helperを起動します。

「変換」を押して、HSP3Dish.jsに変換します。

出てきたhtmlをテキストエディタで開いて、ENV.HSP_LIMIT_STEPを探します。 次の行にENV.HSP_SYNC_DIRを追加します。

JavaScript

      ・・・
      ENV.HSP_LIMIT_STEP = "15000";//ブラウザに処理を返すまでの実行ステップ数
      ENV.HSP_SYNC_DIR = '/save';
      

HSP3Dish.js、.data、.hmtlをサーバーにアップロードすれば完成です。

IndexedDB:使い方

 アプリの使い方です。

保存 入力ボックスの内容をIndexedDBに保存します。
読込 IndexedDBの内容を読み込んで、入力ボックスに表示します。
削除 IndexedDBに保存したデータを削除します。機能しません。
コピー IndexedDBに保存したデータをコピーしてIndexedDBに保存します。
note保存note系命令でテキストを保存します。
note読込note系命令でテキストを読み込みます。
dir dirlist命令のテスト。

 これだけでは何が起きているか把握が難しいので、デベロッパーツールで動きを確認します。

  1. デベロッパーツールのアプリケーションを開きます。
  2. 左側に表示されたツリーのIndexedDBを開きます。
  3. 閲覧中のサイトのドメインで使用しているIndexedDBを見ることができます。
  4. /saveがHSP3が作成したデータベース名、FILE_DATAがオブジェクトストアです。
  5. データを保存するとオブジェクトストア内に"/save/strVar"などのキーと値が出てきます。
    出てこない場合は、右クリックして「更新」をクリックしてください。

ストレージ
▼indexedDB
  └myDatabease	データベース名
    └myObjectStore	オブジェクトストア
        キーと値が対となって保存されている。
        

IndexedDB:解説 保存

JavaScript

	ENV.HSP_SYNC_DIR = '/save';
  

 htmlにHSP_SYNC_DIRを追加することで、IndexedDBの利用が可能になるようです。 引数として渡したデータベース名「/save」でデータベースが作成されます。 最初の一文字目は「/(スラッシュ)」である必要がありますが、「save」の部分はスラッシュを含まなければ何でもいいようです。(IndexedDBの仕様では日本語やスペース、数字や記号が使えます。HSP3側が対応しているかはわかりません。) オブジェクトストア「FILE_DATA」は変更できません。

HSP3

	bsave "/save/strVar", strVar
	devcontrol "syncfs"
  

 HSP3からのデータ保存はbsaveで行います。ファイル名の代わりに「/save/strVar」をキーとして渡すと変数の内容を保存できます。 「/save」の部分がデータベース名です。「データベース名/重複しない名前」という感じで指定します。 「/save」フォルダに「strVar」というファイル名で保存する、というイメージで使えるようにしてあるようです。

 データベース名(/saveの部分)はwebアプリごとに独自の名称を使用した方がよさそうです。 IndexedDBは同一ドメイン内で共有しています。 同一ドメイン内に複数のアプリを置いていて、データベース名を同じにしているとキー名が競合してしまう可能性があります。 アプリごとにデータを管理するには、アプリごとにデータベース名を変えておくと安全です。

 最後に、bsaveで書き込んだらdevcontrol "syncfs"を実行します。意味は分かりません。だって掲示板に書いてあったんだもん! (きっと更新した内容をデータベースに反映するとかそういうやつ。)

IndexedDB:解説 読み込み

HSP3

	exist "/save/strVar"
	if strsize > 0 {
		strVar = ""
		bload "/save/strVar", strVar
		objprm idObjStr, strVar
	}

 existstrsizeでデータベースにキーが村債することを確認します。 ファイルの有無を確認するのと同じ要領です。

 データの読み込みは、bloadで行います。ファイル名としてキー名を指定します。 読み込んだ後は、Windows環境での利用時と同じように使用することができます。

IndexedDB:解説 削除、コピー

 作成したデータベース、オブジェクトストア、キーは削除する方法が用意されていません。 delete命令ではエラーが出て削除することができません。今後のバージョンで対応されるといいですね。

 IndexedDBの削除を行う場合、同じキー名をサイズゼロで上書きするか、JavaScriptで削除するスクリプトを用意する必要があります。 またIndexedDBは、サイトのキャッシュを削除しても消えません。手動で消す場合は、デベロッパーツールを使って削除する必要があります。

 データの削除はできませんが、保存したデータはbcopy命令でコピーが可能です。説明の必要もないくらい簡単です。

IndexedDB:解説 note系命令

 note系命令での読み書きも可能です。注意点はbsave/bloadと同じです。

HSP3

; 読み込み
	notesel buf_note
	noteadd "note系命令のテスト"
	noteadd "行の追加テスト"
	notesave "/save/hsp_note"
	devcontrol "syncfs"
HSP3

; 書き込み
	buf_note = ""
	exist "/save/hsp_note"
	if strsize > 0 {
		notesel buf_note
		noteload "/save/hsp_note"
	}

 説明いらないぐらい簡単です。

localStorage

 HSP3Dish.jsでは、exec命令でJavaScriptが実行可能です。 この機能を使ってlocalStorageの読み書きに対応させることが可能です。

 localStorageは使用できる容量が5MBと小さく、テキストしか保存できません。 またドメイン内で共有され階層構造も持たないため、アプリ間でキーの競合が発生しやすい状況です。 使用する場合は、ドメイン何の全アプリ共通の設定などを保存させるような使い方がいいと思います。 サイズが大きいものも避けた方がいいです。

localStorage:作例と設置方法

 こちらが作例です。 作例:./dish/save02.html

HSP3

; スマホ向けHSP3Dishのサンプル
; 保存のテスト
; localstorage
#include "hsp3dish.as"
#module
; JavaScript エスケープ処理
#defcfunc escapeJS str p_intxt
	res_txt = p_intxt
	strrep res_txt, "\\", "\\\\"	; \ → \\
	strrep res_txt, "'" , "\\'"		; ' → \'
	strrep res_txt, "\"" , "\\\""	; " → \"
	strrep res_txt, "/" , "\\/"		; / → \/
	strrep res_txt, "<" , "\\x3c"	; < → \x3c
	strrep res_txt, ">" , "\\x3e"	; > → \x3e
	strrep res_txt, "\n" , "\\n"	; \n → \\n
	strrep res_txt, "\r" , "\\r"	; \r → \\r
	return res_txt
	
; JavaScript エスケープ処理を戻す
#defcfunc unescapeJS str p_intxt
	res_txt = p_intxt
	strrep res_txt, "\\'",   "'"	; \' → ' 
	strrep res_txt, "\\\"",  "\""	; \" → " 
	strrep res_txt, "\\/",   "/"	; \/ → / 
	strrep res_txt, "\\x3c", "<"	; \x3c → < 
	strrep res_txt, "\\x3e", ">"	; \x3e → > 
	strrep res_txt, "\\n" ,  "\n"	; \\n → \n
	strrep res_txt, "\\r" ,  "\r"	; \\r → \r
	strrep res_txt, "\\\\",  "\\"	; \\ → \ 
	return res_txt
#global

; キー
keyName = "HSP3_key"
; 読み込み用バッファ
sdim strbuf, 128
; 保存するテキスト
strObj = "alert('Hello !')"
; 確認用
exec_text = ""
msg = ""

; GUI
objsize 200
input strObj, , , 256 : idObjStr = stat

button gosub "保存", *l_save
button gosub "読込", *l_load
button gosub "削除", *l_clear
button gosub "実行", *l_run
redraw 1

;	メインループ
*main
	redraw 1 : await 16 : redraw 0 : color 255, 255, 255 : boxf : color : pos 0,0

	pos 0, 130
	mes exec_text
	mes msg
	
	goto *main

stop

;	保存
; 入力された文字列をそのまま使うと危険なので、エスケープ文字に変換してから保存作業をする。
*l_save
	; localStorage.setItem('キー', '保存する文字列');
	exec_text = "localStorage.setItem('" + keyName + "', '" + escapeJS(strObj) + "');"
	exec exec_text
	msg = "save:\n" + strObj
	return

;	読み込み
; JavaScript側からHSP3側の受け取り用変数に値を書き込む。
; エスケープ文字に変換してあるので元に戻す。
*l_load
	ptrStrbuf  = varptr(strbuf)
	sizeStrbuf = varsize(strbuf)
	; {
	;   const ptr = HSP側の変数のポインタ;
	;   const str = localStorage.getItem('キー');
	;   if(!!str){
	;     stringToUTF8Array(str, Module.HEAP8, ptr, HSP側の変数のバッファサイズ);
	;   }
	; }
	exec_text = "{"
	exec_text+= "const ptr = " + ptrStrbuf + ";"
	exec_text+= "const str = localStorage.getItem('" + keyName + "');"
	exec_text+= "if(!!str){
	exec_text+= "  stringToUTF8Array(str, Module.HEAP8, ptr, " + sizeStrbuf + ");"
	exec_text+= "}"
	exec_text+= "}"
	exec exec_text
	strbuf = unescapeJS(strbuf)
	objprm idObjStr, strbuf
	msg = "load:\n" + strbuf
	return

;	削除
*l_clear
	strbuf = ""
	objprm idObjStr, ""
	; localStorage.removeItem('キー');
	exec_text = "localStorage.removeItem('" + keyName + "');"
	exec exec_text
	msg = "delete !"
	return

;	実行
; 入力された文字列をそのまま実行
*l_run
	exec_text = strObj
	exec exec_text
	msg = "run !"
	return

 HSP3Dish helperで作成した、HSP3Dish.js、.data、.hmtlをサーバーにアップロードすれば完成です。

localStorage:使い方

 アプリの使い方です。

保存入力ボックスの内容をlocalStorageに保存します。
読込localStorageの内容を読み込んで、入力ボックスに表示します。
削除localStorageに保存したデータを削除します。機能しません。
実行入力ボックスの内容をそのまま実行します。

 これだけでは何が起きているか把握が難しいので、デベロッパーツールで動きを確認します。

  1. デベロッパーツールのアプリケーションを開きます。
  2. 左側に表示されたツリーのローカル ストレージを開きます。
  3. 閲覧中のサイトのドメインで使用しているローカル ストレージを見ることができます。
  4. webアプリを置いているドメイン「https://~」を開いてください。
  5. 保存されているキーと値の一覧が表示されます。

ストレージ
▼ローカル ストレージ
  └htts://example.com
      キーと値が対となって保存されている。

localStorage:解説 保存

 exec命令で次のようなJavaScriptを実行するだけで文字列を保存できます。

JavaScript

	localStorage.setItem('キー', '保存する文字列');

 HSP3側では、保存する文字列に「'(シングルクォーテーション)」などが入っていると予期しない動作を起こす可能性があるので、 保存する文字列の一部をJavaScriptのエスケープ文字に変換しています。

 キーには記号や日本語なども使用できます。 しかし予想外のトラブルを避けたいなら、英数字とアンダースコアのみを使用した方がよさそうです。

localStorage:解説 読み込み

 exec命令で次のようなJavaScriptを実行すると、localStorageから文字列を読み込めます。

JavaScript

	{
	  const ptr = HSP側の変数のポインタ;
	  const str = localStorage.getItem('キー');
	  if(!!str){
	    stringToUTF8Array(str, Module.HEAP8, ptr, HSP側の変数のバッファサイズ);
	  }
	}

 localStorageで読み込んだ文字列をJavaScript側からHSP3側の変数に書き込む作業を行っています。 HSP3側の変数の場所を示すためにポインタを利用しています。 stringToUTF8ArrayはHSP3Dish.js内の関数です。 HSP3では次のような実装を行っています。

HSP3

	ptrStrbuf  = varptr(strbuf)
	sizeStrbuf = varsize(strbuf)
	exec_text = "{"
	exec_text+= "const ptr = " + ptrStrbuf + ";"
	exec_text+= "const str = localStorage.getItem('" + keyName + "');"
	exec_text+= "if(!!str){
	exec_text+= "  stringToUTF8Array(str, Module.HEAP8, ptr, " + sizeStrbuf + ");"
	exec_text+= "}"
	exec_text+= "}"
	exec exec_text
	strbuf = unescapeJS(strbuf)

 varptrでポインタの取得、varsizeで文字列変数サイズの取得を行っています。 sizeStrbufには、書き込み先変数(strbuf)のサイズ以下を指定すればいいので、必ずvarsizeが必要というわけではありません。今回は例として毎回サイズを調べるようにしてみました。 最後にJavaScript側から受け取った文字列は、一部がJavaScriptのエスケープ文字になっているのでunescapeJSで元に戻しています。

localStorage:解説 削除

 exec命令で次のようなJavaScriptを実行するだけで削除できます。

JavaScript

	localStorage.removeItem('キー');

localStorage:注意点

 HSP3のスクリプトだけで実装できるので、モジュール化すれば使いやすそう。 ただし理解しないまま使用するとトラブルの原因となってしまいそうです。 例えば以下のようなものが考えられます。

  • 安易に使ってしまい、容量制限に到達してしまう。
  • 複数のアプリを作った際に、気づかないうちにキーが競合してしまう。
  • 同一ドメインを他のひとが作ったアプリと共用している場合、キーの競合や容量制限でトラブルを起こす。

IndexedDB、localStorage

 IndexedDBを使ってみるとファイルへの保存と変わらない感覚で利用できるので、とても使いやすいと感じました。 データの削除さえ出来れば、正式な運用が見えてくるかもですね。 その削除機能もJavaScriptで書いてしまえば、実装できてしまいます。

 localStorageは容量制限と競合のリスクを考えると、ゲームのセーブには使わない方がよさそうです。 ドメイン内全体に関する情報(サイト内のwebアプリの利用実績、アプリの音量など)の保存などに使うとよさそうです。

 HSP3.7β9のHSP3Dishには、初心者向けの誰でも使える保存機能は実装されていません。 JavaScriptを駆使すれば使えるという感じです。 実装をミスると、ユーザーのブラウザにデータを残してしまう可能性があります。 JavaScriptが分からない方は、安易にマネしないようお願いします。

execで出来ること

 localStorageの実装方法を使うと、HSP3とJavaScriptで文字列を受け渡しすることができます。 これはつまりJavaScriptでしか使えない機能もHSP3から間接的に使用できるということです。 様々な情報が取得できる可能性があります。(以下は取得できそうな情報の例)

  • 加速度、重力加速度、回転速度。
  • デバイスの方位、傾き角度。
  • 緯度、経度、高度、移動速度、移動方向。
  • バッテリーの充電状態、残量。

 また、デバイスを振動させたり、音声認識や音声合成も使えるかもしれません。 作る予定はないですが、面白そうです。 HSP3のモジュールで実装できそうなのもいいですね。

関連記事

  1. HSP3Dish.jsでフルスクリーン化(1) はじめに HSP3Dish.jsを始める これからやりたい事の計画 HSP3Dish.jsを使った例...
  2. HSP3Dish.jsでPWA 言い訳から始める PWAとは HSP3Dish.jsでPWA 作成例 PWAに必要なもの HTMLフ...