データを制するものは、デジタルを制すると言われても、ピンと来ない、ユゲタです。
SPAに限りませんが、SSRなどのクライアントに優しいWEBサイト(システム)を作る時に、どうしても、サーバーからのデータロード処理が必要になります。
※SSRは、Reactなどでのサーバーサイドレンダリングする処理です。詳しくは、
https://tech-wiki.online/jp/react-server-side-rendering.htmlをご覧ください。
実は今回は、とあるゲーム開発に必要な処理として、膨大なデータをロードする必要があったので、その時のLoadingって、どうするのが効果的なのかと考えたところ、
色々なライブラリでloaderっていう処理やファイル名を目にすることがあったので、「このデータロードを1つの共通処理にしてしまえばええんや!」と思いつきました。
という事で、今回はこの、自前Loaderシステムを公開してみたいと思います。
ソースコード
import { Ajax } from "./ajax.js"
export class Loader{
datas = []
files = []
maxCount = 0
currentCount = 0
styleTagName = 'games-loader-style'
targetSelector = 'games-loader-progress'
targetSelectorBase = 'games-loader-progress-base'
progressPrefix = '--progress-bar'
options = {
files : [],
durationTime : 500,
progressBarPadding : '15px 5px',
progressHeight: '20px',
progress: function(data , count , req , res){},
finish: function(datas , options){},
}
constructor(options){
if(!options){return}
this.setOptions(options)
if(!options.files
|| typeof options.files !== 'object'
|| !options.files.length){return}
if(!this.setFiles(options)){return}
this.init()
this.setStyle()
this.viewProgress()
this.load()
}
setOptions(options){
for(var i in options){
this.options[i] = options[i]
}
}
/**
* 初期設定
*/
init(){
this.maxCount = this.files.length
}
/**
* ファイル一覧をメモリ保持
*/
setFiles(options){
if(!options.files){
return
}
if(this.setFilesArray(options.files)
|| this.setFilesObject(options.files)){
return true
}
}
/**
* ファイル一覧をメモリ保持 : Array処理
*/
setFilesArray(files){
if(files.constructor !== Array){return}
for(let file of files){
this.files.push({
id : file,
file : file,
})
}
return true
}
/**
* ファイル一覧をメモリ保持 : Object処理
*/
setFilesObject(files){
if(!Object.keys(files).length){return}
for(let key in files){
this.files.push({
id : key,
file : options.files[file],
})
}
return true
}
/**
* ファイル読み込み処理(1ファイルずつ)
*/
load(){
if(!this.files || this.currentCount > this.files.length-1){
return
}
const data = this.files[this.currentCount]
// this.files.shift()
new Ajax({
url: data.file,
method: 'get',
success: (function(data, res, req){
data.size = this.fileSizeFormat(res.length)
this.datas.push({
id : data.id,
file : data.file,
data : res,
size : data.size,
})
// this.files.unshift()
this.setProgress()
// this.progressProc(data , req , res)
setTimeout(this.progressProc.bind(this, data , this.currentCount , req , res) , this.options.durationTime)
if(this.isFinish()){
setTimeout(this.finish.bind(this) , this.options.durationTime +100)
}
else{
this.currentCount++
this.load()
}
}).bind(this, data),
error: (function(err){
}).bind(this),
})
}
/**
* 1ファイル読み込むごとに呼ばれる処理
*/
progressProc(data , count , req , res){
if(this.options.progress){
this.options.progress(data , count+1 , req , res)
}
}
// 全てのファイルを読み込み完了確認
isFinish(){
return !this.files || this.currentCount >= this.files.length-1 ? true : false
}
/**
* 全てのファイルを読み込んだ後の処理
*/
finish(){
this.removeProgress()
if(this.options.finish){
this.options.finish(this.datas , this.options)
}
}
/**
* 全てのファイルを読み込んだ後の処理
*/
viewProgress(){
switch(document.readyState){
case 'complete':
this.progressView();
case 'interactive':
default:
window.addEventListener('load' , this.progressView.bind(this));
break
}
}
removeProgress(){
const baseElm = document.querySelector('.'+ this.targetSelectorBase)
if(!baseElm){return}
baseElm.parentNode.removeChild(baseElm)
}
progressView(){
if(!document.body){return}
const progressElement = document.createElement('div')
progressElement.className = this.targetSelectorBase
document.body.appendChild(progressElement)
const progressString = document.createElement('div')
progressString.className = this.targetSelector +'-text'
progressString.innerHTML = 'Loading...'
progressElement.appendChild(progressString)
const progressBar = document.createElement('div')
progressBar.className = this.targetSelector +'-bar'
progressElement.appendChild(progressBar)
// progressBar.textContent= 'hoge-hoge-hoge'
}
calcProgressRate(){
if(!this.datas
|| !this.datas.length){return 0}
return ~~((this.datas.length / this.maxCount) * 1000) / 10
}
setStyle(){
const head = document.querySelector('head')
if(!head){return}
let styleElm = head.querySelector('style.'+ this.styleTagName)
if(styleElm){return}
styleElm = document.createElement('style')
styleElm.className = this.styleTagName
styleElm.type = 'text/css'
head.appendChild(styleElm)
styleElm.textContent = this.cssText
}
/**
* Progressバーを進める
*/
setProgress(){
const per = this.calcProgressRate()
this.setProgressBarRate(per)
}
getSS(){
const styles = document.styleSheets;
if(styles && styles.length){
for(let style of styles){
if(style.ownerNode.className === this.styleTagName){
return style;
}
}
}
}
getProgressBarRateSs(){
const ss = this.getSS()
if(!ss){return}
for(let i=0; i<ss.cssRules.length; i++){
if(ss.cssRules[i].selectorText === '.'+ this.targetSelectorBase){
return ss.cssRules[i]
}
}
}
setProgressBarRate(per){
const css = this.getProgressBarRateSs()
if(!css){return}
css.style.setProperty(this.progressPrefix, per +'%' ,'')
}
/**
* ファイルサイズを単位をつけて返す
* @param int decimal
*/
fileSizeFormat(bite , decimal){
decimal = (decimal) ? Math.pow(10,decimal) : 10;
var kiro = 1024;
var size = bite;
var unit = "B";
var units = ["B" , "KB" , "MB" , "GB" , "TB"];
for(var i=(units.length-1); i>0; i--){
if(bite / Math.pow(kiro,i) > 1){
size = Math.round(bite / Math.pow(kiro,i) * decimal) / decimal ;
unit = units[i];
break;
}
}
return String(size) +" "+ unit;
}
cssText = ''
+ '.'+ this.targetSelectorBase +' {'
+ ' '+ this.progressPrefix +': 0;'
+ ' position: absolute;'
+ ' top: 0;'
+ ' left: 0;'
+ ' width: 100vw;'
+ ' height: 100vh;'
+ ' display: block;'
+ ' background-color: rgba(0,0,0,0.4);'
+ ' font-size: 10px;'
+ ' margin: 0;'
+ ' padding: 0;'
+ ' border: 0;'
+ '}'
+ '.'+ this.targetSelector +'-text {'
+ ' position: absolute;'
+ ' top: calc(50% - 50px);'
+ ' left: 0;'
+ ' width: 100%;'
+ ' height: auto;'
+ ' display: block;'
+ ' transform: translate(0, -50%);'
+ ' text-align: center;'
+ ' font-weight: bold;'
+ ' font-size: 3rem;'
+ ' color: white;'
+ ' margin: 0;'
+ ' padding: 0;'
+ '}'
+ '.'+ this.targetSelector +'-bar {'
+ ' position: absolute;'
+ ' top: calc(50% + 50px);'
+ ' left: 50%;'
+ ' width: 70%;'
+ ' height: auto;'
+ ' min-height: '+ this.options.progressHeight +';'
+ ' display: block;'
+ ' padding: '+ this.options.progressBarPadding +';'
+ ' margin: 0;'
+ ' transform: translate(-50%, -50%);'
+ ' text-align: center;'
+ ' font-weight: normal;'
+ ' font-size: 1rem;'
+ ' color: white;'
+ ' border: 1px solid white;'
+ '}'
+ '.'+ this.targetSelector +'-bar:before {'
+ ' content: "";'
+ ' position: absolute;'
+ ' width: var('+ this.progressPrefix +');'
+ ' height:100%;'
+ ' top: 0;'
+ ' left: 0;'
+ ' z-index: -1;'
+ ' margin: 0;'
+ ' padding: 0;'
+ ' background-color: rgba(255,255,255,0.5);'
+ ' transition-property: width;'
+ ' transition-duration: '+ this.options.durationTime +'ms;'
+ '}';
}
(注意)このライブラリを実行するには、先日ブログで書いたajaxライブラリを同じ階層に設置してください。(ソースコードは下記URLから取得してください)
今時のxhrでajaxするライブラリを作った話
使い方
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>Loader</title>
<script type="module" src="sample.js"></script>
</head>
<body>
<h1>Loader-Sample</h1>
<div id="loader"></div>
</body>
</html>
sample.js
import { Loader } from "./loader.js"
dataフォルダ内のファイル
const datas = new Loader({
files:[
"data/aaa.txt",
"data/bbb.mp3",
"data/ccc.jpg",
"data/ddd.zip",
'data/eee.m4a',
],
progress : function(data , count , req , res){
console.log(count +":"+ data.file +"("+ data.size +")")
},
finish : function(datas , options){
document.getElementById("loader").innerHTML = "finished: "+ datas.length +" files."
},
})
console.log(datas)
上記では、dataフォルダ内に、次のファイルを置いてある状態です。
aaa.txt
bbb.mp3
ccc.jpg
ddd.zip
eee.m4a
あと、loaderライブラリも同じ階層に設置してある状態ですが、別の場所に設置している場合は、sample.jsの1行目のimportのパスを変更してください。
そして、インターネットブラウザで、index.htmlにアクセスすると、
こんな感じのloading画面が現れて、データの読み込みに合わせてprogressバーが動くようにしています。
そして、デバッグコンソールには、
Javascriptコンソールの表示結果
1:data/aaa.txt(11 B)
2:data/bbb.mp3(1 MB)
3:data/ccc.jpg(854.8 KB)
4:data/ddd.zip(3.2 MB)
5:data/eee.m4a(150.3 KB)
こんな感じで、読み込まれたファイルと、容量を表示しています。
sample.jsで、Loaderインスタンスを実行しているんですが、そのインスタンスの中のdatasというプロパティに、全てのデータがその情報とともに格納されています。
インスタンス作成時にprogressといういfunctionをつけると、1ファイルずつ読み込みをする処理にしてくれて、
finishというfunctionをつけると、全てのファイルを読み込み完了した時の処理として、任意イベントを発火させることができます。
最後に
読み込みファイル数が多い場合や、データ容量が大きい場合に、progressバーがあるだけで、ユーザーの心構えが大きく変わるので、重要なUIであると考えて作ってみました。
まだ、バージョン0.1なので、改善点や、機能追加などを予定していますが、ファイル毎に、読み込みmimeタイプをセットしたり、プログレスバーの精度をファイル容量に合わせた割合にするとか、見た目をカスタマイズできるようにするなど、
さらなる進化をしていく予定です。
もし、このプログラムが気に入ってくれた人がいたら、ご連絡いただければ、サポートさせていただくことも可能ですよ。
0 件のコメント:
コメントを投稿