拡張スクリプトの仕組み
拡張スクリプトは、プリザンター全体(または条件で絞った範囲)に適用できる JavaScript です。管理画面の「スクリプト」がサイト単位の設定であるのに対し、拡張スクリプトはシステム管理者が設定するグローバルなカスタマイズ手段です。このページでは、どこに置けば動くかに加えて、いつ読み込まれ、HTML のどこに展開され、どんな条件で絞り込まれるかを整理します。
管理画面のスクリプトとの違い
| 拡張スクリプト | 管理画面のスクリプト | |
|---|---|---|
| 設定方法 | ファイル配置、または DB の Extensions テーブル | 管理画面から編集 |
| 適用範囲 | システム全体(フィルタリングで絞り込み可) | サイト単位 |
| HTML への挿入形式 | <script src="...">(外部ファイル) | <script>(インライン) |
| 反映タイミング | ファイル・DB とも再起動またはパラメータリロードが必要 | 保存後すぐ反映 |
設定方法
ファイルで設定する
App_Data/Parameters/ExtendedScripts/ に .js ファイルを置きます。
App_Data/
└── Parameters/
└── ExtendedScripts/
├── 01_common.js ← ファイル名順で読み込まれる
├── 02_custom.js
└── feature-a/
└── feature-a.js ← サブディレクトリも再帰的に読み込まれるファイルはファイル名のアルファベット順に処理されます。サブディレクトリも再帰的に走査されるので、機能ごとにディレクトリを分けて管理できます。依存関係がある場合は、01_base.js・02_feature.js のように数字の接頭辞で順序を明示すると分かりやすくなります。
WARNING
ファイルはアプリ起動時(初期化処理)に読み込まれ、メモリに保持されます。ファイルを追加・変更・削除したら、プリザンターを再起動するか、特権ユーザでパラメータリロード(/admins/reloadparameters)を実行するまで反映されません。 確認したソースでは、パラメータリロードでも ExtendedScripts を読み直します(Initializer.cs)。本番環境で変更するときは、サービスへの影響を考えてタイミングを決めてください。
Extensions テーブルで設定する 1.4.7.0 以降
1.4.7.0 以降は、DB の Extensions テーブルで管理することもできます。ExtensionType に "Script"、Body にスクリプト本文、ExtensionSettings に絞り込み条件を JSON で書きます。
確認したソースでは、Extensions テーブルの内容も起動時とパラメータリロードのときにだけ読み込まれ、メモリ上の Parameters.ExtendedScripts に追加されます(ExtensionInitializer.cs、ParametersInitializer.cs)。DB を書き換えただけでは反映されないため、再起動またはパラメータリロードが必要です。
{
"SiteIdList": [123, 456],
"Actions": ["edit", "new"]
}読み込みの仕組み
起動時の初期化(ファイルの場合)
アプリ起動時に Implem.DefinitionAccessor.Initializer が App_Data/Parameters/ExtendedScripts/ を走査し、すべての .js ファイルをメモリに読み込んで Parameters.ExtendedScripts リストに保持します。
private static List<ExtendedScript> ExtendedScripts(
string path = null, List<ExtendedScript> list = null)
{
list = list ?? new List<ExtendedScript>();
path = path ?? Path.Combine(ParametersPath, "ExtendedScripts");
var files = new DirectoryInfo(path)
.GetFiles("*.js")
.OrderBy(file => file.Name); // ファイル名順
foreach (var file in files)
{
var script = Files.Read(file.FullName);
if (script != null)
{
list.Add(new ExtendedScript()
{
Name = file.Name,
Path = file.FullName,
Script = script
});
}
}
// サブディレクトリも再帰処理
foreach (var dir in new DirectoryInfo(path).GetDirectories()
.OrderBy(dir => dir.Name))
{
list = ExtendedScripts(dir.FullName, list);
}
return list;
}/resources/scripts エンドポイント
拡張スクリプトは HTML にインラインで埋め込まれるのではなく、/resources/scripts エンドポイントを参照する <script src> タグとして出力されます。
public static ContentResultInheritance Get(Context context)
{
var siteId = context.QueryStrings.Long("site-id");
var id = context.QueryStrings.Long("id");
var controller = context.QueryStrings.Data("controller");
var action = context.QueryStrings.Data("action");
// ...
return new ContentResultInheritance
{
ContentType = "text/javascript",
Content = HtmlScripts.ExtendedScripts(
context: context,
deptId: context.DeptId,
groups: context.Groups,
userId: context.UserId,
siteTop: siteTop,
siteId: siteId,
id: id,
controller: controller,
action: action)
};
}このエンドポイントには [ResponseCache(Duration = int.MaxValue)] が付いており、ブラウザによる長期キャッシュが有効です。URL の v パラメータ(後述)が変わったときだけ再取得されます。
図を読み込み中…
フィルタリングの仕組み
Parameters.ExtendedScripts の中から、現在のリクエストの条件に一致するものだけを選んで結合します。
public static IEnumerable<T> ExtensionWhere<T>(
IEnumerable<ExtendedBase> extensions,
string name,
int deptId, List<int> groups, int userId,
long siteId, long id,
string controller, string action,
string columnName = null)
{
return extensions
?.Where(o => !o.SpecifyByName || o.Name == name)
.Where(o => MeetConditions(o.DeptIdList, deptId))
.Where(o => o.GroupIdList?.Any() != true
|| groups?.Any(groupId => MeetConditions(o.GroupIdList, groupId)) == true)
.Where(o => MeetConditions(o.UserIdList, userId))
.Where(o => MeetConditions(o.SiteIdList, siteId))
.Where(o => MeetConditions(o.IdList, id))
.Where(o => MeetConditions(o.Controllers, controller))
.Where(o => MeetConditions(o.Actions, action))
.Where(o => !o.Disabled)
.Cast<T>();
}各条件の評価ルールは次のとおりです。
| 条件リストの状態 | 評価結果 |
|---|---|
| リストが空(null を含む) | 常に一致(条件なし扱い) |
| 値が指定されている | 一致するものだけ適用(肯定指定) |
- を先頭に付けた値 | 一致するもの以外に適用(否定指定) |
たとえば SiteIdList に [123] と指定すればサイト ID 123 のページだけで適用され、["-123"] と指定すればサイト ID 123 以外のすべてのページで適用されます。
ファイルの拡張スクリプトは絞り込めない
ファイルから読み込んだ ExtendedScript は Name・Path・Script しか設定されておらず、絞り込み用のプロパティ(DeptIdList など)はすべて空です。そのため条件なし扱いとなり、システム全体に適用されます。絞り込みたい場合は Extensions テーブルを使ってください。
HTML への挿入位置
拡張スクリプトは <body> 内に <script src="/resources/scripts?..."> として挿入されます。nonce 属性も付与されます。
var extendedScripts = ExtendedScripts(context: context);
return hb
// ... jQuery, jQuery-UI, その他プラグイン類 ...
.Script(src: $"resources/scripts?v={extendedScripts.Sha512Cng()}"
+ $"&site-id={context.SiteId}"
+ $"&id={context.Id}"
+ $"&controller={context.Controller}"
+ $"&action={context.Action}",
nonce: context.Nonce,
_using: !extendedScripts.IsNullOrEmpty())
// 管理画面のスクリプト(All指定のもの)
.Script(script: ss.GetScriptBody(context, o => o.All == true), ...)
// ページ固有の管理画面スクリプト
.Script(script: userScript, ...);<body> の末尾付近のスクリプトの読み込み順は次のとおりです。
<body>
...コンテンツ...
<script src="assets/plugins/jquery-3.6.0.min.js">
<script src="assets/plugins/jquery-ui.min.js">
<!-- その他プラグイン(datetimepicker, multiselect, validate, d3, etc.)-->
<!-- app.manifest.json / manifest.json から生成されるアセット -->
<script src="/resources/scripts?v=..."> ← 拡張スクリプト(ここ)
<script>/* 管理画面のスクリプト(All) */</script>
<script>/* ページ固有の管理画面スクリプト */</script>
<script>(function() { var run = () => { $p.execEvents('on_editor_load',''); }; if (document.readyState === 'complete') { run(); } else { window.addEventListener('load', run); } })();</script>
</body>拡張スクリプトは jQuery などのプラグインが読み込まれた後、管理画面のスクリプトより前に入ります。そのため、$ を使った jQuery の操作や $p オブジェクトへのアクセスができます。
1.5.6.0 での変更
1.5.6.0 から、末尾に埋め込まれるインラインスクリプトが OnLoadScript()/OnDomReadyScript() でラップされるようになりました。すでに load/DOMContentLoaded が発火済みならその場で実行し、まだならイベントを待ちます。1.5.5.0 以前は単純な window.addEventListener('load', ...) でした。
v パラメータとキャッシュ
/resources/scripts?v=... の v には、すべての拡張スクリプトを結合した文字列の SHA512 ハッシュが入ります。スクリプトの内容が変わるとハッシュも変わり、ブラウザのキャッシュが無効になって新しいスクリプトが取得されます。
正確には、v のハッシュは「全拡張スクリプト」ではなく、そのページを表示したユーザ・サイト・アクションの条件で絞り込んだ後の結合結果から計算されます(HtmlScripts.cs、L171-L182)。そのため、フィルタリング条件によってユーザごとに違うスクリプトになる場合は v も変わり、別の URL として取得されます。内容が同じなら同じ URL になり、2 回目以降はブラウザのキャッシュがそのまま使われます。
サイトのトップ(一覧の最上位)では、テナントの管理画面で設定したトップのスクリプト(TopScript)も先頭に結合されます(L215-L217)。
Ajax による画面遷移では再実行されない
<script> タグが出力されるのは、Ajax ではないリクエストのときだけです。Ajax でページを遷移するプリザンターの通常操作では、拡張スクリプトは再実行されません。最初のページ読み込み時に一度だけ実行されます。
if (!context.Ajax)
{
// <script> タグの出力(AJAXの場合はスキップ)
return hb.Script(...);
}運用上の注意点
グローバルな名前の衝突
拡張スクリプトは、条件に一致するすべてのページ(ファイルの場合は全ページ)で動きます。グローバルスコープに関数や変数を定義すると、他のスクリプトと衝突するおそれがあります。即時実行関数(IIFE)でスコープを閉じる、const/let を使うなど、スコープを意識して書いてください。
// 推奨: IIFEでスコープを閉じる
(function () {
const myFeature = { ... };
// ...
})();CSP(コンテンツセキュリティポリシー)
プリザンターは CSP の nonce 属性に対応しており、拡張スクリプトの <script> タグにも nonce が付与されます。CSP ヘッダーで nonce ベースの制御をしている環境でも意図どおりに動きます。
ただし、拡張スクリプトの中で eval() や <script> タグの動的生成を行う場合は、CSP の unsafe-eval・unsafe-inline が必要になることがあります。CSP の設定と拡張スクリプトの内容を合わせて確認してください。
まとめ
| 項目 | 内容 |
|---|---|
| ファイルの配置場所 | App_Data/Parameters/ExtendedScripts/*.js(サブディレクトリも再帰、ファイル名順) |
| 読み込みタイミング | ファイル・DB ともアプリ起動時とパラメータリロード時 |
| HTML への挿入形式 | <body> の末尾近くに <script src="/resources/scripts?v=..."> |
| 挿入位置 | jQuery などのプラグインの後、管理画面のスクリプトの前 |
| フィルタリング | 組織・グループ・ユーザ・サイト ID・レコード ID・コントローラ・アクション(DB で設定した場合のみ) |
| キャッシュ | 内容の SHA512 ハッシュで URL が変わり、キャッシュが無効になる |
拡張スクリプトは管理画面を介さずにシステム全体へ効かせられる反面、「全ページに影響する」リスクもあります。フィルタリングで適用範囲を絞り込み、運用ルールを整えてから使うとトラブルを防ぎやすくなります。