サブパッケージの非同期読み込み
微信小程序はバージョン2.11.2以降、サブパッケージの非同期読み込みをサポートしています。この機能により、「跨サブパッケージカスタムコンポーネント参照」と「跨サブパッケージJSコード参照」が可能になり、メインパッケージのサイズを最適化できます。
1. カスタムコンポーネントの使用制限
カスタムコンポーネントの使用は、その所在するパッケージに制限されます。
- コンポーネントがメインパッケージにある場合、メインパッケージとすべてのサブパッケージのページから参照できます。
- コンポーネントが特定のサブパッケージにある場合、そのサブパッケージ内のページからのみ参照でき、他のサブパッケージやメインパッケージのページから直接参照することはできません。ただし、「跨サブパッケージカスタムコンポーネント参照」を使用すれば可能です。
- サブパッケージ非同期の仕組みにより、メインパッケージはサブパッケージのコンポーネントを非同期で読み込み、使用できます。読み込みが完了するまで、プレースホルダーコンポーネントを表示し、完了後に実際のコンポーネントに置き換えます。
- サブパッケージは、両方が読み込まれた場合にのみ、他のサブパッケージのリソースに直接アクセスできます。この問題は「サブパッケージ非同期化」で解決できます。
2. 跨サブパッケージカスタムコンポーネント参照
跨サブパッケージカスタムコンポーネント参照は、プレースホルダーコンポーネントの設定と組み合わせる必要があります。公式ドキュメントによると、以下のように設定します。
設定後、対応するメインパッケージのページで直接コンポーネントを使用できます。しかし、いくつかの問題が存在します。
- (1) サブパッケージが読み込み完了する前に、コンポーネントはプレースホルダー状態です。もしviewやtextのような基本コンポーネントの場合、スロットの内容が直接表示され、ユーザー体験に影響を与える可能性があります。
- (2) この方法は、ページ内で直接サブパッケージのコンポーネントを使用する場合にのみサポートされており、ページ内の子コンポーネントからの参照は無効です。
- (3) サブパッケージが大きい場合、長い読み込み時間が発生しやすくなります。
これらの問題があるものの、一部のビジネスコンポーネントをサブパッケージに分離し、跨パッケージコンポーネントを使用することで、メインパッケージのサイズを削減できます。例えば、このプロジェクトでは、腾讯云IMをこの形式で統合し、メッセージリストをtabbarに組み込みました。そうしなければ、この部分がメインパッケージの多くのスペースを占有していました。
3. サブパッケージの設定例
subPackagesはサブパッケージページを格納するためのものです。各サブパッケージが一つの項目であり、rootは現在のサブパッケージのルートパスです。
{
"pages": ["pages/home/home"], // メインパッケージのページ
"subpackages": [
{
"root": "pages-feature", // サブパッケージA
"pages": [
{
"path": "feature-render/feature-render",
"style": { "navigationBarTitleText": "サブパッケージのテストページ" }
}
]
},
{ "root": "package-utils", // サブパッケージB
"pages": ["pages/utils/utils"] }
]
}
メインパッケージがサブパッケージAのfeature-renderページのコンポーネントをロードするには、page.jsonを以下のように変更します。
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "ホーム",
"usingComponents": { "feature-render": "/pages-feature/feature/feature" }, // コンポーネントのパス
"componentPlaceholder": { "feature-render": "view" } // 読み込み完了前のプレースホルダー
}
}
]
}
重要な注意点
重要:サブパッケージのコンポーネントは、必ずサブパッケージ内のいずれかのページで使用する必要があります。そうしないと、ビルド時にファイルが見つからず、コンポーネントが含まれません。サブパッケージのページでコンポーネントをインポートしてレンダリングするだけで、ビルド時にfeatureがファイルに含まれます。
4. メインパッケージのページがサブパッケージのコンポーネントを参照する際のビルドサイズ問題の解決
uni-appで開発した小程序では、node_modules内のパッケージは、メインパッケージで直接使用していない場合でも、常にvendor.jsにバンドルされます。
解決策
解決策は簡単です。まず、node_modules内のnpmパッケージをサブパッケージのディレクトリに移動します。
次に、npmのインポートパスを変更するか、設定ファイルでエイリアスを一括設定することで、より便利になります。
// vite.config.js
import path from "path";
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
export default defineConfig({
plugins: [uni()],
resolve: {
alias: {
"large-library": path.resolve(__dirname, "./pages-feature/npm/large-library"),
},
},
});
こうすることで、large-libraryはサブパッケージにバンドルされます。
サードパーティフレームワークでの非同期化
サードパーティフレームワーク(例:Vue, React)は、この非同期化機能を直接使用できません。しかし、サブパッケージプラグイン非同期化を使用することで、これを達成できます。
1. requirePluginの使用
微信公式が提供するrequirePlugin APIを利用します。WebpackはこのAPIをコンパイルしないため、小程序のホスト環境のrequirePlugin APIに正常にアクセスでき、非同期読み込みを実現できます。
// コールバック関数スタイルの呼び出し
requirePlugin('chat-plugin', (chatPlugin) => {
console.log(chatPlugin.getPluginVersion());
}, ({ mod, errMsg }) => {
console.error(`path: ${mod}, ${errMsg}`);
});
// またはPromiseスタイルの呼び出し
requirePlugin.async('chat-plugin')
.then((chatPlugin) => {
console.log(chatPlugin.getPluginVersion());
})
.catch(({ mod, errMsg }) => {
console.error(`path: ${mod}, ${errMsg}`);
});
2. プラグインの登録
まず、微信公式にプラグインを登録する必要があります。プラグインのコードはSDKをロードし、エクスポートします。
// chat-plugin/index.js
import TIM from 'tim-wx-sdk';
module.exports = {
TIM,
};
3. サブパッケージでのプラグインの参照
サブパッケージのapp.jsonでプラグインを参照します。
{
"plugins": {
"chat-plugin": {
"version": "dev-01055b63731de071ffb850464bd5c7b1",
"provider": "chat-plugin appid"
}
}
}
4. ユーティリティの封装
プラグインの読み込みをユーティリティ関数で封装します。
// utils/im.js
let TIM = null;
requirePlugin.async('chat-plugin').then(({ TIM: modTIM }) => {
TIM = modTIM;
}).catch(({ mod, errMsg }) => {
console.error(`chat-plugin path: ${mod}, ${errMsg}`);
});
// 導出
export { TIM };
より良い実装方法:
// utils/im.js
let TIM = {};
import { fetchPluginAsync } from '@/utils/async-loader';
const TIMSdk = fetchPluginAsync('chat-plugin');
TIMSdk.then((mod) => { TIM = mod.TIM; });
// 導出
export { TIM, TIMSdk };
5. プラグイン読み込みの封装
// @/utils/async-loader.ts
/**
* @param pluginName プラグイン名
* @returns Promise<any> プラグインのモジュール
*/
export async function fetchPluginAsync(pluginName: string): Promise<any> {
try {
// @ts-ignore
const mod = await requirePlugin.async(pluginName);
return mod;
} catch ({ mod, errMsg }) {
console.error(`fetchPluginAsync '${pluginName}' error path: ${mod}, ${errMsg}`);
return {};
}
}
6. プラグインの使用
// app.js
(async function init() {
const { TIM } = await fetchPluginAsync('chat-plugin');
TIM.login({ userID: 'user123', userSig: 'sig123' });
})();
この方法でも問題があります。複数の場所でこのJSを使用する場合、それぞれでプラグインの読み込みメソッドを記述する必要があります。小程序の起動時に一度だけ読み込み、後で使用するすべての場所で同じPromiseを共有するようにすれば、効率化できます。
// utils/im.js
let TIM = {};
import { fetchPluginAsync } from '@/utils/async-loader';
const TIMSdk = fetchPluginAsync('chat-plugin');
TIMSdk.then((mod) => { TIM = mod.TIM; });
// 導出
export { TIM, TIMSdk };
TIMSdkは一度だけ実行されるPromiseです。一度読み込まれた後は、後続の呼び出しで同じ結果が返されます。
7. 使用方法
// app.js
import { TIM, TIMSdk } from '@/utils/im';
/** IMの初期化 */
const init = async () => {
// 使用する前にawaitする
await TIMSdk;
const tim = TIM.create({ SDKAppID: config.SDKAppID });
};
init();
上記の記述は少し冗長なので、最適な方法はトップレベルで直接awaitすることです。
// app.js
const { TIM } = await fetchPluginAsync('chat-plugin');
注意点として、この方案を使用するには、最低基础库バージョンを2.11.2に設定する必要があります。
小程序コードパッケージサイズの管理戦略
- 静的リソースは可能な限りCDN経由で配信する。
- サブパッケージに配置可能なページやコンポーネントはすべてサブパッケージに配置し、メインパッケージには分割できないもののみ残す。サブパッケージの読み込み速度を向上させるには、サブパッケージのプリロード(
preloadRule)を有効にする。 - リソースがメインパッケージで参照されなければならず、サイズが制御できない場合は、「サブパッケージ非同期化」または「サブパッケージプラグイン非同期化」を使用する。「サブパッケージ非同期化」と「サブパッケージプラグイン非同期化」の選択については、状況に応じて判断する。