Skip to content

拡張スクリプトの仕組み ​

第6版作成 最終更新 (日本時間)
確認バージョン1.5.7.01.5.8.1

拡張スクリプトは、プリザンター全体(または条件で絞った範囲)に適用できる JavaScript です。管理画面の「スクリプト」がサイト単位の設定であるのに対し、拡張スクリプトはシステム管理者が設定するグローバルなカスタマイズ手段です。このページでは、どこに置けば動くかに加えて、いつ読み込まれ、HTML のどこに展開され、どんな条件で絞り込まれるかを整理します。

管理画面のスクリプトとの違い ​

拡張スクリプト管理画面のスクリプト
設定方法ファイル配置、または DB の Extensions テーブル管理画面から編集
適用範囲システム全体(フィルタリングで絞り込み可)サイト単位
HTML への挿入形式<script src="...">(外部ファイル)<script>(インライン)
反映タイミングファイル・DB とも再起動またはパラメータリロードが必要保存後すぐ反映

設定方法 ​

ファイルで設定する ​

App_Data/Parameters/ExtendedScripts/ に .js ファイルを置きます。

text
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 を書き換えただけでは反映されないため、再起動またはパラメータリロードが必要です。

json
{
  "SiteIdList": [123, 456],
  "Actions": ["edit", "new"]
}

読み込みの仕組み ​

起動時の初期化(ファイルの場合) ​

アプリ起動時に Implem.DefinitionAccessor.Initializer が App_Data/Parameters/ExtendedScripts/ を走査し、すべての .js ファイルをメモリに読み込んで Parameters.ExtendedScripts リストに保持します。

csharp
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> タグとして出力されます。

csharp
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 の中から、現在のリクエストの条件に一致するものだけを選んで結合します。

csharp
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 属性も付与されます。

csharp
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> の末尾付近のスクリプトの読み込み順は次のとおりです。

html
<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 でページを遷移するプリザンターの通常操作では、拡張スクリプトは再実行されません。最初のページ読み込み時に一度だけ実行されます。

csharp
if (!context.Ajax)
{
    // <script> タグの出力(AJAXの場合はスキップ)
    return hb.Script(...);
}

運用上の注意点 ​

グローバルな名前の衝突 ​

拡張スクリプトは、条件に一致するすべてのページ(ファイルの場合は全ページ)で動きます。グローバルスコープに関数や変数を定義すると、他のスクリプトと衝突するおそれがあります。即時実行関数(IIFE)でスコープを閉じる、const/let を使うなど、スコープを意識して書いてください。

javascript
// 推奨: 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 が変わり、キャッシュが無効になる

拡張スクリプトは管理画面を介さずにシステム全体へ効かせられる反面、「全ページに影響する」リスクもあります。フィルタリングで適用範囲を絞り込み、運用ルールを整えてから使うとトラブルを防ぎやすくなります。

関連ページ ​

変更履歴

第6版記事の確認版を繰り返す表現を整理する
第5版「拡張機能」「画面カスタマイズ集」に対応バージョンを表示
第4版本文から元記事や以前の版への言及を除き、正しい動作だけを書く形に整理
第3版「拡張機能」「API」を 1.5.8.1 のソースで検証して修正
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版拡張 HTML・拡張スタイル・拡張スクリプト・拡張フィールドの仕組みを追加し、拡張 SQL の設定パラメータを拡充