はじめに
込み入ったタイトルとなっていますが
タイトルの通り、自分が管理しているテナントにユーザ皆さんが使える共通機能を追加する時に苦戦した内容の備忘録になります。
フロントエンド領域自体経験が浅い身ではあるのですが、ローコードで既にあるものに対し後乗せで機能を付加する、というのはゼロから要件に沿った機能を作るというのとは別の躓き・クセがありますよね。
「JavaScript 〇〇する機能」だけでは綺麗な答えが出てこない。
いつか誰かの「その手があったか!」になれたら幸いです。
検証バージョン:1.4.12.0 1.4.18.1
対象UI:第二世代(ceruleanなど)
ブラウザ:Chrome Edge ※バージョンは執筆日時からふんわり察してください
プリザンターのローコード機能「スクリプト」の軽い紹介
まずは今回使う機能の概要から。
プリザンターではサイト(テナント内のユーザが作れるアプリ)毎に設定出来る機能「スクリプト」と、テナント(プリザンターという1システム全体)に適用できる「拡張スクリプト」という機能があります。※どちらもクライアントサイドのJavaScriptです
「スクリプト」は適用範囲が狭く、サイトの利用者(=作った業務アプリなどを使うユーザ)が見れない画面に処理を載せることは出来ないのですが、「拡張スクリプト」はその縛りがありません。
実装方法によっては全ユーザーに影響を及ぼす機能が追加できます。
プリザンターというシステムに対して、本体のコード改修をせずとも「アドオン」的な変更が加えられるものとイメージしてもらえればよいと思います。
今回のプログラムの要件と制約
追加機能の全容は長くなるので省略しますが、今回の条件は下記の〇点。
- 画面の種類を判定し、メインコマンド欄に画面毎に別々のボタンを表示する
- 画面の単位は「テーブルの管理」から「一覧画面」、「編集画面」までさまざま
- ユーザ側のスクリプトに影響を与えないよう、$p.eventシリーズは使用不可
3.の制約については下記参照
https://pleasanter.org/ja/manual/faq-multiple-on-editor-load
表示位置と3の制約に苦しめられることになります。
完成イメージ 白いボタンが画面IDに沿った名称に変わります。
課題:Ajax通信による画面変更時に表示が安定しない
処理軽減の一つとして、プリザンターでは一部の画面更新をAjax通信で行っており、都度必要なDOM要素だけ再構成するようになっています。
Ajax通信、ポストバックの関連機能
サイト作成する際に、負荷軽減のために下記のような設定ができたりします。
テーブルの管理:エディタ:レコードの遷移にAjaxを使用
https://pleasanter.org/ja/manual/table-management-ajax-transferちょっと細かいですがこんな設定も。
テーブルの管理:エディタ:自動ポストバック時にコマンドボタンを切り替える
https://pleasanter.org/ja/manual/table-management-change-commandbuttonざっくり、画面IDで処理が固定される作りではないと念頭に置いていただければ。
今回ボタン表示をすると決めたメインコマンド欄も大いに再構成がかかる場所であり、更新がかかったタイミングでボタンの描画処理もその度にかけなおす必要があります。
まずはスタート地点。
画面読み込み時にボタン描画処理を走らせるところから始まります。
※以後、本記事の主題の理解に影響のない関数の詳細はコメントに機能を記載して省略していますのでご了承ください。※
// 画面ID判定&ボタン描画処理
handleScenarioButton();
async function handleScenarioButton() {
// 画面ID取得
// #Controller、#TableName、#Actionの値と
// [data-action="SetSiteSettings"]の有無を組み合わせて自動生成します。
const baseScreenId = await getScreenId();
// 判定IDをキーに対象ボタン作成
// 識別のために、この処理で作成するボタンには同じクラスが付与されます。
createButton(baseScreenId);
}
この状態では、ポストバック発生時にボタンが消失します。
以下、試行錯誤の中散っていく失敗策をご覧ください。
まずはポストバック発生時を掴んで再描画させればいいんだ!で以下の実装。
// handleScenarioButton()の中身はfarstStep.jsと同じです
// 画面ID判定&ボタン描画処理
handleScenarioButton();
// ポストバック成功時
$(document).ajaxSuccess(function () {
// 再描画
handleScenarioButton();
});
結果
メインコマンドを再構築しないポストバック時に増える。
※スクロールする度どんどん増殖します
しかもポストバックではIDのチェックに利用している画面要素(#Action)の値が変わらないことが判明。
「表示」タブ系は画面IDの取得すら失敗してしまいました……。
handleScenarioButton()のポストバック対応詳細
async function handleScenarioButton() {
// 画面ID取得(ポストバック対応版)
const baseScreenId = await setupId();
// 判定IDをキーに対象ボタン作成
createButton(baseScreenId);
}
// 画面ID取得(ポストバック対応版)
async function setupId() {
// BSIDの要素取得
// 初版の getScreenId()を呼び出す
const bsidParts = await getScreenId();
/* 改善による追加処理がここから */
//
let execScreenId; // 正しい画面ID
const checkword = ['index', 'calendar', 'crosstab', 'gantt', 'burndown', 'timeseries', 'analy', 'kamban', 'imagelib',];
const currentUrl = window.location.pathname;
const rawSegment = currentUrl.split("/").filter(Boolean).pop() || "";
const pathSegments = sanitizePathSegment(rawSegment);
const isMatch = checkword.some(word => pathSegments.includes(word));
// 「表示」タブのAjax遷移により、
// 画面読み込み時のActionとURL末尾のリテラルが異なるとき
if (bsidParts.actionValue != pathSegments && isMatch) {
// URL末尾リテラルをAction要素としてID生成
execScreenId = generateButtonKey(bsidParts.controller, bsidParts.tableName, bsidParts.siteViewMode, pathSegments);
} else {
// 要素名でそのままベースIDを生成する
execScreenId = generateButtonKey(bsidParts.controller, bsidParts.tableName, bsidParts.siteViewMode, bsidParts.action);
}
return execScreenId;
}
敗因はチェックの網を広げすぎて不必要な処理をはしらせたこと。
であれば必要最小限、対象要素を監視だ!
// 監視対象:ボタン描画する場所
const initialNode = document.getElementById('MainCommands');
let tgMutationObserver = null;
let isObserving = false;
let lastObservedNode = null;
// 初期表示
tgHandleScenarioButton();
// 監視開始
startObserving(initialNode);
function startObserving(targetNode) {
if (!targetNode) {
return;
}
if (tgMutationObserver) {
tgMutationObserver.disconnect();
}
tgMutationObserver = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
handleScenarioButton();
break;
}
}
});
tgMutationObserver.observe(targetNode, {
childList: true,
subtree: false
});
isObserving = true;
lastObservedNode = targetNode;
}
結果
親要素ごと再生成に巻き込まれるので監視の再設置が不足し2回目で再描画処理が消える。
その後、安定した要素に貼ろうとすると監視範囲を広げすぎて絞り込み条件が複雑化。
負荷を気にしつつ定点監視などを試すも、大掛かりに仕掛けすぎていてよい実装とはとても思えず。
最終結果
色々苦心した結果、Ajax通信成功時に状態をチェックして、ボタンが消えていたら再描画を走らせる。
考え方としては試行錯誤時の合わせ技のような形で安定した表示がされるようになりました。
// 画面ID判定&ボタン描画処理:ポストバック対応版
// 初期表示
handleScenarioButton();
// ポストバック時、状態チェックしシナリオボタン再生成
$(document).ajaxSuccess(function () {
waitForMainCommandsAndRenderButton();
});
// ボタンの状態チェック付きの再描画処理
function waitForMainCommandsAndRenderButton(maxWait = 5000) {
const start = Date.now();
function check() {
const main = document.getElementById('MainCommands');
// 追加ボタンに共通して付与しているクラスを指定し、有り無しを確認させる
const alreadyRendered = document.querySelector('.addedBotton');
if (main && !alreadyRendered) {
// 表示が必要なので再描画処理
handleScenarioButton();
} else if (Date.now() - start < maxWait) {
// 最低5秒は変更を監視
requestAnimationFrame(check);
} else {
// MainCommandsの再生成無しorタイムアウト
}
}
requestAnimationFrame(check);
}
最後に
「プリザンター」、「ローコード」の組み合わさった課題かなと取り上げてみました。
無残な失敗策達も、要件次第でベストアンサー足りえたかもしれないと供養を兼ねて掲載。
システム利用者の中でエンジニアが多い場合は、汎用関数を拡張スクリプトで組み込んでより簡単にローコード開発が出来る自社特化プリザンターを構築するのも良さそうですね。
ユーザさんがテナント向け機能を作ることはそうそう無いかもしれませんが、システム全体にまとめてカスタムも出来るんだ!と知っていただければ幸いです。