[Javascript] SPA開発に便利なLoaderシステムを作った話

2022年4月8日

Javascript テクノロジー

eyecatch データを制するものは、デジタルを制すると言われても、ピンと来ない、ユゲタです。 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タイプをセットしたり、プログレスバーの精度をファイル容量に合わせた割合にするとか、見た目をカスタマイズできるようにするなど、 さらなる進化をしていく予定です。 もし、このプログラムが気に入ってくれた人がいたら、ご連絡いただければ、サポートさせていただくことも可能ですよ。

人気の投稿

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。

ブログ アーカイブ