【JavaScript】 複数のモジュールを動的に順次読み込みしてみる
更新日:2020/03/19
最近僕はJavaScriptのモジュール化について勉強しました。
その時に書いた記事がこちら。
参考:【JavaScript】 export/importでモジュール化とファイル分割する方法
モジュール化したソースはimportで読み込むのですが、普通にインポートすると正常に読み込めたのか判断できません。
そこで動的にインポートして、モジュールが読み込めないときにエラーチェックができるようにしてみます。
importメソッドを使用する
importメソッド(Import Calls)は、ECMAScript2020で策定された機能です。
策定以前からブラウザへの適用が進められていたので、全てではありませんが今現在のブラウザでは使用可能です。
対応状況:https://caniuse.com/#feat=es6-module-dynamic-import
とはいえ数年前のブラウザは確実に動作しませんので、不特定のユーザー向けに使用するのはやめておいたほうがよさそうです。
importメソッドは、モジュールを動的にインポートできます。
構文:
import( url ).then( 成功コールバック , 失敗コールバック );
import()はモジュールへのパスを引数で受け取り、Promiseオブジェクトを返します。
そのため結果をPromiseオブジェクトのthen()で受け取ることができます。
Promiseオブジェクトについては、次の記事で紹介しているので参考にしてください。
参考:【JavaScript】 非同期はPromise?解説が難しいので自分で理解してみた
インポートが成功すると、インポートしたモジュールを含むモジュールオブジェクトを引数として、コールバック関数が実行されます。
a.js
export function a(){ };
インポート先
import( "https://xxxx/a.js" ).then(
module => { // インポート成功
module.a();
},
error => { // インポート失敗
console.log( error.message);
}
);
上の例では、コールバックが終了すると変数moduleにアクセスできなくなります。
引き続き使用するなら、コールバック内で他の変数などに代入する必要があります。
読み込み失敗か成功かだけを知りたいとき
細かい経過はどうでもいいのでインポートが成功か失敗したかだけ知りたいとき、Promise.all()を使用すると簡単に全てのインポートを処理できます。
Promise.allは、次のように使用します。
Promise.all( [import()の配列] ).then(
module => { // 全てインポート成功
// module:import()で取得したモジュールの配列
},
error => { // インポート失敗
}
);
インポートが全て成功すると、Promise.all().then()で指定した最初のコールバックがよばれます。
コールバックの引数は、[import()の配列]の順番で格納された、モジュールの配列です。
一つでも失敗すると、失敗した時点で2番目のコールバックがよばれます。
引数は失敗した理由です。
説明よりも、例を見てもらった方が速そうです。
(手抜き)
LoadModule:Promise.all版
function LoadModule2( r , callBack ){
const promiseArray = r.map( e => import(e.url) ); // import()の配列を作成
const module = {};
Promise.all( promiseArray ).then(
e => { // 全て成功
for( let i = 0; i < r.length ; i ++){
Object.defineProperty( module ,r[i].name,{
value:e[i],
enumerable:true,
});
}
callBack("ok",module);
},
e => callBack("error",e.message) // 失敗あり
);
}
引数の一番目は、次のようなオブジェクトの配列を想定しています。
const r = [
{url:"url",name:"name"},
{url:"url",name:"name"},
{url:"url",name:"name"}
];
引数のニ番目は、コールバック関数です。
説明が面倒なので、使用例を載せます。
(手抜き)
LoadModule:Promise.all版使用例
const r = [
{url:"https://xxxx/test1.js",name:"test1"},
{url:"https://xxxx/test2.js",name:"test2"},
{url:"https://xxxx/test3.js",name:"test3"},
];
let myModule;
LoadModule2( r , ( stat , value ) => {
switch( stat ){
case "ok":
myModule = value;
startfunc();
break;
case "error":
console.log( value );
}
});
function startfunc() {
myModule.test2.func1();
}
問題は、エラー時にどのモジュールで読み込み処理が失敗したかわからない点です。
コールバック関数でエラー内容を受け取ることができます。
ですがメッセージはあまりあてになりません。
今回二つのブラウザで実行したところ、次のようなエラーを受け取りました。
Google Chrome 80.0.3987.132
Failed to fetch dynamically imported module: https://xxxxx/test2.js
モジュール名が入ってる!
と思ったら、
Firefox 74.0
error loading dynamically imported module
Firefoxのエラーには入っていませんでした。
そのため、エラーからモジュール名を判断できません。
ということで、少し改良してみます。
複数のモジュールを並列風読み込みする
Promise.all()は個々の結果を受け取ることができません。
しかしimport()に対してthen()メソッドを適用すると、個々の結果に対してアクションをおこなうことができます。
const promiseArray = r.map( e => import(e.url) );
↓ ↓ ↓
const promiseArray = r.map( e => import(e.url).then( …) );
そのことを踏まえて、コードを作成してみます。
LoadModule:並列読み込み版
function LoadModule( moduleDef ,callBack ) {
this.module = {};
this.moduleDef = moduleDef ;
this.count = -1;
this.callBack = callBack;
}
LoadModule.prototype={
run:function () {
// 2回目以降の呼び出しは無効
return ( this.count !== -1 ) ? false : this._allimport();
},
_allimport:function () {
this.count ++;
const callBack = this._callBack.bind(this);
// importの配列を作成
const promiseArray = this.moduleDef.map(
e=> this._import( e , callBack)
);
// 全てのimportの結果を受け取る
Promise.all( promiseArray ).then(
e => { // 全て成功
for( let i = 0; i < this.moduleDef.length ; i ++){
Object.defineProperty( this.module ,this.moduleDef[i].name,{
value:e[i],
enumerable:true,
});
}
this._callBack(4 , this.module );
},
e => this._callBack(3 ) // 失敗あり
);
},
_import:( mD , callBack ) => {
callBack(0 , mD);
return import(mD.url).then(
module => { callBack(1 , mD);return module},
error => {callBack(2 , mD , error.message);
return Promise.reject(error);}
);
},
_stat:["start","fileok","filefaild","faildEnd","end","error"],
_callBack:function (stat,mD=null,message=null) {
this.callBack( { stat : this._stat[stat] , data : mD , message : message } );
}
};
前項と比べて、コールバックへのメッセージを増やしています。
callBackの引数:
{ stat :ステータス ,
data : 実行中のmoduleDef または読み込んだモジュール(stat="end"時) ,
message : エラーメッセージ }
ステータス:
"start" モジュールの読み込み開始
"fileok" モジュールの読み込み成功
"filefaild" モジュールの読み込み失敗
"faildEnd" エラーで終了した
"end" 全てのモジュールを正常に読み込んだ
使用例
const r = [
{url:"https://xxx/test1.js",name:"test1"},
{url:"https://xxx/test2.js",name:"test2"},
{url:"https://xxx/test3.js",name:"test3"},
];
let myModule; // モジュールを格納する変数
new LoadModule( r, (e)=>{
switch(e.stat){
case "end":
myModule = e.data; // 読み込んだモジュールをセット
startfunc(); // 読み込み後に関数を実行
break;
case "filefaild":
console.log( e.data.url + ":" + e.message);
break;
default:
console.log( e.stat + ":" + ((e.data !== null) ? e.data.url : "---"));
}
}).run();
function startfunc() {
myModule.test2.func1();
}
import()を実行した時点で、モジュールの読み込みが始まっています。
(実際に読み込んでいるかどうかは、ブラウザやサーバーの環境によります)
そのため一つのimport()が失敗しても、他のインポート処理は止まりません。
複数のモジュールを順次読み込みする
前項は、並列風に読み込んでいます。
今回は、複数のモジュールを順番に読み込んでみます。
LoadModule:順次読み込み版
function LoadModule( moduleDef ,callBack ) {
this.module = {}; // 結果用オブジェクト
this.moduleDef = moduleDef;
this.count = -1;
this.callBack = callBack;
}
LoadModule.prototype={
run:function () {
// 2回目以降の呼び出しは無効
return ( this.count !== -1 ) ? false : this._nextModule();
},
_nextModule:function () {
this.count ++;
if( this.count >= this.LoadData.length ) { // 終了チェック
this._callBack(4 , this.module );
return false;
}
const mD = this.moduleDef[this.count];
this._callBack(0 , mD);
import( mD.url ).then( // import開始
Module => { // インポート成功
// 読み込んだモジュールを結果用オブジェクトに追加
Object.defineProperty( this.module , mD.name , {
value:Module,
enumerable:true,
});
this._callBack(1 , mD);
this._nextModule(); // 再帰呼び出し
},
error => { // 読み込み失敗
this._callBack(2 , mD , error.message);
this._callBack(3 );
}
);
},
_stat:["start","fileok","filefaild","faildEnd","end","error"],
_callBack:function (stat,mD=null,message=null) {
this.callBack( { stat : this._stat[stat] , data : mD , message : message } );
}
};
再帰呼び出ししているだけで、それほど難しいことをしていませんね。
ただnameの重複チェックなどをしていないので、必要ならチェックを追加してください。
完成したLoadModule()は、次のように使用します。
使用例
const r = [
{url:"https://xxx/test1.js",name:"test1"},
{url:"https://xxx/test2.js",name:"test2"},
{url:"https://xxx/test3.js",name:"test3"},
];
let myModule; // モジュールを格納する変数
new LoadModule( r, (e)=>{
switch(e.stat){
case "end":
myModule = e.data; // 読み込んだモジュールをセット
startfunc(); // 読み込み後に関数を実行
break;
case "filefaild":
console.log( e.data.url + ":" + e.message);
break;
default:
console.log( e.stat + ":" + ((e.data !== null) ? e.data.url : "---"));
}
}).run();
function startfunc() {
myModule.test2.func1();
}
test3.jsが無い状態で実行すると、
e.stat="filefaild"
e.message = "Failed to fetch dynamically imported module: https://xxx/test3.js"
でコールバックが呼び出され、モジュールの読み込みが失敗したときに何らかの処理ができるようになりました。
※エラー内容(e.message)はブラウザによって変わります。urlが通知されるとは限りません。
まとめ
import()を使用すると動的にモジュールをインポートでるだけでなく、正常にインポートできたかどうかチェックできます。
ユーザーから
なんかよくわからないけれど、動かないんですが...
という問い合わせが減りますね。
更新日:2020/03/19
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。