blog

フロントエンド・データ辞書統合管理ソリューション - Vue.js実装に基づく

数年後、多肉植物でいっぱいのワークステーションで、タオは私がこのソリューションを彼に紹介したときの、素っ気ないが威圧的ではない口調を必ず覚えているでしょう。ビジネス開発をしている人なら誰でも知っている...

Sep 5, 2020 · 11 min. read
シェア

背景

数年後、多肉植物でいっぱいのワークステーションで、タオはきっと、私がこのソリューションを紹介するときに使った、平坦だけど威圧的ではない口調を思い出すことでしょう。ビジネス開発をしている人なら誰でも、データディクショナリ管理がどんな規模のフロントエンド・プロジェクトでも非常に重要であることを知っていますが、それをうまく行う方法についての首尾一貫した考え方はありません。新卒のフロントエンドプログラマーがこのようなコードを書くことはまずないでしょう:

<select>
 <option value="1"> </option>
 <option value="2"> </option>
 <option value="3"> </option>
</select>
{
 filters: {
 role(value) {
 switch (value) {
 case 1:
 return ' 
 case 2:
 return ' 
 case 3:
 return ' 
 }
 }
 }
}

その後、他の場所で使用されるシステムでは、過去をコピーすることができます。徐々に、システムはランダムなコピーで満たされ、均質なデータを貼り付けると、一度追加、削除、および変更する必要があり、それはすべての関連する場所に影響を与えることは避けられない、そうでなければ、データの不整合の様々なされます。このタイプの問題の理由は何ですか、どのようにこれを解決するか、またはこのタイプの問題を言うのですか?次のように答えてください:

  1. この問題は、開発者がデータ構造を念頭に置いてシステムのアーキテクチャと進化を分析せず、現在のモジュールの要件の実装にのみ満足しているために発生します;
  2. このような問題を解決するためには、統一データ管理の原則に従うことが重要で、特に同種のデータについては、単一のデータソースが可能な限り保証され、セレクタ、フィルタなどの他のすべての表示形式が単一のデータソースにトレースされなければなりません。

これに基づいて、完全な設計スキームを次のセクションで説明します。ソリューション全体は、Vue 2.xフレームワークに基づいて実装されます。

デザイン

設計は単一データソースの原則に従っています。データソースはデータ辞書と呼ばれ、ソースによって、フロントエンドで管理される静的データ辞書と、バックエンドで管理され、インターフェースアクセスで提供される動的データ辞書の2種類があります。

思考マップ

データ辞書

データ辞書には2つのタイプがあり、1つはフロントエンドとバックエンドの間でネゴシエートされるキー/バリューで、もう1つはAPIインターフェースを提供するためにバックエンドからフロントエンドに動的に保持されます。静的な辞書は static-business-prefix.jsという 名前で、動的な辞書は**dynamic-business-prefix.js **という名前です。辞書のキー値は、ビジネスラインの接頭辞に加えて、分割の形式の具体的な説明に従って、詳細なデータフォーマットの実装を参照してください 。

フィルター

フィルタはグローバル形式で、システム全体に共通です。ディクショナリのキーは、line-of-business prefixで名前空間が区切られているため、衝突の可能性はありません。登録はVueプラグインの形で提供されます。

コンポーネント・リファレンス

コンポーネントの参照シナリオには、検索フォーム、フィルター、入力フォームがあります。検索フォームとエントリーフォームは、別々のコンポーネントとしてカプセル化することができます。

実現

このビジネスラインは、sgyyという3つの接頭辞に象徴されています。

データ辞書

静的辞書

新しいsrc/model/dictsディレクトリを作成し、静的辞書はこのディレクトリにstatic-sgyy.jsを作成します。

// @NOTICE ビジネス上の衝突を避けるため、ネーミングの前に対応するビジネス・ラインの略称を付けている。
export default {
 dictArr: {
 sgyyWei: [
 { key: '1', value: 'Cao Cao' }
 { key: '2', value: 'シマ・イーの }
 ],
 sgyyShu: [ 
 { key: '1', value: '劉備の },
 { key: '2', value: 'グァン・ゴン },
 		{ key: '3', value: '張飛の }
 ],
 sgyyWu: [ 
 { key: '1', value: '孫権の },
 { key: '2', value: '周瑜の }
 ]
 }
}



辞書統合エクスポートレイヤーとして、ディレクトリに新しいindex.jsファイルを作成します。

let subDictKeys = [
 './static-sgyy',
]
export default {
 dictArr: {
 common: [
 { key: '1', value: '共有辞書フィールドの }
 ]
 },
 
 // キーと値のペアへの外部変換を提供する
 getDict: function (key) {
 var arr = this.dictArr[key]
 var dict = {}
 for (var i = 0; i < arr.length; i++) {
 dict[arr[i].key + ''] = arr[i].value
 }
 return dict
 },
 
 // オリジナルの配列フォームexternalによると
 getDictArr: function (key) {
 return this.dictArr[key]
 }
}
subDictKeys.forEach(item => {
 let subDicts = require(item+'')
 dicts.dictArr = mix(dicts.dictArr, subDicts.dictArr)
})
function mix(o, n) {
 var obj = o || {}
 for (var p in n) {
 if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p))) {
 o[p] = n[p]
 }
 }
 return obj
}

ダイナミック辞書

src/model/dictsディレクトリに新しいdynamic-sgyy.jsファイルを作成します。

export default {
 sgyyAsyncWeiHero: {
 url: constant.baseUri + '/heros/wei',
 optionKey: { // どの2つのフィールドを、バックエンドから返されるデータのキー/値として使うかを指定する。一般的には、バックエンド開発者と交渉する必要がある。
 label: 'name',
 value: 'code'
 }
 }
}

フィルター

フィルタの実装は、静的なデータ辞書のためだけであり、動的な辞書は、フィルタにすることはできません、あなたはAPIインターフェイスに対応する説明を返すために必要な、フロントエンドが直接辞書の説明を表示することができます。フィルタの実装は次のとおりです。

import dicts from '@/model/dicts/index'
export default {
 install(Vue) {
 var dictArr = dicts.dictArr
 for (var key in dictArr) {
 if (dictArr.hasOwnProperty(key)) {
 (function (key) {
 Vue.filter(key, function (n) {
 var item = dicts.getDict(key)
 return item[n + ''] || '-'
 })
 }(key))
 }
 }
 }
}

コンポーネントリファレンス

静的辞書コンポーネント

静的辞書に対応するセレクタをジェネリック・コンポーネントにラップし、グローバル・コンポーネントとして登録します。コードは以下のようになります。

<template>
 <el-select :placeholder="calcPlaceholder" clearable
 :value="value + ''"
 @change="changeFn"
 >
 <el-option
 v-for="item in dictArr"
 :value="item.key"
 :key="item.key"
 :label="item.value"
 />
 </el-select>
</template>
<script>
const PLC_MAP = {
 SEARCH: ' , 
 DATAOPR: 'を選択する。
}
export default {
 name: 'StaticDictSelect',
 model: {
 prop: 'value',
 event: 'change'
 },
 props: {
 value: {
 type: String | Number,
 default: ''
 },
 dictArr: {
 type: Array,
 default: []
 },
 actionType: { // 'SEARCH' => 検索操作 | 'DATAOPR' => データ操作
 type: 'SEARCH' | 'DATAOPR',
 default: 'DATAOPR'
 },
 placeholder: ''
 },
 computed: {
 calcPlaceholder() {
 return this.placeholder || PLC_MAP[this.actionType]
 }
 },
 data() {
 return {
 PLC_MAP: {
 SEARCH: ' ,
 SELECT: 'を選択する。
 }
 }
 },
 methods: {
 changeFn(value) {
 this.$emit('change', value)
 }
 }
}
</script>
<style lang="scss" scoped></style>

コンポーネントは以下のように呼び出されます。

<template>
 <gp-page class="page">
 <gp-search>
 <el-form inline :model="table.params" >
 <el-form-item label="支払い状況">
 <gp-static-dict-select actionType="SEARCH"
 v-model="table.params.wei"
 :dictArr="searchDicts.wei"
 />
 </el-form-item>
 </el-form>
 <div slot="right">
 <el-button type="primary" @click="search(1)"> </el-button>
 <el-button @click="resetSearchFilter">空の条件</el-button>
 </div>
 </gp-search>
 <gp-table
 :tableData="table"
 :loading="table.loading"
 @change="search"
 >
 <el-table-column label="名前 "プロップ="name" min-width="200" 
 show-overflow-tooltip
 >
 <template slot-scope="scope">
 {{ scope.row.wei | sgyyWei }}
				</template>
 </el-table-column>
 </gp-table>
 </gp-page>
</template>
<script>
import dicts from '@M/dicts/index'
const INIT_SEARCH_FILTER = {
 wei: '',
}
export default {
 name: 'page',
 data() {
 return {
 searchDicts: {
 wei: dicts.getDictArr('sgyyWei'),
 },
 table: {
 params: {
 ...INIT_SEARCH_FILTER
 },
 loading: true,
 total: 0,
 page: 1,
 size: 10,
 list: []
 },
 viewOrderId: ''
 }
 },
 filters: {},
 methods: {
 async search(page) {
 // リスト・データを取得する
 const res = await this.$post('/search', {
 ...this.table.params
 })
 /* 若干のフォローアップ処理 */
 },
 
 resetSearchFilter() {
 this.table.params = {
 ...INIT_SEARCH_FILTER
 }
 }
 },
 mounted() {
 this.search()
 }
}
</script>
<style lang="scss" scoped></style>

動的辞書コンポーネント

ダイナミック・ディクショナリー・コンポーネントは、ジェネリック・コンポーネントとしてカプセル化され、以下のコードでグローバル・コンポーネントとして登録することもできます。

<template>
 <el-select
 class="dynamic-dict-select"
 :placeholder="calcPlaceholder"
 :no-data-text="noDataText || 'データなし"
 :size="actionType === 'SEARCH' ? 'small' : ''"
 v-model="innerValue"
 :filterable="filterable"
 :clearable="clearable"
 :disabled="disabled"
 :loading="loading"
 :remote="remote"
 :remote-method="remoteMethod"
 @change="change"
 @visible-change="visibleChange"
 >
 <el-option 
 v-for="item in options" 
 :key="item.value" 
 :label="item.label" 
 :value="item.value"
 />
 </el-select>
</template>
<script>
const PLC_MAP = {
 SEARCH: ' ,
 DATAOPR: 'を選択する。
}
export default {
 name: 'DynamicDictSelect',
 props: {
 actionType: {
 // 'SEARCH' => 検索操作 | 'DATAOPR' => データ操作
 type: 'SEARCH' | 'DATAOPR',
 default: 'DATAOPR'
 },
 bindSelItem: {}, // {url: '', optionKey: {label: 'labelKey', value: 'labelKey'}}
 appendParam: {}, // 追加パラメーターは、パスがあれば処理され、パスがなければ処理されない。
 validFlagNew: {
 //0を入力するとすべてをチェックし、1を入力すると正常をチェックする。
 type: Number,
 default: 0
 },
 value: {},
 label: {}, //ラベルを取得する
 filterable: {
 type: Boolean,
 default: true
 },
 clearable: {
 type: Boolean,
 default: true
 },
 disabled: false,
 load: {
 type: Boolean,
 default: false
 },
 placeholder: {
 type: String,
 default: ''
 },
 remote: {
 type: Boolean,
 default: true
 },
 paging: {
 type: Boolean,
 default: true
 }
 },
 data() {
 return {
 loading: false,
 innerValue: '',
 options: [],
 noDataText: '',
 chooseArr: []
 }
 },
 watch: {
 appendParam: {
 handler(newAppendParam, oldAppendParam) {
 this.findData('', true)
 },
 immediate: true,
 deep: true
 }
 },
 computed: {
 calcPlaceholder() {
 return this.placeholder || PLC_MAP[this.actionType]
 }
 },
 created() {
 this.remoteMethod = this.$utils.debounce(this.remoteMethod, 300)
 if (this.value) {
 this.innerValue = JSON.parse(JSON.stringify(this.value))
 }
 this.findData()
 },
 methods: {
 visibleChange(visible) {
 if (visible) {
 this.findData()
 }
 },
 async findData(keywords, forceUpdate) {
 let pageIndex = 1
 let pageSize = 50
 const options = this.options
 if (options.length > 0 && !forceUpdate) {
 return
 }
 this.chooseArr = []
 let isRemote = this.remote
 if (this.loading) {
 return
 }
 if (!isRemote && this.options.length > 0) {
 return
 }
 this.loading = true
 let params = {
 keywords: keywords || '',
 name: keywords || ''
 }
 // 追加パラメーターを扱う
 let appendParam = this.appendParam
 for (let key in appendParam) {
 if (appendParam.hasOwnProperty(key)) {
 params[key] = appendParam[key]
 }
 }
 params.validFlagNew = this.validFlagNew
 if (this.paging) {
 params.pageNum = pageIndex
 params.pageSize = pageSize
 params.rows = pageSize
 }
 let bindSelItem = this.bindSelItem
 // リストをロードする前に値を代入する問題を解決する
 let cache = ''
 if (this.innerValue) {
 cache = JSON.parse(JSON.stringify(this.innerValue))
 this.innerValue = ''
 }
 const result = await this.$post(bindSelItem.url, {
 data: params
 })
 
 if (result.err) {
 this.noDataText = result.errmsg
 } else {
 const data = result.data || {}
 const arr = []
 let list = []
 if (data.list) {
 list = data.list
 } else {
 list = data
 }
 list = list.splice(0, pageSize)
 list.forEach(item => {
 this.chooseArr.push(item)
 arr.push({
 label: item[bindSelItem.optionKey['label']],
 value: item[bindSelItem.optionKey['value']]
 })
 })
 this.options = arr
 setTimeout(() => {
 this.$emit('load-data', JSON.parse(JSON.stringify(arr)))
 }, 10)
 // 値があり、リストが読み込まれておらず、ドロップダウン・リストが表示状態にあるときに、ラベルが正しく表示されない問題を解決するために遅延を設定する。
 setTimeout(() => {
 if (cache) {
 this.innerValue = cache
 }
 }, 25)
 this.noDataText = ''
 }
 },
 change(e) {
 this.$emit('input', e)
 this.getLabel(e)
 this.$emit('change', e)
 },
 getLabel(value) {
 let label = ''
 let obj = {}
 for (let i = 0; i < this.options.length; i++) {
 const option = this.options[i]
 if (option.value == value) {
 label = option.label
 obj = this.chooseArr[i]
 break
 }
 }
 this.$emit('update:label', label)
 this.$emit('listenToChildListLabel', label)
 this.$emit('listenToChildListEvent', obj)
 },
 remoteMethod(query) {
 this.findData(query, true)
 }
 },
 watch: {
 value(value) {
 this.innerValue = value
 this.getLabel(value)
 }
 }
}
</script>
<style lang="scss">
.gp-dynamic-dict-select {
 .el-select__caret {
 &.el-input__icon:not(.el-icon-circle-close) {
 &:before {
 content: '\E6E1';
 }
 }
 }
}
</style>

動的辞書コンポーネントの参照方法の例を以下に示します。

<template>
 <div class="page">
 <layout-search>
 <el-form
 ref="sgyySearchForm"
 inline
 label-width="100px"
 :model="searchForm"
 >
 <el-form-item label="Wei Generals" プロップ="weiHero">
 <gp-dynamic-dict-select
 actionType="SEARCH"
 v-model="searchForm.weiHero"
 :bindSelItem="bindDynamicSel.sgyyAsyncWeiHero"
 />
 </el-form-item>
 </el-form>
 <div slot="right">
 <el-button type="primary" @click="search(1)"> </el-button>
 </div>
 </layout-search>
 <el-dialog :visible="visble">
 <el-form ref="sgyyDataForm" inline label-width="100px" :model="dataForm">
 <el-form-item label="Wei Generals" プロップ="weiHero">
 <gp-dynamic-dict-select
 actionType="DATAOPR"
 v-model="dataForm.weiHero"
 :bindSelItem="bindDynamicSel.sgyyAsyncWeiHero"
 />
 </el-form-item>
 </el-form>
 </el-dialog>
 </div>
</template>
<script>
import bindDynamicSel from "@M/dicts/dynamic-sgyy";
export default {
 data() {
 return {
 searchForm: {
 weiHero: "",
 },
 bindDynamicSel: bindDynamicSel,
 visble: false,
 dataForm: {
 weiHero: "",
 },
 };
 },
 methods: {
 async search(page) {
 const searchPage = page || 1;
 const result = await this.$post(constant.baseURI + "/search/heros", {
 data: {
 ...this.searchForm,
 pageNum: searchPage,
 pageSize: 20,
 }
 })
 /* 若干のフォローアップ処理 */
 },
 },
};
</script>
<style lang="scss" scoped></style>

最適化

概要

この統合データ・ディクショナリ管理ソリューションは、システム・ディクショナリ・データの統一的かつ集中的な管理を実現し、セレクタやフィルタを含むすべてのインターフェース・フォームが単一のデータ・ソースに依存するようにします。結局、システム・インターフェースやインタラクション・パターンがどのように変化しても、基盤となるデータ層は安定したままです。製品が反復し進化しても、単一のデータソースを変更するだけでよいので、今後グローバル検索と置換に別れを告げるのは簡単です。

最後に、いつものようにソースコードを添付します:

Read next

Androidユニットテスト

ユニットテストとは何ですか?Javaプログラムの最小の機能単位はメソッドなので、Javaプログラムのユニットテストは単一のJavaメソッドのテストです。 ユニットテストの利点は何ですか?ユニットテストの学習において

Sep 5, 2020 · 3 min read