Skip to content

スクリプトとサーバースクリプトで定数を共通化 ​

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

ステータス値やサイト ID などの定数は、拡張サーバースクリプト(Shared: true)に実体を 1 か所だけ置き、BeforeOpeningPage のサーバースクリプトから hidden.Add でクライアントへ配り、拡張スクリプトの読み出しヘルパー経由で参照する構成にすると、本体コードを改変せずに共通化できます。 テーブル管理画面のスクリプトには「他のスクリプトを読み込む」仕組みがありませんが、拡張機能側には共有と取り込みの仕組みが用意されています。

対象バージョン

バージョン 1.5.7.1 を対象にしています。

何を共通化するか ​

テーブルごとのスクリプトやサーバースクリプトには、次のような同じ値が何度も登場しがちです。

  • ステータスの値(900 = 完了、200 = 進行中)
  • 分類項目に入れるコード("A001" = 承認済み)
  • 連携先のサイト ID
  • 外部システムのエンドポイントやタイムアウト値

テーブルが増えるとこれらのコピーも増え、値を変えるときに全テーブルのスクリプトを探し回ることになり、直し漏れが発生します。

全体像 ​

サーバサイドとクライアントサイドはスクリプトエンジンが別なので、クライアントへは Hidden 要素に載せて配ります。

図を読み込み中…

経路使う機能届く先
共有サーバースクリプト拡張サーバースクリプトの Sharedすべてのテーブルのサーバースクリプト
インクルードサーバースクリプト本文の //Include:明示的に取り込んだサーバースクリプトのみ
クライアントへの配布hidden.Add と拡張スクリプトのヘルパー拡張スクリプト・テーブルのスクリプト(ブラウザ側)

サーバサイド: 拡張サーバースクリプトを共有スクリプトにする ​

拡張サーバースクリプトは App_Data/Parameters/ExtendedServerScripts/ に、定義ファイル(.json)と本体(.json.js)の 2 ファイルで配置します。

json
{
    "Name": "Constants",
    "Description": "全テーブル共通の定数定義",
    "SiteIdList": null,
    "Controllers": null,
    "Actions": null,
    "Shared": true,
    "Functionalize": false,
    "TryCatch": false
}
js
const APP_CONST = Object.freeze({
    Status: Object.freeze({
        Draft: 100,
        InProgress: 200,
        Approved: 300,
        Closed: 900
    }),
    Approval: Object.freeze({
        Approved: 'A001',
        Rejected: 'A002'
    }),
    SiteId: Object.freeze({
        Customers: 12345,
        Orders: 12346
    }),
    Api: Object.freeze({
        TimeoutMs: 10000
    })
});

// 定数と一緒に共通関数を置いても構いません
function isClosed(status) {
    return status === APP_CONST.Status.Closed;
}

これだけで、すべてのテーブルのサーバースクリプトから APP_CONST を参照できます。

js
if (model.Status === APP_CONST.Status.Closed && !context.HasPrivilege) {
    context.Error('完了済みのレコードは更新できません');
}

context.Error はプロパティではなく、メッセージを引数に取るメソッドです(ServerScriptModelContext.cs)。代入の形(context.Error = '...')ではエラーを設定できないため、メソッドとして呼び出します。

参照できる理由 ​

サーバースクリプトの実行時、共有スクリプトの本文は実行対象のスクリプトより前に連結され、1 回の Execute に渡されます。

csharp
engine.Execute(
    code: new[]
    {
        (sharedScripts ?? []).Select(script =>
            script.Body).Join("\n"),
        scripts.Select(script =>
            ProcessedBody(
                ss: ss,
                script: script)).Join("\n")
    }
    .Where(o => !o.IsNullOrEmpty())
    .Join("\n"),
    debug: debug);

ServerScriptUtilities.cs#L1165-L1177

同じスクリプトエンジンの同じスコープで評価されるため、共有スクリプトのトップレベルで宣言した const や関数がそのまま見えます。

共有スクリプトとそれ以外の振り分けは次の部分です。

csharp
var scripts = allScripts
    .Where(where)
    .Where(script => script.Shared != true) // 共有スクリプト以外でフィルタ
    .ToArray();
if (!scripts.Any())
{
    return null;
}
var sharedScripts = allScripts
    .Where(script => script.Shared == true)
    .ToArray();

ServerScriptUtilities.cs#L1261-L1271

拡張サーバースクリプトは、テーブル管理画面で設定したサーバースクリプトと同じリストにマージされてから、この振り分けを通ります(SiteSettings.cs#L6176-L6204)。

共有スクリプトの注意点 ​

#注意点対策
1そのタイミングで動くサーバースクリプトが 1 本もないと、共有スクリプトも評価されない共有スクリプトには副作用のある処理を書かず、定数の宣言にとどめる
2Functionalize と TryCatch は共有スクリプトには効かないFunctionalize は false にする。実行時に失敗しうる処理は書かない
3同じ名前を const で二重宣言すると SyntaxError になり、そのタイミングのサーバースクリプトがすべて動かなくなる定数用の名前空間を 1 つ(例: APP_CONST)に絞り、テーブル側では宣言しない
4SpecifyByName を true にすると読み込まれないSpecifyByName は true にしない(SiteIdList や Controllers による絞り込みは使える)
5ファイル配置の共有スクリプトはバックグラウンドサーバースクリプトに届かないテーブル管理画面側で「バックグラウンド」と「共有」を有効にしたスクリプトを用意する

1. 共有スクリプトだけでは実行されない ​

上のコードの if (!scripts.Any()) で早期リターンしているため、そのタイミングで動く(共有でない)サーバースクリプトが 1 本もないと、共有スクリプトも評価されません。定数を置いておく用途では問題になりませんが、共有スクリプトに副作用のある処理を書いても走らないことがあります。

2. Functionalize と TryCatch は共有スクリプトには効かない ​

ProcessedBody は実行対象のスクリプトにだけ適用されます。

csharp
private static string ProcessedBody(SiteSettings ss, ServerScript script)
{
    var body = script.Body;
    if (script.Functionalize == true)
    {
        body = $"(()=>{{\n{script.Body}\n}})();";
    }

ServerScriptUtilities.cs#L1196-L1213

Functionalize は本文を即時関数で包む処理なので、適用されると定数がスコープの外から見えなくなります。共有スクリプトでは Functionalize を false にしてください。また TryCatch も効かないため、共有スクリプトの例外は捕捉されません。宣言だけを置き、実行時に失敗しうる処理は書かないのが安全です。

3. 名前の二重宣言でスクリプト全体が落ちる ​

共有スクリプトと対象スクリプトは 1 回の Execute にまとめられるため、同じ名前を const で二重宣言すると SyntaxError になり、そのタイミングのサーバースクリプトがすべて動かなくなります。

4. SpecifyByName は true にしない ​

拡張機能の絞り込み条件は次の共通処理で評価されます。

csharp
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 => MeetConditions(o.ColumnList, columnName))
    .Where(o => !o.Disabled)
    .Cast<T>();

ExtensionUtilities.cs#L216-L230

サーバースクリプトの読み込みでは name に null が渡るため、SpecifyByName を true にすると読み込まれません。一方、SiteIdList や Controllers は使えるので、「特定のテーブルだけ別の定数を配る」といった絞り込みは可能です。

5. バックグラウンドサーバースクリプトには届かない ​

バックグラウンドサーバースクリプトは別の経路で実行され、共有スクリプトもテナントに登録されたスクリプトの中から選ばれます。

csharp
var scripts = new List<ServerScript>();
scripts.AddRange(inScripts
    .Scripts
    .Where(s => s.Disabled != true && s.Background == true && s.Shared == true));

BackgroundServerScriptJob.cs#L45-L48

拡張サーバースクリプトには Background の項目自体がないため、ファイルで配置した共有スクリプトはバックグラウンド実行時には読み込まれません。バックグラウンドで定数を使いたい場合は、テーブル管理画面のサーバースクリプトで「バックグラウンド」と「共有」を有効にしたスクリプトを用意します。

WARNING

バックグラウンド側の共有スクリプトは ProcessedBody を通る経路に入ります。Functionalize を有効にすると定数が見えなくなるので、こちらでも false にしてください。

//Include: で明示的に取り込む ​

全スクリプトに暗黙で配られるのを避けたい場合や、依存関係をスクリプト側に明記したい場合は //Include: を使います。サーバースクリプト本文の行頭に //Include: 名前 と書くと、その名前のサーバースクリプトの本文が展開されます。

js
//Include: Constants

if (model.ClassA === APP_CONST.Approval.Approved) {
    model.Status = APP_CONST.Status.Approved;
}

展開処理は次のとおりです。

csharp
foreach (var line in body.Split('\n'))
{
    if (line.StartsWith("//Include:"))
    {
        var name = line.Substring(line.IndexOf(":") + 1).Trim();
        var includeBody = serverScripts
            .Where(o => o.Name == name)
            .Select(o => o.Body)
            .Join("\n");

SiteSettings.cs#L6235-L6275

  • 取り込み元の Name が必須です。拡張サーバースクリプトの Name は JSON の "Name" から取るので、必ず設定します
  • //Include: は行頭から書きます。インデントすると単なるコメント扱いになります
  • 名前が一致するスクリプトが複数あると、すべてが連結されて展開されます
  • 再帰展開の深さは Script.json の ServerScriptIncludeDepthLimit(既定値 10)で制限されています

共有とインクルードの使い分け ​

観点Shared//Include:
参照側の記述不要各スクリプトに 1 行必要
依存関係の見え方スクリプトを見ても分からない本文に明示される
適用範囲対象条件に合うすべてのテーブル書いたスクリプトのみ
名前の衝突全スクリプトに影響取り込んだスクリプトのみに影響

全社共通の定数は Shared、特定業務でだけ使う定数セットは //Include: と分けると扱いやすくなります。

クライアントへ配る: hidden.Add ​

クライアント用に定数を書き写すと二重管理に戻ってしまうため、サーバから丸ごと配り、クライアントは読むだけにします。

hidden はサーバースクリプトに公開されているホストオブジェクトで、キーと値を追加すると input type="hidden" として HTML に出力されます。

csharp
public string Get(string key = null)
{
    return data[key];
}

public void Add(string key = null, object value = null)
{
    data.Add(key, value.ToString());
}

ServerScriptModelHidden.cs#L18-L26

追加した値は HiddenServerScript で、キーをそのまま id にした Hidden 要素として出力されます。

csharp
private static HtmlBuilder HiddenServerScript(
    this HtmlBuilder hb,
    Context context,
    SiteSettings ss,
    ServerScriptModelRow serverScriptModelRow)
{
    serverScriptModelRow?.Hidden?.ForEach(hidden => hb
        .Hidden(controlId: hidden.Key, value: hidden.Value));

HtmlTemplates.cs#L779-L793

配布用の拡張サーバースクリプトを 1 本用意し、共有スクリプトで定義した APP_CONST を JSON 化して流し込みます。

json
{
    "Name": "PublishConstants",
    "Description": "共通定数をクライアントへ配布",
    "BeforeOpeningPage": true,
    "Shared": false,
    "Functionalize": true,
    "TryCatch": true
}
js
hidden.Add('AppConst', JSON.stringify(APP_CONST));

これで全テーブルの画面に id="AppConst" の Hidden 要素が出力されます。定数を増やしても、書き換えるのは共有スクリプトの APP_CONST だけです。

hidden.Add の注意点 ​

  • 実装は Dictionary.Add なので、同じキーを 2 回追加すると例外になります。配布用スクリプトは 1 本に絞り、TryCatch: true を付けておくと安全です
  • value.ToString() を呼ぶため、null を渡すと例外になります
  • hidden.Get は存在しないキーで例外になります。キーの有無を確認する用途には向きません
  • キーはそのまま要素の id になります。プリザンター標準の Hidden 要素(SiteId、Columns など)と衝突しない名前を付けてください(標準の Hidden 要素はスクリプトで使えるシステム変数を参照)
  • 値は必ず JSON.stringify した文字列で渡します。サーバースクリプトのオブジェクトをそのまま渡すと、サーバ側でのシリアライズ結果が意図した形になりません

秘密情報は配らない

Hidden 要素は誰でも読めます。クライアントで使わない定数(API キーや接続文字列など)は配布しないでください。

クライアントサイド: 拡張スクリプトに読み出しヘルパーを置く ​

Hidden 要素を読む処理をテーブルごとのスクリプトに書くと、JSON.parse と存在チェックがあちこちに散らばります。拡張スクリプトに読み出しヘルパーを 1 本用意し、テーブルのスクリプトからはヘルパー経由で参照します。

js
(function () {
    var HIDDEN_ID = '#AppConst';
    var cache;

    function load() {
        if (cache !== undefined) {
            return cache;
        }
        var raw = $(HIDDEN_ID).val();
        if (!raw) {
            // 拡張スクリプトの絞り込みなどで配布されていない場合
            cache = Object.freeze({});
            return cache;
        }
        try {
            cache = Object.freeze(JSON.parse(raw));
        } catch (e) {
            console.error('appConst: JSONの解析に失敗しました', e);
            cache = Object.freeze({});
        }
        return cache;
    }

    // 'Status.Closed' のようなドット区切りのパスで取り出す
    function get(path, fallback) {
        var value = String(path || '')
            .split('.')
            .reduce(function (obj, key) {
                return (obj === null || obj === undefined) ? undefined : obj[key];
            }, load());
        return value === undefined ? fallback : value;
    }

    window.appConst = {
        get: get,
        all: load,
        reload: function () {
            cache = undefined;
            return load();
        }
    };
})();

テーブルのスクリプトからは次のように使います。

js
$p.events.on_editor_load = function () {
    if ($p.getControl('Status').val() === String(appConst.get('Status.Closed'))) {
        $('#Results_Body').prop('disabled', true);
    }
};

第 2 引数に既定値を渡せるので、定数が配られていない画面でも動作を止めずに済みます。

js
var timeout = appConst.get('Api.TimeoutMs', 10000);
var siteId = appConst.get('SiteId.Customers');
if (siteId === undefined) { return; }

項目の値は文字列で返ってくるため、数値の定数と比較するときは String() で揃えるか Number() で寄せるかを決めておきます。

ヘルパーにしておくと、次の利点があります。

  • JSON.parse と存在チェック、パース失敗時のフォールバックが 1 か所に閉じ込められる
  • キーをタイプミスしても undefined が返るだけで、ReferenceError にならない
  • 読み出しが遅延評価なので、拡張スクリプトの評価タイミングに依存しない
  • 配布経路を Hidden 要素から別の手段に変えても、appConst.get の呼び出し側は書き換えずに済む

INFO

Object.freeze は浅い凍結です。入れ子のオブジェクトまで凍らせたい場合は、再帰的に Object.freeze を適用するヘルパーを追加してください。

拡張スクリプトに置く理由(読み込み順) ​

拡張スクリプトは App_Data/Parameters/ExtendedScripts/ に .js ファイルを置くだけで読み込まれ、ファイル名の昇順で評価されます。

csharp
var files = new DirectoryInfo(path)
    .GetFiles("*.js")
    .OrderBy(file => file.Name);

Initializer.cs#L727-L729

ヘルパーのファイル名を 00_Constants.js にしておけば、他の拡張スクリプトより先に評価されるので、他の拡張スクリプトからも appConst.get を使えます。読み込まれた拡張スクリプトは 1 本の JavaScript に連結され、resources/scripts として配信されます。

さらに HTML への出力順では、拡張スクリプトの script タグはテーブル管理画面のスクリプトより前に置かれています。

csharp
.Script(src: Responses.Locations.Get(
    context: context,
    parts: $"resources/scripts?v={extendedScripts.Sha512Cng()}"
        + $"&site-id={context.SiteId}"
        + $"&id={context.Id}"
        + $"&controller={context.Controller}"
        + $"&action={context.Action}"),
    nonce: context.Nonce,
    _using: !extendedScripts.IsNullOrEmpty())
.Script(
    script: ss.GetScriptBody(
        context: context,
        peredicate: o =>
            o.All == true
            && o.Disabled != true),

HtmlScripts.cs#L120-L138

async も defer も付いていない同期スクリプトなので、テーブルのスクリプトが動く時点で window.appConst は必ず定義済みです。URL の v= は連結後のスクリプトのハッシュなので、内容を変えればキャッシュも自動で切り替わります。

Hidden 要素はスクリプトより前に出力される ​

ヘルパーが Hidden 要素を読めるのは、ページのテンプレートで Hidden 要素の出力(HiddenData)が script タグの出力(Scripts)より前にあるためです。

csharp
.HiddenData(
    context: context,
    ss: ss,
    serverScriptModelRow: serverScriptModelRow)
.LoaderContainer(
    context: context,
    ss: ss)
.VideoDialog(
    context: context,
    ss: ss)
.Styles(
    context: context,
    ss: ss,
    userStyle: userStyle)
.Htmls(
    context: context,
    ss: ss,
    positionType: Settings.Html.PositionTypes.BodyScriptTop,
    methodType: methodType)
.Scripts(
    context: context,
    ss: ss,
    script: script,
    userScript: userScript)

HtmlTemplates.cs#L71-L94

serverScriptModelRow は ss.GetServerScriptModelRow で作られ、ここで BeforeOpeningPage のサーバースクリプトが実行されます。一覧・エディタのどちらもこの経路を通るため、両方の画面で Hidden 要素が出力されます。DOMContentLoaded を待つ必要がないので、ヘルパーをトップレベルで呼んでも値を取得できます。

補足: SetMemory で渡す方法 ​

サーバースクリプトのレスポンスには SetMemory というメソッドもあり、$p のプロパティに値を書き込めます。

js
case 'SetMemory':
    $p[target] = value;
    break;

_dispatch.js#L76-L78

js
context.AddResponse('SetMemory', 'appConstJson', JSON.stringify(APP_CONST));

SetMemory は Ajax のレスポンスにも乗るため、更新後に値を差し替えたい場合に向いています。ただし全画面描画時は次のスクリプトで適用されるため、参照できるのは DOMContentLoaded の後です。

csharp
.Hidden(
    controlId: "ServerScriptResponseCollection",
    value: context.ResponseCollection.ToJson())
.Script(
    script: OnDomReadyScript("$p.setByJson(undefined, undefined, $p.getData($('#MainForm')), $('#MainForm'), undefined, JSON.parse($('#ServerScriptResponseCollection').val()));"),
    nonce: context.Nonce);

HtmlScripts.cs#L246-L253

方法受け取り方参照できるタイミングAjax 後の更新
hidden.Add$('#キー名').val()スクリプト評価時からされない
SetMemory$p.キー名DOMContentLoaded 後される

値が変わらない定数の配布には hidden.Add、リクエストごとに変わる値を渡したいときは SetMemory と使い分けます。ヘルパーの load を差し替えれば両対応にもできます。

まとめ ​

  • 定数の実体は拡張サーバースクリプトに Shared: true を設定して置く。トップレベルで宣言した const がすべてのテーブルのサーバースクリプトから参照できる
  • 明示的に取り込みたい場合は //Include: 名前 を行頭に書く
  • クライアントへは BeforeOpeningPage のサーバースクリプトから hidden.Add で JSON をまとめて配る
  • クライアント側は拡張スクリプトに 00_Constants.js として読み出しヘルパーを置き、appConst.get('Status.Closed') の形で参照する
  • Ajax 後も値を差し替えたい場合は SetMemory を使う(参照できるのは DOMContentLoaded 後)

関連ページ ​

変更履歴

第5版本文から元記事や以前の版への言及を除き、正しい動作だけを書く形に整理
第4版「スクリプト」を 1.5.8.1 のソースで検証して修正
第3版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第2版スクリプト関数($p.get 系・$p.set 系)の解説を追加し、定数共通化と jQuery 4 移行の図を Mermaid に変更
第1版「スクリプト」セクションの記事を追加