はじめに
業務システムあるあるの機能がそろった
ログイン→一覧→登録、編集機能のWEBテンプレートを作りました。
ログイン画面は非常によくあるIDとパスワードによる認証画面です。
デザインはSemantic-UIを使用しているためとてもスタイリッシュに仕上がっています。
フォームのバリデーションとそのメッセージ表示機能を備えています。
トップ画面では機能の紹介を載せていますが、ここはプロジェクトによってダッシュボードに変わったりするかと思います。
全画面共通のヘッダからサイドメニュー、ログイン情報の閲覧ができます。
全画面共通のヘッダ等は当然ソースが共通化されているのでそれらをincludeするだけで使用することができます。
一覧画面はag-gridを使用しているので行選択やフィルタ機能等をそのまま使用することができます。
一覧の削除、編集、新規登録、excel出力の機能がありますが、個々の画面での記述量は極力少なくなるようにしています!
登録画面も記述量はかなり少なくなっています。
Semantic-UIのモーダルがやはりスタイリッシュ。
余談ですがSemantic-UIは個人的にお気に入りなCSSフレームワークです。
諸事情で開発が止まっているみたいですが、コミュニティメンバによりFomantic-UIという名前で
分岐して開発されているようです。いずれは本家にマージするみたいですね。
https://fomantic-ui.com/
開発の経緯
業務システムではマスタメンテナンス画面などをよく作りますよね?
一覧→登録の流れが50画面とかあったりします。
その時のあるあるなのが
- 人によって実装の仕方が違う
- 同じ実装をそれぞれで書いている
ですね。
これは工数的にも無駄ですしメンテナンス性も最悪です。
このWEBテンプレートはそれらの解消を目的としてとにかく実装の共通化、
個々の画面の実装のみになるような作りにしました。
諸事情によりgithub等でソースの公開ができないので
設計の参考、自分の設計思想の共有程度にご覧頂ければと思います。
技術要素
◆フロントエンド
- pug
- TypeScript
- Semantic-UI
- ag-grid
◆バックエンド
- go beego
機能一覧
◆基本機能
- ログイン、ログアウト(有効/無効切り替えあり)
- ajaxでの送受信(ag-grid含む)
- バリデーションチェック
◆フロントエンドの機能
- csv&excel出力(sheetJS)
- ファイルアップロード(バックエンド未実装)
◆バックエンドの機能
- DB接続
- DB種類の切替
- ログ出力
- パンくずリスト(状態維持)
◆セキュリティ機能
- CSRF対策(有効/無効切り替えあり)
- ロールによる画面アクセスコントロール(有効/無効切り替えあり)
◆対応端末
- スマホ、タブレット
- PC(chrome、firefox、IE11)
※いろいろ機能がありますが今回はフロントエンドのみ解説しています!
構成
├── conf
├── app.conf //バックエンドの設定
├── config.json //バックエンド、フロンドエンド共通の設定
├── messages.json //バックエンド、フロンドエンド共通のメッセージの設定
├── src //コンパイル前ソース
├── css
├── common.scss
├── login.scss
├── templateList.scss
├── temolateRegist.scss
├── top.scss
├── js
├── common.ts
├── login.ts
├── templateList.ts
├── temolateRegist.ts
├── top.ts
├── ...//その他 共通ファイル省略
├── pug
├── common
├── layout.pug
├── mixin.pug
├── ...//その他 共通ファイル省略
├── login.pug
├── templateList.pug
├── temolateRegist.pug
├── top.pug
├── static //コンパイル後ソース
├── views //コンパイル後ソース(pug)
見てのとおり1画面につきscss,ts,pugが1ファイルずつ存在していて
それらはそれぞれのcommon.〇〇を継承する作りになっています。
共通部分はcommonがよしなにしているので、個々の画面で記述する実装は非常に少ないです。
設定ファイルはconfにまとめられていてフロントエンド、バックエンド共通で使えるようになっています!
ありがちなフロントエンド、バックエンドそれぞれで設定ファイルを持つなんてことはありません。
{
"baseDir": "src",
"distDir": "static",
"jsDir": "js",
"cssDir": "css",
"agGrid_contains": "を含む",
"agGrid_endsWith": "で終わる",
"agGrid_equals": "と等しい",
"agGrid_filterOoo": "フィルタ...",
"agGrid_greaterThan": "より大きい",
"agGrid_greaterThanOrEqual": "以上",
"agGrid_inRange": "範囲",
"agGrid_lessThan": "より少ない",
"agGrid_lessThanOrEqual": "以下",
"agGrid_noRowsToShow": " ",
"agGrid_notContains": "を含まない",
"agGrid_notEqual": "と等しくない",
"agGrid_startsWith": "で始まる",
"requestParameter": {
"isValid": "isValid",
"data": "data",
"validationSummary": "validationSummary"
},
"routingURL": {
"login": "/Login",
"loginDoLogout": "/Login/DoLogout",
"loginDoLogin": "/Login/DoLogin",
"top": "/Top",
"templateList": "/TemplateList",
"templateListGetRoleMaster": "/TemplateList/GetRoleMaster",
"templateListDoDelete": "/TemplateList/DoDelete",
"templateRegist": "/TemplateRegist",
"templateRegistDoRegist": "/TemplateRegist/DoRegist"
},
"imageFolder": "/static/img/",
"validationMode": {
"doLogin": "DoLogin",
"doLogout": "DoLogout",
"doDelete": "DoDelete",
"doRegist": "DoRegist",
"doSearch": "DoSearch"
}
}
それでは一覧画面を例に見ていきましょう。
scss
.ag-grid {
height: 350px;
}
/*スマホ用の記述*/
@media screen and (max-width: 480px) {
.ag-grid {
height: 360px;
}
}
画面のスタイルはcommon.scssに書かれているので、個別の画面定義にはag-gridのスタイルのみを記述します。
TypeScript
import { AjaxOptions } from './ajax-options';
import { AgGrid } from './ag-grid';
import { Common, CommonViewModel } from './common';
import { ButtonRenderer } from './components';
const config = require('/conf/config.json');
import "../css/common.scss";
import "../css/templateList.scss";
/**
* 個別ビュークラス
*
*/
class TemplateListViewModel extends CommonViewModel {
RoleMasterGrid: string = "RoleMasterGrid";
}
/**
* 個別クラス
*
*/
class TemplateList extends Common {
readonly model: TemplateListViewModel;
readonly gridId: string;
/**
* コンストラクタ
*
*/
constructor() {
super();
this.model = new TemplateListViewModel();
this.gridId = this.model.RoleMasterGrid;
}
/**
* バインドを実行する
*
* @returns 実行結果
*/
async bind(): Promise<any> {
await super.bind();
//検索ボタン
$(this.model.SearchBtn.id()).on("click", (event) => {
//スクロール位置をリセットする
this.grid[this.gridId].resetScrollTop();
this.GetRoleMaster();
return false;
});
//新規登録ボタン
$(this.model.NewRegistBtn.id()).on("click", (event) => {
this.startLoading();
//現在のスクロール位置をセットする
this.grid[this.gridId].setScrollTop();
//登録画面へ遷移
location.href = config.routingURL.templateRegist;
return false;
});
//削除ボタン
$(this.model.DeleteBtn.id()).on("click", (event) => {
let modalOptions = this.getUsualRegistModalOptions(
this.messageFormat("C_000")
, config.validationMode.doDelete
, this.messageFormat("I_000")
, returnData => config.routingURL.templateList
, config.routingURL.templateListDoDelete
);
this.startConfirmModal(modalOptions);
return false;
});
//excel出力ボタン
$(this.model.EXCELOutputBtn.id()).on("click", (event) => {
this.grid[this.gridId].exportDataAsExcel();
return false;
});
return true;
}
/**
* 初期化を実行する
*
* @returns 実行結果
*/
async init(): Promise<any> {
await super.init();
this.grid[this.gridId] = new AgGrid(this.gridId);
var grid = this.grid[this.gridId];
//列定義を設定する
var columnDefs = [
grid.getCheckColumnDef(),
{
headerName: "編集"
, field: "Edit"
, width: 85
, pinned: "left"
//ボタンコンポーネントを設定する
, cellRenderer: ButtonRenderer
, cellRendererParams: { buttonValue: "編集" }
, onButtonClicked: (params) => {
this.startLoading();
//現在のスクロール位置をセットする
grid.setScrollTop();
location.href = config.routingURL.templateRegist + "?id=" + params.data.RoleId;
}
},
{ headerName: "ロールID", field: "RoleId", width: 100 },
{ headerName: "ロール名", field: "RoleName" },
{ headerName: "ロール説明", field: "RoleDescription", width: 250 }
];
grid.setColumnDefs(columnDefs);
grid.init();
await this.GetRoleMaster();
return true;
}
/**
* ロールマスタを取得する
*
* @returns プロミス
*/
async GetRoleMaster(): Promise<any> {
return this.submitSetGrid(config.validationMode.doSearch, this.gridId, config.routingURL.templateListGetRoleMaster);
}
}
$(function () {
let templateList = new TemplateList();
});
個々の画面のTSはCommonを継承した画面のクラス(個別クラス)を持ちます。
個別クラスには画面の項目を定義したクラス(個別ビュークラス)を持っています。
個別ビュークラスはCommonViewModelを継承していて共通の画面の項目はそちらに定義されています。
ビュークラスがあることによって、よくある項目名の記述ミスを防ぐことができます。
(項目名の最初が大文字だったとかで書き間違えて動かないとかあるあるを防ぎます)
個別クラスをインスタンス化するとCommonのコンストラクタでbindとinitが実行されて共通の処理が実行されます。
個々の画面の初期処理はbindとinitに書き込むだけになっています。
面倒なDBから一覧取得→gridに反映の部分も共通メソッドを呼ぶだけなので
人によって違う実装がされることもあり得ません。
return this.submitSetGrid(
config.validationMode.doSearch, //バリデーションチェックの指定
this.gridId, //反映するgridのID
config.routingURL.templateListGetRoleMaster //一覧取得先のURL
);
削除ボタン押下時には
確認メッセージ表示
→削除処理
→処理結果メッセージ表示
→画面再読み込み
上記のように意外とイベントが多いのですが、共通メソッドを呼ぶだけで完結するように処理をまとめました。
let modalOptions = this.getUsualRegistModalOptions(
this.messageFormat("C_000") //確認メッセージ
, config.validationMode.doDelete //バリデーションチェックの指定
, this.messageFormat("I_000") //処理結果メッセージ
, returnData => config.routingURL.templateList //処理結果メッセージ表示後の移動先
, config.routingURL.templateListDoDelete //削除処理のURL
);
this.startConfirmModal(modalOptions);
このgetUsualRegistModalOptionsには定型のボタン処理が定義されていて
可変部分の確認メッセージ等を引数として渡すだけで定型のボタン処理が完成します。
定型のボタン処理以外にしたい場合はstartConfirmModalに渡す変数を独自に定義すればOKです。
参考までに関連するCommon部分を抜粋するとこのような感じになってます。
export class Common {
async submitSetGrid(validationMode: string, gridId: string, url: string, extraDone: Function = () => { }): Promise<any> {
let options = this.getUsualSearchOptions(validationMode, gridId, extraDone);
return this.submit(url, options);
}
getUsualSearchOptions(validationMode: string, gridId: string, extraDone: Function = () => { }): AjaxOptions {
let options = new AjaxOptions();
options.validationMode = validationMode;
options.done = (returnData) => {
this.grid[gridId].setRowData(returnData[config.requestParameter.data]);
extraDone(returnData);
};
return options;
}
}
pug
extends ./common/layout.pug
block vars
- var Title = "一覧系テンプレート"
- var jsSrc = "templateList.js"
block main-contents
+form("")
div(class="ui two column stackable grid search")
div(class="six wide column field")
div(class="ui labeled input")
label(class="ui label" for="RoleName") ロール名
+inputText("RoleName", "{{.viewmodel.RoleName}}")
+button("SearchBtn", "teal")
+icon("search")
| 検索
div(class="ui horizontal divider")
+icon("search-hide-show angle up")
h4(class="ui dividing header") ロールマスタ
div(id="RoleMasterGrid" class="ag-theme-balham ag-grid")
block footer-contents
div(class="ui item right")
+iconButton("EXCELOutputBtn", "primary", "file", "excel出力")
+iconButton("NewRegistBtn", "positive", "edit", "新規登録")
+iconButton("DeleteBtn", "negative", "trash", "削除")
pugのincludeとmixinを活用して記述量を極力少なくしました。
大半のコードはlayout.pugに書かれています。
layout.pugでさらにいろんなpugファイルをincludeしており別のレイアウトにしたいときも再利用しやすくなっています。
mixinはpugの独自の機能でhtmlを吐き出す関数のようなものです。
繰り返し出てくるボタン等をmixinにすることで記述量を少なくし記述ミスも減らすことができます。
mixin iconButton(_id,_addClass,_iconAddClass,_text)
+button(`${_id}`, `${_addClass}`)
+icon(`${_iconAddClass}`) !{_text}
if block
block
まとめ
今回はフロントエンドのみを解説しました。
とにかくCommonに処理を集めて個々の記述を減らす設計にした結果こうなりました。
今までの経験として個々の記述で同じことを何回も書かされてきたので
それをなんとかしたいという思いから作ってみましたが、実際に案件で使っているわけではありません。
次の改善案としてはVue.js化をしてみたいのですがなかなか手が出せず。
次回はバックエンド編を解説します。