- 投稿日:2019-04-13T23:02:17+09:00
VuejsのGUIジェネレーターを改良した話
はじめに
作成中のGUIジェネレータを改良した。
改良点
vuetify 以外のコンポーネントフレームワークを利用可能にした
任意の css と javascript を読み込めるようにしたため好きなフレームワークを利用できるようになった。
プロジェクトデータの保存と読み込みを可能にした
作成中のプロジェクトをjsonファイルでダウンロードできるようにし、ファイルを読み込むことで保存した状態を再現できるようにした。
任意のインラインスクリプトを挿入できるようにした
Vue.use(plugin)などが書けるようにすることが目的
技術的な話
iframeのリロード
スクリプトを読み直したりしたときはiframeをリロードしないといけないが以下のような感じで書けた
private reload() { const ele = obtainIframe(); ele.onload = () => { // 必要な処理 }; ele.contentWindow.location.reload(); }インラインスクリプトが更新された時もリロードするが、イベントの間引きがしたかった。
_.debounceを使おうと思っていたが、https://github.com/duxiaofeng-github/typescript-debounce-decorator が使いやすくてよかった。こんな感じで使える。
@debounce(500, { leading: false }) private reload() { }類似ツール
今回の更新中に見つけた。
- PreVue
- 完全に使い方を理解していないが目的が異なるようなイメージを持った
終わりに
今後やりたいこと。
- 追加したい機能
- 複数ページへの対応 (vue-routerで)
- attribute の型指定
- 機能以外で対応したいこと
- リファクタリング
- CI・CDの導入
- 投稿日:2019-04-13T22:02:28+09:00
Vue.js to TypeScriptの書き方一覧
最近ひたすら既存のVueプロジェクトのTypeScript化をやっているのでメモとしてまとめます。
随時追加予定。前提
- .vueファイルで<script lang="ts">を使う
- vue-property-decoratorを使ったデコレータ方式のTypeScript化を行う
data
dataはそのままクラスのプロパティとして宣言します。
JavaScript
<script > export default { name: 'Human', data() { return { name: '山田 太郎', age: 19 }; } }; </script>TypeScript
<script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class Human extends Vue { name: string = '山田 太郎'; age: number = 19; } </script>components
SFCで他Vueコンポーネントを参照する際に使うComponentsは
@Component()デコレータにcomponentsをkeyに持つオブジェクトを渡し定義します.JavaScript
<script> import Header from './Header' import Footer from './Footer' export default { name: 'Page', components: { Header, Footer } } </script>TypeScript
<script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; import Header from './Header.vue'; import Footer from './Footer.vue'; @Component({ components: { Header, Footer } }) export default class Page extends Vue { } </script>props
親コンポーネントからデータを受け取るpropsは@Propデコレータを使い定義します。
@Prop(options: (PropOptions | Constructor[] | Constructor) = {})JavaScript
<script> export default { name: 'Post', props: { contents: { default: '', type: String, require: true }, postNumber: { default: 0, type: [Number, String], require: true }, publish: { default: true, type: Boolean, require: true }, option: { type: Object, require: false } // optionには {new: boolean, important: boolean, sortNumber: number} が設定される想定 } } </script>TypeScript
<script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator'; @Component export default class Post extends Vue { @Prop({ default: '' }) contents!: string; @Prop({ default: 0 }) postNumber!: number | string; @Prop({ default: false }) publish!: boolean; @Prop option?: { new: boolean, important: boolean, sortNumber: number }; } </script>computed
算出プロパティはgetter付きのメソッドで定義します。
JavaScript
<script> export default { name: 'Human', data () { return { firstName: '太郎', lastName: '山田' } }, computed: { fullName () { return `${this.firstName} ${this.lastName}` } } } </script>TypeScript
<script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class Human extends Vue { firstName: string = '太郎'; lastName: string = '山田'; get fullName() { return `${this.firstName} ${this.lastName}`; } } </script>emit
親コンポーネントに値を送信する$emitは@Emitデコレータを使いfunctionとして定義します。
メソッド名が、$emitに渡すイベント名がメソッド名と同様の場合は、@Emit()の引数(イベント名)は省略できます。
※ 以下のcountUpのケース。camelCaseとkebabe-caseは自動で変換されます@Prop(options: (PropOptions | Constructor[] | Constructor) = {})JavaScript
<script> export default { name: 'Counter', data () { return { count: 0 } }, methods: { countUp (n) { this.count += n this.$emit('count-up', n) }, resetCount () { this.count = 0 this.$emit('reset') } } } </script>TypeScript
<script lang="ts"> import { Component, Emit, Vue } from 'vue-property-decorator'; @Component export default class Counter extends Vue { count: number = 0; @Emit('count-up') countUp(n) { this.count += n; return n; } @Emit('reset') resetCount() { this.count = 0; } } </script>model
input要素のコンポーネント化の際にvalueが競合しないように用いるmodelは
@Modelデコレータを使ってプロパティを定義します。@Model(event?: string, options: (PropOptions | Constructor[] | Constructor) = {})JavaScript
<script> export default { name: 'MyRadio', model: { prop: 'checked', event: 'change' }, props: { checked: { type: Boolean } } } </script>TypeScript
<script lang="ts"> import { Component, Model, Vue } from 'vue-property-decorator'; @Component export default class MyCheckBox extends Vue { @Model('change', { type: Boolean }) readonly checked!: boolean; } </script>watch
dataの変更を検知するWatchは@Watchデコレータを使ってメソッドを定義します。
optionにwatchオプションのimmediateやdeepもオブジェクトとして渡すことで指定できます。@Watch(path: string, options: WatchOptions = {})JavaScript
<script> export default { name: 'InputText', data () { return { text: '', lengthDiff: 0 } }, watch: { text: function (newText, oldText) { this.lengthDiff = newText.length - oldText.length } } } </script>TypeScript
<script lang="ts"> import { Component, Vue, Watch } from 'vue-property-decorator'; @Component export default class InputText extends Vue { text: string = ''; lengthDiff: number = 0; @Watch('text') onTextChanged(newText: string, oldText: string) { this.lengthDiff = newText.length - oldText.length; } } </script>provide, inject
深い階層でも親コンポーネントと子コンポーネントでデータを共有する際に用いるprovideとinjectは@provide, @injectデコレータで定義します。
@Provide(key?: string | symbol) @Inject(options?: { from?: InjectKey, default?: any } | InjectKey)JavaScript
Parent.vue
<script> export default { name: 'Parent', provide: { commonValue: 'foo' } } </script>Child.vue
<script> export default { name: 'Child', inject: { commonValue: { default: 'hoge' } } } </script>TypeScript
Parent.vue
<script lang="ts"> import { Component, Provide, Vue } from 'vue-property-decorator'; @Component() export default class Parent extends Vue { @Provide() commonValue = 'foo'; } </script>Child.vue
<script lang="ts"> import { Component, Inject, Vue } from 'vue-property-decorator'; @Component export default class Child extends Vue { @Inject({ default: 'hoge' }) readonly commonValue!: string; } </script>mixins
コンポーネント間の共通処理を定義するミックスインはMixins()の継承を使って表現します。
ミックスイン自身もVueを継承したクラスとして定義します。
複数のミックスインを使う場合も、Mixins()の引数として与えることが可能です。
(最大5つ)
https://github.com/vuejs/vue-class-component/blob/master/src/util.ts#L35JavaScript
mixinLogger.js
export default { created () { const now = new Date console.log(`created: ${now}`) }, methods: { log (param) { console.log(`log: ${param}`) } } }Human.vue
<script> import MixinLogger from './mixinLogger' export default { name: 'Human', mixins: [MixinLogger], methods: { greeting () { console.log('おはよう') this.log('call greeting') } } } </script>TypeScript
mixin.ts
import { Component, Vue } from 'vue-property-decorator'; @Component export default class mixinLogger extends Vue { created() { const now = new Date; console.log(`created: ${now}`); } log(param) { console.log(`log: ${param}`); } }Human.vue
<script lang="ts"> import MixinLogger from './mixinLogger'; import { Component, Mixins } from 'vue-property-decorator'; @Component export default class Human extends Mixins(MixinLogger) { greeting() { console.log('おはよう'); this.log('call greeting'); } } </script>created, mounted, updated, destroyed ...etc
ライフサイクルフックはそのまま同名のメソッドとして宣言することですることで実装可能です。
JavaScript
<script> export default { name: 'Human', created: function () { // 何か処理 }, mounted: function () { // 何か処理 }, updated: function () { // 何か処理 }, destroyed: function () { // 何か処理 } } </script>TypeScript
<script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class Human extends Vue { created() { // 何か処理 } mounted() { // 何か処理 } updated() { // 何か処理 } destroyed() { // 何か処理 } } </script>参考
https://github.com/vuejs/vue-class-component
https://github.com/kaorun343/vue-property-decorator
- 投稿日:2019-04-13T22:02:28+09:00
Vue.js to TypeScriptの覚え書き
最近ひたすら既存のVueプロジェクトのTypeScript化をやっているのでメモとしてまとめます。
随時追加予定。前提
- .vueファイルで<script lang="ts">を使う
- vue-property-decoratorを使ったデコレータ方式のTypeScript化を行う
data
dataはそのままクラスのプロパティとして宣言します。
JavaScript
<script > export default { name: 'Human', data() { return { name: '山田 太郎', age: 19 }; } }; </script>TypeScript
<script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class Human extends Vue { name: string = '山田 太郎'; age: number = 19; } </script>components
SFCで他Vueコンポーネントを参照する際に使うComponentsは
@Component()デコレータにcomponentsをkeyに持つオブジェクトを渡し定義します.JavaScript
<script> import Header from './Header' import Footer from './Footer' export default { name: 'Page', components: { Header, Footer } } </script>TypeScript
<script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; import Header from './Header.vue'; import Footer from './Footer.vue'; @Component({ components: { Header, Footer } }) export default class Page extends Vue { } </script>props
親コンポーネントからデータを受け取るpropsは@Propデコレータを使い定義します。
@Prop(options: (PropOptions | Constructor[] | Constructor) = {})JavaScript
<script> export default { name: 'Post', props: { contents: { default: '', type: String, require: true }, postNumber: { default: 0, type: [Number, String], require: true }, publish: { default: true, type: Boolean, require: true }, option: { type: Object, require: false } // optionには {new: boolean, important: boolean, sortNumber: number} が設定される想定 } } </script>TypeScript
<script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator'; @Component export default class Post extends Vue { @Prop({ default: '' }) contents!: string; @Prop({ default: 0 }) postNumber!: number | string; @Prop({ default: false }) publish!: boolean; @Prop option?: { new: boolean, important: boolean, sortNumber: number }; } </script>computed
算出プロパティはgetter付きのメソッドで定義します。
JavaScript
<script> export default { name: 'Human', data () { return { firstName: '太郎', lastName: '山田' } }, computed: { fullName () { return `${this.firstName} ${this.lastName}` } } } </script>TypeScript
<script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class Human extends Vue { firstName: string = '太郎'; lastName: string = '山田'; get fullName() { return `${this.firstName} ${this.lastName}`; } } </script>emit
親コンポーネントに値を送信する$emitは@Emitデコレータを使いfunctionとして定義します。
メソッド名が、$emitに渡すイベント名がメソッド名と同様の場合は、@Emit()の引数(イベント名)は省略できます。
※ 以下のcountUpのケース。camelCaseとkebabe-caseは自動で変換されます@Prop(options: (PropOptions | Constructor[] | Constructor) = {})JavaScript
<script> export default { name: 'Counter', data () { return { count: 0 } }, methods: { countUp (n) { this.count += n this.$emit('count-up', n) }, resetCount () { this.count = 0 this.$emit('reset') } } } </script>TypeScript
<script lang="ts"> import { Component, Emit, Vue } from 'vue-property-decorator'; @Component export default class Counter extends Vue { count: number = 0; @Emit('count-up') countUp(n) { this.count += n; return n; } @Emit('reset') resetCount() { this.count = 0; } } </script>model
input要素のコンポーネント化の際にvalueが競合しないように用いるmodelは
@Modelデコレータを使ってプロパティを定義します。@Model(event?: string, options: (PropOptions | Constructor[] | Constructor) = {})JavaScript
<script> export default { name: 'MyRadio', model: { prop: 'checked', event: 'change' }, props: { checked: { type: Boolean } } } </script>TypeScript
```vue
```watch
dataの変更を検知するWatchは@Watchデコレータを使ってメソッドを定義します。
optionにwatchオプションのimmediateやdeepもオブジェクトとして渡すことで指定できます。@Watch(path: string, options: WatchOptions = {})JavaScript
<script> export default { name: 'InputText', data () { return { text: '', lengthDiff: 0 } }, watch: { text: function (newText, oldText) { this.lengthDiff = newText.length - oldText.length } } } </script>TypeScript
<script lang="ts"> import { Component, Vue, Watch } from 'vue-property-decorator'; @Component export default class InputText extends Vue { text: string = ''; lengthDiff: number = 0; @Watch('text') onTextChanged(newText: string, oldText: string) { this.lengthDiff = newText.length - oldText.length; } } </script>provide, inject
深い階層でも親コンポーネントと子コンポーネントでデータを共有する際に用いるprovideとinjectは@provide, @injectデコレータで定義します。
@Provide(key?: string | symbol) @Inject(options?: { from?: InjectKey, default?: any } | InjectKey)JavaScript
Parent.vue
<script> export default { name: 'Parent', provide: { commonValue: 'foo' } } </script>Child.vue
<script> export default { name: 'Child', inject: { commonValue: { default: 'hoge' } } } </script>TypeScript
Parent.vue
<script lang="ts"> import { Component, Provide, Vue } from 'vue-property-decorator'; @Component() export default class Parent extends Vue { @Provide() commonValue = 'foo'; } </script>Child.vue
<script lang="ts"> import { Component, Inject, Vue } from 'vue-property-decorator'; @Component export default class Child extends Vue { @Inject({ default: 'hoge' }) readonly commonValue!: string; } </script>mixins
コンポーネント間の共通処理を定義するミックスインはMixins()の継承を使って表現します。
ミックスイン自身もVueを継承したクラスとして定義します。
複数のミックスインを使う場合も、Mixins()の引数として与えることが可能です。
(最大5つ)
https://github.com/vuejs/vue-class-component/blob/master/src/util.ts#L35JavaScript
mixinLogger.js
export default { created () { const now = new Date console.log(`created: ${now}`) }, methods: { log (param) { console.log(`log: ${param}`) } } }Human.vue
<script> import MixinLogger from './mixinLogger' export default { name: 'Human', mixins: [MixinLogger], methods: { greeting () { console.log('おはよう') this.log('call greeting') } } } </script>TypeScript
mixin.ts
import { Component, Vue } from 'vue-property-decorator'; @Component export default class mixinLogger extends Vue { created() { const now = new Date; console.log(`created: ${now}`); } log(param) { console.log(`log: ${param}`); } }Human.vue
<script lang="ts"> import MixinLogger from './mixinLogger'; import { Component, Mixins } from 'vue-property-decorator'; @Component export default class Human extends Mixins(MixinLogger) { greeting() { console.log('おはよう'); this.log('call greeting'); } } </script>created, mounted, updated, destroyed ...etc
ライフサイクルフックはそのまま同名のメソッドとして宣言することですることで実装可能です。
JavaScript
<script> export default { name: 'Human', created: function () { // 何か処理 }, mounted: function () { // 何か処理 }, updated: function () { // 何か処理 }, destroyed: function () { // 何か処理 } } </script>TypeScript
<script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class Human extends Vue { created() { // 何か処理 } mounted() { // 何か処理 } updated() { // 何か処理 } destroyed() { // 何か処理 } } </script>参考
https://github.com/vuejs/vue-class-component
https://github.com/kaorun343/vue-property-decorator
- 投稿日:2019-04-13T19:01:11+09:00
ASP.NET CoreとVue.jsとHTTP Streamingでオンラインゲーム作ってみたおはなし
あすかです。ニャーン。
ちょっとなんか作っていたので、技術的にどうのこうのってのをちょっとお話してみようと思いますー。
作ったもの
三国志NET KMY Versionというゲームです。
10〜13年前にも同名のゲームがありましたが、あれはあすかが中学生の時に原作から引っ張って運営してた別のやつです。
今回はイチから作り直して、システム的には昔とはほぼ別物になってます。後述しますが、三国志NETは昔はやっていたゲームです。当時がなつかしいなーと思って、思わず作ってしまいました。
今は他のサーバで仲良くなった人や、旧KMYをプレイしていた人にテストをお手伝いしてもらってる感じですー。ソースコードはGitHubで公開しています。
分類 GitHubリポジトリ 言語 フレームワーク 開発環境 サーバサイド GitHub C# ASP.NET Core 2.1、EntityFrameworkCore VS4M '17/'19 クライアントサイド GitHub TypeScript Vue.js VSC 最初はDDDで作ろうと思ってましたが、DDDでデータベースを扱うのは難しいなーと思ったので、途中からトランザクションスクリプトに変えてあります。
コードにはいろいろ見苦しいところが多いのです‥‥。CGIゲームとしての三国志NETとは
三国志NETはもともと、maccyu氏の制作した、Perlで記述された2000年代のCGIゲームです。
協力型・コマンド先行入力型のオンライン戦略シミュレーションゲームです。コマンド先行入力型って今どきのゲームではめったに見かけませんよね。
例えば、「2018年4月1日19時00分」という項目に「農業開発」コマンドを設定します。
すると、その日付と時刻になったら、コマンドが実行され、都市の農業が上がります。
コマンドは、あるところでは60分、あるところでは30分などといった間隔で入力することができます。
KMYは10分間隔です。(俗に10分鯖と呼ばれます)コマンドをあらかじめ入力することができるので、忙しくても1日3〜5分程度のログインで十分プレイ可能だったりする反面、
コマンド実行までタイムラグがあるため、先の結果が予想しづらい、人によっては待たされている感じがする部分もあるかもしれません。
また、仕組み的にログイン時間の長い人(俗にONの多い人、廃ON、玉葱)が有利になるので、武将能力以上にONが重要となる状況も出てきたりしてて、それもデメリットの1つに数えられます。
10分更新だと展開も速くなるので、本当に忙しい人にはつらかったりしますが。。今の世代の人たちがどれだけ知っているかはわかりませんが、
当時を知る人たちにとってはなつかしいんじゃないでしょうか。そのゲームスクリプトは誰でもダウンロードできるものでしたから、
それぞれがダウンロードして、思い思いの改造をして公開していた感じです。2006年から2009年ころにかけて、一部の人の間で流行していました。
今も残っている翼、伝説、秘密、Nika、今は人数ほぼないけどリベラ、一休、黒子、そのほかにも、幻想、NEO、蒼天、圭、狂、大人、そしてあすかが10〜13年前くらいに運営していた旧KMYなどなど、いろいろなサーバがあり、それぞれが独自の改造をして世界を創っていました。似たようなもの(配布型CGIゲーム)に、TOWN、商人物語、罪と罰などなどがありました。
当時はCGIゲームが人気の時代でした。今回の三国志NETを技術的に以前のものと比較すると
まず、今回作った三国志NETは、CGIゲームではないです。
maccyu氏の制作したスクリプト(以下「原作」)を参考に、ゲームシステムやいろいろな計算式をもってきはしましたけど、プログラム自体は一から組み直したものになります。
ASP.NET Coreアプリケーションサーバとして、MariaDBとつなげて開発しました。開発
HTTP Streaming
今回作ったゲームの売りは、リアルタイム更新かもですねー。他の三国志NETにはないというか、CGIだとそもそもここまで柔軟なものは作れないと思います。
リアルタイム更新といっても、プレイヤーが動きたいときに自由に動き回れるという意味ではなく、コマンドが10分に1回ずつ実行されるんですが、その実行結果がすぐ画面に反映される。都市の内政が、画面を手動で更新しなくても反映されるんです。
ゲーム内チャット(手紙)でも同じです。誰かがチャットに書き込んだら、その中身が他の人に配信される。
戦争も同じです、誰かが攻め込んだらその結果が全員に配信されます。都市が落ちたら、他の人達の画面の地図も変わります。三国志NETをリアルタイム更新にした理由
上にも書きましたが、三国志NETはコマンド先行入力型ゲームです。10分更新と言いましたね。あれ、1人の武将あたりなんです。
実は武将ごとに、更新時刻が違います。同じターンでも、武将Aは19時2分更新(俗に2分更新)、武将Bは19時4分更新(俗に4分更新)と、ばらばらになっています。
1人の武将は10分に1回しかコマンドが実行されなくても、たくさんの武将がいると、それだけのコマンドが順に実行されるわけです。なので、例えば戦争中の場合、次に戦局が変わるのは10分後とは限りません。特に、国同士が潰し合って残り2国になったあとでおこなわれる最後の戦争(ラス戦)では、参加する武将の数も多いので、人数にもよりますが、30秒〜2分ごとに戦局が変わるわけです。
誰かが都市を支配して地図が変わった場合など、戦局に応じて実行するコマンドが変わる場合もあります。作戦も変わる場合があります。そのうえに、作戦を決める人は、新しい作戦を、たくさんの武将に伝える必要があります。
そういうのを加味すると、三国志NETって、常に最新の情報をとってこなければいけない。三国志NETだけでなく当時のCGIゲーム全般の問題ですが、最新の状況を知るためには、手動で更新ボタンを押さなければいけません。
更新ボタンを押すと毎回HTML描画します。必然的に、ページのリロードが大量発生します。
それに、たとえば最新の手紙(チャット)を読みたいと思っても、それ以外のデータ、地図とか武将情報とかもいちいちダウンロードしなければいけない。このオーバーヘッドが非常に邪魔なわけです。
それに、
- リロードした瞬間に戦局が変わったらどうするのか?
- 新しい作戦を書こうと思って手紙を書いている途中に状況が変わっても気づかないのではないか?
- みんなの意見を聞きながら作戦を決めるのは、10分鯖くらい展開が速すぎるとやりづらいのではないか?
- ただでさえ長時間のONを要求されるのに、ながら作業がやりづらいのは負担に感じるのではないか?(作業途中にいちいちブラウザまでマウスカーソル移動して更新ボタン押さないといけない)
いろいろあって、三国志NETというシステム、特に展開の速い10分鯖には、リアルタイム更新が非常によく合うと思いました。
pushとpolling
サーバのデータをクライアントでリアルタイムに表示する手段として、サーバとクライアントを同期する手段は、大きく分けて2つあります。
pushとpollingです。polling
クライアントが、3秒間隔とかでサーバにいちいち最新情報を問い合わせに行きます。TCP接続によるコストだけでなく、サーバはいちいちデータベース接続を初期化したりしなければいけないので大変です。負荷かかります。
push
サーバが自分からクライアントにデータを送る感じです。サーバが選択した必要最低限の情報しか回線に乗らないので、ネット通信のコストも抑えられます。クライアントからのリクエストもあまりないので、サーバのCPU負荷はかかりにくいですが、同時接続数を食うので、それだけメモリ消費が大きくなります。
今回はpushのほうを採用しました。ていうかもう、人数が5人とか10人とか決まっている場合はpollingでもある程度効率的なシステムは組めたと思うのですが、今回は
- 参加人数が不定
- 更新間隔も不定
- 1人の更新は10分間隔だけど、人によって更新時刻が異なるため、次に状況が変わるのが何分後なのか読めない
- 宣戦布告や同盟締結、チャットの新しいメッセージなどの情報は武将が手動で操作するため、最新情報を取得するのにpollingだと限界がある
などの条件から、pollingとして実装するメリットは1つもないと思いました。ていうかpollingに手軽・シンプル・レガシーなやつが使える以外のメリットってあったっけ。
Comet?pollingやんあれ(´・ω・`)
今回のpush通信の実装
SignalRなんてなかった
ASP.NETでリアルタイム通信と言えばSignalR!ってのがいろんなとこで言われてると思うんですが、
開発のための調査の段階では、存在そのものに気づかなかった(´・ω・`)
半年くらい下積みして、さあ作ろうってなった直前に発表された感じのやつです。存在自体はずっと前からあったようですが、
ASP.NET Coreの機能としてMSDNに載ったのは昨年11月ころ?ってGoogleやGitHubの履歴に描いてありました。
それ以前でもどっかにドキュメント転がってたはずなんですが、
あすかの探し方が悪かったのか、まったく上がってきてなかったです。で、今年に入ってSignalRの存在を知った頃には、もうリアルタイム更新のシステムはほぼ固まってしまったし
今のままでも安定して動作しているようですし、
今更わざわざSignalRに切り替える理由もないじゃろなーと思って今に至ります。この手のシステムって、リアルタイム更新のコア部分を真っ先に作るんですよね‥‥汗
ただ、車輪の再発明(今回当てはまるか分からんけど)にはデメリットも多いので、いつか検討しなくちゃなーって思うところはあります。WebSocketなんてなかった
リアルタイム通信をHTTP Streamingで実装しちゃった後に知ったんですよねこれ。
下調べの時何やってたの、としか。今回のシステムは双方向で通信をするものなので、WebSocketのほうが都合がいいかなーと思ったんですが、もういいわこれめんどいと思ってぽいしました。
会社で同じような案件が来たら挑戦してみたい‥‥。こういう案件永遠に来ないと思うけど。SignalRは内部でHTTP Streaming、WebSocket、その他にもいろいろな通信手法から環境にあわせて選択してくれるらしいので、SignalR使えば同時に解決できる問題だと思うんですけどねー。
みんなは下調べちゃんとしよう!(教訓)event-streamなんてなかった
今回のシステム、HTTP Headerには
Content-Type: event-stream;なんて書いちゃっているんですけども、
event-streamってあれじゃないですか、start: data: [でーた] data: [でーた]みたいに、データの前に
data:ってつくのが本来の仕様なんですよね。
今回、経緯はよく分かりませんけど、開発の過程の中のどこで間違えたのか、data:を省略してしまい、{"type": 12, ...} {"type": 12, ...}みたいに、ひたすらJSONだけを流す感じになってしまいました。
別にこのままでも使えるんですけど、event-stream本来の様式ではないので、Node.jsの各種すごいライブラリとか、あとはevent-streamを想定したHTML5 APIとかが使えないのは痛かったかなーとおもいます。クライアント側の実装
この手の接続って、特殊なんですよね。
何時間もかけて大きなファイルをダウンロードしているのと同じ状態でして、まだ接続が完了していない状態で、それまでに送られてきた情報を見なくちゃいけない。
これ、jQueryやaxiosではもちろんできません。対応していません。
じゃあクライアントではどうやって受信しているかというと、やっぱりXMLHttpRequest直打ちしかなかったです。// ※シンプルに読めるようにするため、あえて古いバージョンのソースを載せています。 // 最新の完全版コードはGitHubから // https://github.com/kmycode/sangokukmy-client/blob/master/src/api/streaming.ts // リクエストを受け取っていないか確認 let length = 0; const ajaxTimer = setInterval(() => { if (this.ajax != null) { if (length !== this.ajax.responseText.length) { const updatedText = this.ajax.responseText.slice(length); length = this.ajax.responseText.length; // JSONになおしてイベント発行 const lines = updatedText.split('\n'); lines.filter((line) => line).forEach((line) => { this.output(line); }); } } else { clearInterval(ajaxTimer); } }, 100);データを改行で区切って、1行ずつ読んでいます。
しかもこれ、サーバから送られてくるデータはXMLHttpRequest.responseTextの中に文字列としてまとめて入っているんですが、
「古い情報はいらないよ、新しい情報だけ送ってくれればそれでいいよ」みたいなことはしてくれなかったです。
常に、接続開始当初からのすべてのデータがいっぺんに入れられているんですね。なので、ストリーミングの接続時間が伸びれば伸びるほど、この
responseTextに入っている文字列もえらい長くなって、
それをsliceするときとか、えらい重くなるんじゃないかなーって心配してたりします。
sliceが内部ではヒープ確保・メモリコピーではなく、単にC#の(名前忘れた)みたいに長い文字列の一部分をさすポインタみたいなやつだったらいいんですけどなー。そうゆう仕様だったらすみません。。今の所特にこれといった障害もなく動いてくれているのはいいんですけど、いつか送信内容を本来の
event-streaming仕様にして、それを想定したライブラリ使えるようにしたいなーってのが本音です。サーバ側の実装
クライアントと比べると簡単です。
ストリーミングのメソッドはここにあります。https://github.com/kmycode/sangokukmy/blob/master/SangokuKmy/Controllers/SangokuKmyStreamingController.csまず、コントローラのメソッドに
asyncをつけます。
そもそもストリーミング自体、同時に複数接続していることを前提にしているシステムですから、asyncがないと話にならない。public async Task StatusStreamingAsync()次に、HTTPヘッダを送ります。
// HTTPヘッダを設定する this.Response.Headers.Add("Content-Type", "text/event-stream; charset=UTF-8"); this.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");この手の接続ではキャッシュをとらせないようにするのが作法です。ついてにいうと、クライアント側でも乱数とか日付とかつこて毎回適当にURL変えるべきです。でないと、PCは大丈夫でもスマホで引っかかりまくりますから。(実話)
データを文字列として送信する時は、
await this.Response.WriteAsync(initializeData.ToString());というふうに、
ResponseのWriteAsyncを使います。
そしてあとは、今回のシステム内で別途作ったストリーミングクラスに接続情報を渡して、接続が閉じるまでひたすら待機します。
コントローラのメソッドをすぐに終わらしてはいけません。そんなことをしたら接続が完了しておしまいです。// ストリーミング対象に追加し、対象から外れるまで待機する var isRemoved = false; StatusStreaming.Default.Add(this.Response, this.AuthData, chara, () => isRemoved = true); while (!isRemoved) { await Task.Delay(5000); }この
whileが終わったら、接続切れた後に必要な処理があればそれを実行して終わりです。
ちなみに接続が切れたかどうかを検出するには、Response.HttpContext.RequestAborted.IsCancellationRequestedが使えます。これが
trueになっていれば、切断が検出されたということです。ただ、これ、ユーザが実際に接続を切ってから切断が検出されるまでにいくらか空きがありまして、
あすかのスマホ(iOSのSafari)ですと、スマホの電源を切ってから(※スリープではない)、切断状態が検出されるまで10分くらい空きがありました。
過信は禁物らしいです。余談ですが、スマホからだと、ページを開いている間、常にローディングのくるくるが回り続けるんですよね‥‥。
仕組み上仕方ないものではありますが、問い合わせがあった時にきちんと答えられるか今から不安なのです。Vue.jsの選択
これだけのものを作るためには、まずモデルとUIをバインディングできることが重要かなーと考えました。
データを変更するだけで、画面に反映される。画面描画の手間が省略できるので、大量のデータを扱うには必要不可欠です。
特に、リアルタイム更新だと、サーバから絶え間無くデータが送られてくるわけですから、プログラマはそれをデータに保存するだけで、実際の画面処理はフレームワークがやる。
というわけで、Vue.jsに白羽の矢がたちました。なぜjQueryだとだめだったのか
バインディング
大量のデータを扱うのにjQueryだと限界がある。jQueryだと、データが来るたびに各コントロールの更新処理を一気にやらなければいけないんです。その更新処理が非常に厄介。
例えば、誰かがどこかの都市を支配し、新しい都市データが配信されるとします。それはすなわち、その都市の所属国が変わることを意味します。
画面描画だけでも、
- 地図上の都市の色を変える
- その都市に所在している武将の画面では、
- 都市情報タブの色を変える
- 都市情報タブのタイトル、中身を変える
- 新しい中身にあわせて、都市情報の農業・商業とかの内政進捗をあらわす青いバーの横幅を変える
- 都市を支配している国情報タブの中身も同じように変える
というふうに、いろいろやることがあります。
しかもこれ、都市データに限った話です。実際は都市データだけではなく、情勢を伝えるログ(マップログ)、支配した武将の場合は武将行動ログ、というように、いろいろな種類のデータが一気に来るのです。それらに対して、上みたいな大変な処理をいちいちやらなければいけない。
これはもう、モデルバインディングでないと無理だと思いました。ていうか、Vue.jsじゃないと2ヶ月でここまで作れないし、今みたいな1〜2万行規模のクライアントなんてとてもできない。セレクタ
jQueryのセレクタはたしかに便利です。特定のクラス名を持った要素を参照する、その子要素を参照する、などなど。
小規模アプリを作るならVue.jsよりもjQueryのほうが早いというのは、私はそう思います。ただ、大規模アプリを開発する時はVue.jsかなーと思います。
その理由ですが、jQueryだと、せっかくセレクタで要素をとってきても、それが期待通りのDOM構造でないと誤動作起こすんです。jQueryコードとHTML(DOM)が密接に結びつくことになるんです。これだと、HTMLを変えたくなってもなかなか自由に変えられない。
そのぶん、Vue.jsはバインディングによってデータを反映させるわけですから、HTMLの中にちょっと埋め込むだけ<div id="current-display"> <span class="number">{{ model.gameDate.year }}</span><span class="unit">年</span> <span class="number">{{ model.gameDate.month }}</span><span class="unit">月</span> </div>で、ここに年を埋め込んでくれる、ここに月を埋め込んでくれる、と決めることができます。
これだと、HTML構造を柔軟に変えられますし、DOMとjQueryのコードをいちいち見比べながら修正する必要もない。HTMLとJavaScriptの分業がしやすいです。修正コストが明らかに下がったと感じています。リスト
配列を反復してDOM要素を生成するの、jQueryでもHTMLの要素をcloneすることによってできますね。
ただ、今回作るアプリでは、その場面が多すぎるんです。例えば。コマンドリスト。
手紙。
ちょっと発展的な例としては、会議室。(スレッドフロート型掲示板)
スレッドもレスも全部リストです。さらに発展的な例としては、この地図も一次元配列をバインディングすることで作っています。
都市ごとに座標を設定していますので、それをposition:absoluteとか使って配置しています。というように、このゲームにおいては配列から作る要素があまりにも多すぎる。jQueryでもゴリ押しで作れないことはないんですけど、Vue.jsだとかなり効率的に作れると判断しました。
具体的には、コマンドリストの場合、<div class="command-list"> <div v-for="command in list.commands" :key="command.commandNumber" :class="{ 'command-list-item': true, 'selected': command.isSelected, 'disabled': !command.canSelect }" @click="onCommandSelected(command, $event)"> <div class="number">{{ command.commandNumber }}</div> <div class="command-information"> <div class="command-helper"><span class="gamedate">{{ command.gameDate | gamedate }}</span><span class="realdate" v-if="command.commandNumber > 1">{{ command.date | realdate }}</span><span class="rest" v-if="command.commandNumber === 1">実行まであと<span class="rest-time">{{ list.secondsOfNextCommand }}</span>秒</span></div> <div class="command-text">{{ command.name }}</div> </div> </div> </div>みたいな感じ。
v-forを書くだけのお手軽バインディングです。
jQueryみたいに、DOM要素にいちいちIDを指定する、cloneしたものをどっかに置いておく、テンプレートのHTML構成が変わったときのコード修正などを気にする必要がないです。だって変数をそのままHTMLに埋め込んで終わりですから。このように、jQueryよりもVue.jsを選ぶメリットがあまりにも多すぎるので、短期間で手っ取り早く作るなら、jQueryと比べるならVue.jsのほうが格段にいいと判断しました。
実はVue.jsでの開発経験なかったです。会社でもなかったです。でもWPFやUWPやXamarin.Formsの経験がありますので、配列のバインディングはそれほど抵抗感じませんでした。
なぜAngularJSだとだめだったのか
何年か前挑戦して挫折したからですorz
いまだに苦手意識が‥‥アニメーション
jQueryにはアニメーション機能もありましたよね。Vue.jsにはこれ、ないんです。
ただ、CSS3のtransitionとkeyframeで大体事足りるので、結果としてjQueryなくてもいいんじゃないかなと。jQueryに拘ってる人は、よっぽと複雑なアニメーションでもしない限り、拘ってる理由がよくわからない。
たまに、CSSでできるレベルのアニメーションをjQueryで書いている人がいるんですけど、CSSとJavaScriptの分業(画面表示と振る舞いの分離)ができたほうが何かと便利かなーと思いました。ぴよ。Ajax
これもVue.jsにはないけどaxiosがry
Bootstrap
今回のUI制作では、Bootstrapも大きな役割を果たしています。
今回、jQueryは使っていないので、Bootstrap Nativeを使っています。CSSをCDNでつこている感じです。レスポンシブとスマホ対応
見てのとおりです。
スマホからでも快適に見れるよう意識したんですが、ここで落とし穴が2つ‥‥
まず、このページでは、あちこちに
.outer { overflow: auto; height: 50vh; }みたいなCSSを入れて、ページの一部分をスクロールさせているところがたくさんあるんですけど、そこ、そのままだとスマホでスムーズにスクロールできないんです。惰性がつかない。
そういった場合には、.outer { overflow: auto; height: 50vh; -webkit-overflow-scroll: touch; }をつけなければいけないんです。これちょっとはまりました。
もう1つですけど、iOSのSafariだけの問題かもしれませんが、ページの一部分だけをスクロールできるようにした場合、その部分をスクロールする時にちょっとしたコツが必要みたいな感じです。
いずれposition: stickyをつこて、一部分だけスクロールそのものをなくそうとは思っているんですが、いやーちょっと不便ですね。慣れが必要です。タッチデバイス考慮したUI
徴兵画面を例にとりますけど、今までの三国志NETで、徴兵画面のUIはこうなっています。
これ、PC使うんなら最悪このままでも問題はないかなーと思うんですけど、
スマホからだと、キーボード入力がめんどい。(あすかだけかもしれませんが)
とにかくキーボード入力がめんどい。
いやもうキーボード入力がめんどい。キーボード入力なしで大抵のことができたりしないかなーと思って考えたのがこれです。
兵種を選択して、
三国志NETって、徴兵には大体5パターンくらいありまして、
- ALL徴兵(徴兵できる数は武将によって変わるが、その武将が徴兵できるぶんだけを全部徴兵する)
- 末尾9徴兵(最大141人徴兵できる場合は139とかにする。徴兵で都市民忠が減少するが、その計算式を考慮した徴兵方法)
- 9人徴兵(民忠が全く減らない最大の数)
- 1人徴兵(多人数でいわゆる「守備ループ」を行うときに多用)
- ALLからちょっと控えめの徴兵(所持金が残り少ない時によくやる)
このうち最後はさすがにどうしようもないですが、それ以外の4つについては、ボタンをワンタッチで選べるようにしたみたいな感じです。
しかしスマホの文字入力やっぱり慣れない‥‥
がちのデザイナから見るとこれでもツッコミ所満載かもしれませんが、とにかくこういう工夫をしましたよと。
こういうUIも簡単に作れます。Bootstrapならね。ゲームシステムの工夫(特筆点のみ)
Infinite Scroll
ツイッターとかで、下までスクロールすると古いのが自動でロードされて、そのままスクロール続くあれです。
Infinite Scroll(無限スクロール)という名前があります。あれ、最近のUIのはやりなんでしょうか、PCからもスマホからもわりと便利です。
古いログを見るボタンを押さなくても、スクロールするだけでいい、お手軽です。最初はこんなだったのが
下へスクロールし続けていくとこうなったり。スクロールバーの長さが変わってますねー。
見た目難しそうですが、わりと簡単に実装できます。
Vue.jsですと、scrollイベントと連動させます。<div ... @scroll="onCharacterLogScrolled($event)">実装の方はこないなってます。
private onCharacterLogScrolled(event: any) { if (this.isScrolled(event)) { this.model.loadOldCharacterLogs(); } } private isScrolled(event: any): boolean { // スクロールの現在位置 + 親(.item-container)の高さ >= スクロール内のコンテンツの高さ return (event.target.scrollTop + 50 + event.target.offsetHeight) >= event.target.scrollHeight; }スクロールイベントと連動させて、モデルのメソッドを呼び出す感じです。
モデルのほうも、Ajaxでデータとってきて配列の末尾にpushするだけです。Vue.jsのバインディング便利。AI国家
AIといっても、さすがに最近はやりの機械学習や深層学習ではないです。手書きです。
三国志NETはもちろん人間が主体になってやるゲームですが、災害やイベントとかで発生するものにつけてみたかった、みたいなやつです。
具体的に言うと、農民反乱、異民族、蛮族。農民反乱と異民族は、あすかが中学生の時に運営していた旧KMYにもありましたが、
あれは、単に都市が無所属になるだけで終わりです。何の面白みもないかなーと思いました。
なので、今回はAIを作って乗せてみました。AIが自動で宣戦布告して、がおー☆しちゃう感じです。戦略シミュレーションゲームのAIって、30年以上戦略シミュレーションゲーム作ってる某企業の製品でもいろいろ批判されたりするあたり、
人間に勝てるものを作るのはプロでも難しいのかなーって印象持っています。三国志NETももちろん例外ではなく、コンピュータに人間の行動をある程度プログラミングしているものの、
状況に応じた柔軟な行動ができない、いつもワンパターンになってしまうのが難点です。
なので、今回はアルゴリズム以前に、アルゴリズムを組むための考え方を簡単に紹介するだけにとどめます。今回の戦略シミュレーションAIは、処理の流れが大きく分けて2つあります。
- 国全体の思考
- 個人の思考
この2つに分けて説明してみます。
国全体の思考
その国が、全体として何を目標にしてどの行動をとるべきかを考えます。
個人の状態にととまらない、できる限り広い視点で考えます。
といっても現在は、(あすかの技術力不足や開発時間不足もあって)これしか決めていないのが現状です。
- 自国のメイン都市
- 自国の前線都市
- 相手国の目標都市
- 相手国の次に攻める都市
HoI4のアレを参考にAI作ってみました的なやつです。
戦争の時、まず相手国で一番大きいというか、栄えていると思われる都市を目標に設定します。(もちろんこの都市を奪っただけで戦争が終わるとは限らないので、その時はまた別の目標を設定します)
そして、その目標都市が自国と隣接しているとは限らないので、経路探索して隣接していない場合は、別の都市を「次に攻める都市」に指定します。
そして、そこに隣接する自国の都市(正確に言うと、メイン都市から目標都市までの最短経路上に乗る都市)を、自国の前線都市に指定します。自国の武将は、前線都市に集まって、次に攻める都市を攻めていく形になります。
これ、欠点としては、三国志NETってゲリラ戦もよく発生するんですよね、あとは「裏抜き」といって、相手国の目標都市以外の城壁を削って、相手の逃げ道を塞ぎやすくする作戦。それに対応しぎれてないかなーみたいなとこがあります。(裏抜きは旧KMYでは聞いたこと無いので、もしかしたら翼だけの用語かもしれない)
また、堀といって、あえて人口の極端に少ない都市を作る作戦ありますよね、それもやっていません。それらは三国志NETのプレイヤーの間ではもう手法として確立されていて、AIとして実装するのは時間の問題やろみたいなところがあるので、今後の課題です。これは国全体の行動に結びつくので、目標都市が頻繁に変わるようなことがあってはいけません。
作戦をDBに保存しといて、不定期的に見直すことで、なるべく行動に一貫性をもたせ、かつ最新の戦局に対応できるようにしています。個人の思考
国全体の思考の結果を踏まえまして、今度は個人レベルでコマンドを決めます。
国全体の目標をまず見ますが、個人って、所持金とか兵士数とか管理しなければいけなものがいろいろある。
自分は今、指定された都市に侵攻できる状況になっているのか。兵士が足りなければどこで徴兵するか、そもそも戦争中でなければどのコマンドを実行するのか。国全体の方針は結構、それにあわせて自分はどのように動くべきか。それを決めて、具体的な行動に落とし込みます。
まとめますと、まず「全体」で物事を考えて、次第に考える範囲を狭めていく感じのロジックになっています。
地図の自動生成
三国志NETって、どこかの国が統一した時にリセットということが行われます。すべての武将、国、ログなどが初期化され、また一からやり直しになります。リセットしてから統一するまでのサイクルを、「期」と呼んでいます。第1期、第2期、‥‥のように言います。
で、リセットしても、地図上の国は消えますけど、地図の形は同じです。毎回同じなんです。なので、この都市は守りやすいとかで、建国される都市が毎回偏ってしまうわけです。建国位置がワンパターンになることは、外交関係や、毎回の戦争の作戦などもある程度規則に縛られてしまう。
もちろん建国位置は毎回微妙に変わってきますので、この状態でもリプレイアブルかといえばそうなんですけど、やっぱり地形も毎回変えたい。
でも、地形を毎回自分で考えるのも大変。ということで、機械に任せることにしました。Civとかでもある地図の自動生成です。今回のプログラムでは、「とりあえずランダムに都市を配置しておいて、全体ができた後から、この地図でいけるかどうか判定する。だめだったら最初から作り直し」みたいなやり方にしています。(あとあとのアプデで変わることもあります)
三国志NETに必要な地図
三国志NETの経験のある方はお分かりだと思いますけど、例えば
こういう都市配置は、攻勢にとっては嫌われる。なぜなら、守る方は中央の都市に全員集まってひたすら守ったり攻撃できたりするのに対し、中央の都市を攻める方はこの都市を攻めるだけでなく、カウンター(自国の都市が奪われた時に取り返すこと)も意識しなければいけない。
守るほうが有利で、攻めるほうが不利。この地形になっただけで、戦争の勝率が最初からはっきりしてしまっている感じ。地図生成でこのような都市ができないようにするため、このルールを一般化しまして、
- 隣接都市グループは1つのみとする
とおきます。
ここで、「ある都市に隣接する都市集合において、互いに隣接している都市集合を1グループ」と定義します。
つまり、上の場合、中央の都市には、橙色と緑色の2つの隣接都市グループが存在します。2つのグループがあるわけですので、これはだめ、ということになります。
このルールに従うと、こういう、都市が2つ以上直線につながった地形もできなくなりますよね。
ただ、これはこれでいいんですけど、例えばこういう地形でやりたくなることもたまにあるんですよねー。個人的な好みですけど。
この場合、中央から見て上・右・左・下が守りやすい都市になってしまいますし、上の条件も満足しないけれど、なんか見てて面白そうだなーと思ったので例外的にこれはOKにすることにしました。
これを一般化すると、
- すべての隣接都市グループが、共通の隣接都市を持つ
となりました。
赤い都市の隣接都市グループは、橙、緑の2つありますね。この2つのグループが、共通の隣接都市である黄を持ちます。この場合は例外的にOK、というプログラムを組みました。
このほかにも、
- 都市が6以上9以下の場合、自分を除いた存在するすべての都市に隣接する都市は存在しない
- すべての都市において、隣接都市が8未満である
といった条件を設けています。
やっぱり三国志NETって都市配置が重要ですよねー。ぴよ。こんな感じで作った地図がこちら。
都市少ないほど条件が相対的に厳しくなるので仕方ないっちゃ仕方ないんですけど、パターンが限られてますね。他にもいくつかパターンはありますけど。あとはこれを回転したり反転したりする程度。
都市名
都市名は悩みました。機械学習の導入も検討しましたが、最後にはもう適当でいいやってことにしました。
適当なので、これをそのまま、生成した地図にかぶせます。
地図は生成した後中央に寄せられるので、雒陽(洛陽。後漢の光帝が五行における水の気を嫌い雒陽と改称したが、魏の時代に戻された)周辺は常に出てくるかんじになりますー。
これも後から改良するかもしれないししないかもしれない。ぴよ。三国志の地図って正方形ではなく、雒陽の左上あたりがぽっかりあいてるんですよねー。無理矢理正方形におさめました。はい。思いっきり歪んでるけどな。
朝歌で商復興プレイしたいのう‥‥配置(継続的インテグレーション)
これって、アプリケーションサーバなんですよね。CGIは作ったらもうFTPにぽいで終わりですけど、今回はASP.NET Coreで作ってますから
- アプリをビルド
- 現在稼働中のサーバを停止
- アプリをサーバ(EC2)に配置
- サーバを起動
みたいなのをいちいち踏まなければいけない。これ、毎回手でやっていたら死にます。まぢです。
特にあすか、WebアプリとしてはCGI時代の三国志NETくらいしか運営経験ないんですよねー。毎回いちいちめんどいデプロイ処理を手動でやっていたら、いつか飽きるかなーと思いました。
そら企業ではなく個人の趣味としてやってるわけですから、誰かに投げられるわけでもないですし。そこで、今回は継続的インテグレーション(CI)をつこてみました。
AWS CodePipeline
サーバサイドではこれを使いました。
詳しくはASP.NET CoreアプリをGitHubにpushしたら自動でAWSにデプロイされるようにする(EC2、CodeBuild|Deploy|Pipeline)に書いてありますけれども、この他にも
- DBサーバのポート番号変更
- DBサーバをSSL化して外部と接続
- Let's Encrypt自動更新対応
- Apacheとリバースプロキシ
- ApacheをSSL化
などなどの設定をしています。
いやー便利。クライアントにも、あらかじめ「ストリーミング接続が切断されたら、自動で再接続を試みる」処理を入れていたおかげで、
アプデの時に鯖落ちしても、自動でつながるようになりました。
ユーザに事前にいちいちページのリロード促さなくてもよくなるのと、その促す書き込みすらユーザは気づかないかもしれないのでー、リアルタイムで更新されるタイプのシステムでは必須の処理かもしれません。Netlify
クライアントサイドではこれ使いました。まぢ一発で便利。お手軽。
保守
実際にあった事例
MacのEFCoreでDBにawaitで接続するとなんか遅い
実環境はAWS EC2のUbuntuですが、開発環境はmacです。
開発しているわけなんですが、どうにもasyncとawaitをつこたDB接続が遅い。DBへの接続、開発当初は同期接続で、後から非同期接続にしたんですが、
非同期接続にしてから、とにかく遅い。単純なCRUD程度の簡単な処理に最低でも1秒待たされる。
ブラウザの複数タブを開いて同時に接続しようとしたら、その分だけ(3〜5秒、ひどい時は10秒)待たされる。Windowsや実環境ではなぜか高速に動くんですよね。
あれ何ででしょう。設定全く同じなのに‥‥。
Ubuntuでは、アプリもDBもサービスとして動いているんですが、関係あるのかな‥‥。むむ。実環境に配置するまでわからないという知見でした。
リアルタイム更新ができていないバグを知識のない人に説明するのは意外と大変だった
運営している折、
援軍機能というのがあって、君主と軍師の2人は、特定の武将に援軍要請を送ることができます。
援軍要請した武将のところには、「援軍要請撤回」というボタンが現れて、それでこの武将にはすでに援軍を要請済だってのがわかるようになっています。で、君主が援軍要請をしたら、君主と軍師の画面で同時に援軍要請撤回ボタンが現れる、っていうのが本来の仕様なんですけど、
君主が援軍要請しても、軍師の画面にボタンが現れない。
それで軍師が、この武将にはまだ援軍要請していないと勘違いして、要請ボタンを押したらエラーが出てきたという。この現象の原因そのものではなく、どうしてこういう表示になってしまうかの理屈を、リアルタイム更新に慣れているみなさんに説明するのが意外と大変でした。
あすかの観測範囲だとリアルタイム更新なブラウザゲームなんてあまり見かけないし、あっても大抵は企業が運営するゲームで、隅から隅までしっかりテストされているからでしょうか。
みなさんにとっては、これはもうめったに見かけないバグだったのかもしれないです。async、awaitの書き忘れによるデータベースエラー
ある日、武将更新処理の途中に、こういうエラーが出て更新が止まりました。
わかりやすいように改行しています。2019-04-08 03:22:14.2514||ERROR|SangokuKmy.Startup|更新処理中にエラーが発生しま>した System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext, however instance members are not guaranteed to be thread safe. This could also be caused by a nested query being evaluated on the client, if this is the case rewrite the query avoiding nested invocations.DBサーバを再起動したら更新また動くようになったのですが、またしばらくしたらこのエラーが起こる‥‥1日に1回位の頻度で起こる。
というわけで、最初はDBが原因だと思ってしまったんですが、やっぱ上のエラーメッセージに書いてあるとおり、プログラムが原因でした。こうゆう非同期エラー系は、どこが原因でこうなったのか具体的に書いていない場合が多いんです。
今回のエラーもその一例でして、このエラーが最初に出た時刻は大体分かっていましたから、その直近のコミットを調べてみましたところ‥‥
こんなプログラムがありました。(Someメソッドは自作したOptional的なやつです)(await repo.Town.GetByIdAsync(character.TownId)).Some(async (town) => { // 自国の都市であれば、同じ国の武将に、共有情報を通知する(それ以外の都市は諜報経由で) if (town.CountryId == character.CountryId) { await StatusStreaming.Default.SendCountryAsync(ApiData.From(town), character.CountryId); } // 同じ都市にいる他国の武将にも通知 // (自分が他国の都市にいる場合は、都市データ受信のさいは、自分もここに含まれる) var charas = (await repo.Town.GetCharactersAsync(town.Id)) .Where(c => c.CountryId != town.CountryId); await StatusStreaming.Default.SendCharacterAsync(ApiData.From(town), charas.Select(c => c.Id)); }); await repo.SaveChangesAsync(); // なんとかの処理 repo.Dispose();なるほどこれは。
asyncのついたラムダ式を待機してなかったせいで、repo.SaveChangesAsync()とrepo.Town.GetCharactersAsyncが同時に実行される可能性がありますね。それであんなエラーが出たわけです。これね、対応は探すのに時間がかかった割には単純で、11文字追加するだけでした。
// 修正前 (await repo.Town.GetByIdAsync(character.TownId)).Some(async (town) => // 修正後 await (await repo.Town.GetByIdAsync(character.TownId)).SomeAsync(async (town) =>非同期処理って、なかなか的確な位置を示してくれないので対応に困ることも多いんですよねー。
今回は、このエラーの初めて出た時期が把握できていたのでgitのコミット履歴見て、思ったよりもわりとあっさり解決できましたが、これからまたこういう問題が出たらと思うと心配‥‥。なのだ。EFのAddRangeAsyncでデータが順番に追加されるとは限らない?(現在進行中)
戦闘って、最大50ターンの間、こういうログが続けて出るわけです。
この順番がおかしい、ばらばらになることがある、との報告をわりと頻繁にいただきます。
調べてみたんですが、どうにもDBの並びからしておかしい!
と思っていろいろやってみたんですが、await this.Context.CharacterLogs.AddRangeAsync(logs);この
AddRangeAsyncメソッドは、与えられたデータを順番通りにDBに追加することを保証しない疑惑が出てまいりました。ドキュメント調べたんですが、いまだにそのような記述は発見できず‥‥
でも、今はとりあえず、そういう前提で対策考えることにしました。ログには時刻も記録されてるので、時刻でソートできないかなーと思っているとこであります。(まだ対応完了してない)
時刻でだめだったら、ログにOrderカラムをつけるしかないのかなあ。まとめ
技術の発達により、10年前は作れれなかったものが、今は誰でも手軽に作れるようになってるなーと感じます。
みなさんもこの機会に、昔やっていたゲームとかを今の技術で組み直すとかやってみては。なの。
- 投稿日:2019-04-13T18:07:42+09:00
Vue.js+axiosでNetlifyのFormsを使ってハマった話
Netlifyとは
ホスティングサービスです。Firebaseなども流行っていますが、
今回はwebホスティングサービスが目的だったためNetlifyにのせました。
Githubと連携してRepositoryをそのままdeployできるのが魅力的です。
https://www.netlify.com/NetlifyのForms
フォームのバック側の処理をしてくれます。
なので側だけ作れば簡単にフォーム機能がつかえるものです。
Slackやメールに通知するのも簡単なのでとても便利です。
https://www.netlify.com/docs/form-handling/Formsの機能の使い方
最初に読ませていただいたのは、こちら。
Forms機能を利用するのは非常に簡単で、
タグ内にnetlifyと記述するだけです。<form name="contact" method="POST" netlify> <!-- 省略 --> </form>そうホントにこれだけで、フォーム機能がつかえます。楽ちんです!
がしかし、
Formsの機能を使ったページだということをNetlifyに認識してもらう必要があってそこでハマりました。
Formsの機能をを使ったページだとNetlifyに認識してもらう必要がある
参考にさせていただいたのはこちら、そしてこちら。
vue-routerなどで実装するSPAでは、静的にこちらをおいてあげなければNetlifyに認識されないということでした。なるほど。。
つまり、
public/index.htmlなどに、index.html<!-- A little help for the Netlify post-processing bots --> <form name="contact" netlify netlify-honeypot="bot-field" hidden> <input type="text" name="name" /> <input type="email" name="email" /> <textarea name="message"></textarea> </form>をおいてあげれば解決できました〜!??? ひえぇ〜
hidden要素でおけとNetlifyのblogにガイドがちゃんとかいてありました〜TT
https://www.netlify.com/blog/2018/09/07/how-to-integrate-netlify-forms-in-a-vue-app/そのほか、フォームのデータをpostするときは
form-nameというものを一緒に送ってあげる必要があるのでそこも注意です。
https://www.netlify.com/docs/form-handling/#ajax-form-submissions【まとめ】Vue.js+axiosでNetlifyのFormsを使うのに必要なこと
長く書きましたので、つまりまとめると以下2点。
public/index.htmlなど静的にhidden要素でNetlifyに認識してもらうためのタグをおく。- データをpostするときに
form-nameも一緒に送ってあげる。以上!
参考:
https://qiita.com/NaokiIshimura/items/bce2f0b865ec1bc16a53
https://qiita.com/nanaki14/items/007eae905d6305f75f6a
https://qiita.com/tatane616/items/f646e84fe4cdd9eac0de
https://www.netlify.com/blog/2018/09/07/how-to-integrate-netlify-forms-in-a-vue-app/
https://www.netlify.com/docs/form-handling/#ajax-form-submissions
https://www.netlify.com/docs/form-handling/私の拙いコードで作ったポートフォリオ:
https://mainichi-chocolate.tokyo
現在WIPですよ^0^
- 投稿日:2019-04-13T17:40:36+09:00
DataがDateになっていませんか
typo
Dataexport default { async asyncData() {Dateeeeeeeeeeexport default { async asyncDate() {commons.app.js:10006 [Vue warn]: Property or method "facts" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties. found in ---> <Anonymous> <Nuxt> <Layouts/default.vue> at layouts/default.vue <Root>
- 投稿日:2019-04-13T16:55:53+09:00
Vue.jsをRails環境下で動かすための環境構築手順
参考サイト
https://qiita.com/jnchito/items/30ab14ebf29b945559f6
https://qiita.com/cohki0305/items/582c0f5ed0750e60c951
説明が丁寧でわかりやすい。開発環境
ruby 2.6.2
Rails 5.2.3ゴール
ローカル環境下でRuby on RailsにVue.jsを動かす環境を構築する。
手順
通常のプロジェクトを作成
※DBMSにはpostgesqlを指定$ rails new vue_app -d postgresqlGemfilegem 'webpacker', github: 'rails/webpacker'gemを入れたら、必ずbundle install
webpackerに関する参考サイト
https://blog.tai2.net/webpacker3.html
https://qiita.com/chimame/items/8d3d6f4afea675cffa7d$ yarn -v上のコマンドでもしyarnのインストールをしていなかったら、インストールをする。
$ rails webpacker:installここでwebpackerをインストール
このコマンドだけで、Vue.jsをインストールできる!
$ rails webpacker:install:vue
hello_vue.jsを使い、Rails上で動作させるための準備
$ rails g controller homes index適当に、コントローラー・アクション・ビューをコマンドで生成
app/views/layouts/application.html.erb<%= yield %> <%= javascript_pack_tag 'hello_vue' %> </body>このタグを挿入する。
当たり前だが、CDNのインポートは必要ない↓このタグの挿入は必要ない!!!!!!!!!!! <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>app/views/homes/index.html.erb<div id="app"> </div>app/javascript/packs/hello_vue.jsimport Vue from 'vue/dist/vue.esm' const vm = new Vue({ })※app/javascript/app_vueのコード、ファイルは削除しても構わない。
これで、Rails上でVue.jsを動かす準備は完了。
- 投稿日:2019-04-13T16:20:38+09:00
Vue.jsってどうやって動いてるんだろう? - Vue.jsを読み込むロジック
最近、Vue.jsを利用する機会が増えてきました。概念もなんとなく理解してきましたが、「じゃあ実際どうやって動いているの?」と聞かれるとわからないですよね。
ということで、Vue.jsのファイルを読み解いてみようと思います。
今回は、CDNで提供されている、下記のソースを使います。
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>即時関数だ!
今回は全体の構造を眺めていきましょう。
スクリプトの出だしはこのようになっています。vue.js(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Vue = factory()); }(this, function () { 'use strict'; // ...(省略) }))うお、長え!と思うかもしれませんが、単純化するとこうなっています。
単純化してみた(function (global, factory){ a===b && c!==d ? E : f===g && h ? I : (j=k || l, m=o) }(this, function () { // ...(省略) }))(function(a, b){ // 関数の中身 })(c, d)という形は即時関数の形式です。
http://analogic.jp/immediate-function/vue.jsが読み込まれた際に
function(a, b)が実行されます。そして、引数a,bにはc,dがそれぞれ代入されます。今回の場合、
globalにthis、そしてfactoryに1万行を超えるvue.jsの関数がそれぞれ代入されるわけですね!ちなみに、
thisにはブラウザで実行した場合、Windowオブジェクトが入ります。
条件分岐がたくさん
さて、では
global,factoryはそれぞれどのような処理がされるのでしょうか?それが、次の三項演算子パラダイスの即時関数の中身です。
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Vue = factory());ここで、三項演算子を簡単におさらいしましょう。
(true or false) ? 'trueだった場合の処理' : 'falseだった場合の処理'これに基づいて、分類をしてみます。
1 typeof exports === 'object' && typeof module !== 'undefined' 2 ? module.exports = factory() 3 : typeof define === 'function' && define.amd 4 ? define(factory) 5 : (global = global || self, global.Vue = factory());1行目は丸々条件式です。
trueになる条件は「exportsがオブジェクト型で、かつmoduleが存在する」です。
trueだった場合、module.exports にfactory()がセットされます。
つまり、Node上だったらモジュールとして機能する、ということですね!
falseだった場合、さらに条件分岐が続きます。3行目がtrueになる条件は「
defineが関数で、かつdefine.amdが存在するとき」です。
trueだった場合、defineにfactoryがぶち込まれます。
これは、AMD(Asynchronous Module Definition)を使う際の処理です。
falseだった場合、
globalにはglobal, もしくはselfが代入されます。この論理演算子
||についてはこちらを参照のこと。
https://qiita.com/Imamotty/items/bc659569239379dded55
globalはthis, つまりwindowオブジェクトのことでしたね。
そして最後に、そのglobalのVueにfactory()を突っ込んでいる、というわけです。つまり、ここでの処理は、AMDかNodeJSかそれ以外(ブラウザ)かによって、
factory()(Vueの本体)を読み込ませる処理の方法を分岐させていたんですね!ちなみに、
global,factoryを引数にもつ即時関数を用意し、内部でそれぞれの環境に応じて処理を分岐させる手法は、 UMD (Universal Module Definition) と呼ばれています!ここら辺のリンクに詳細があります。
https://www.davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/
https://qiita.com/chuck0523/items/1868a4c04ab4d8cdfb23
今回はここまでにします。
メソッド単位でちょこちょこ記事が書けたらいいな、と思っています!
- 投稿日:2019-04-13T14:05:52+09:00
【Nuxt.js】お問い合わせフォーム実装にWordPressのContact Form 7で対応する
今回やること
Nuxt.jsで作成した静的サイトにお問い合わせフォームを実装します。
フォームに入力されたデータをaxiosでWP REST API経由でWordPressのContact Form 7に送り、Contact Form 7からメールを送信します。なお、今回フロントはNuxtで実装していきますが、Vue-cliでも、Reactでも、ES2018などをBabelで使う場合などでも、だいたい同じ方法で実装できるかと思います。
WordPress側の設定
WordPressのインストールはここでは省略させていただきます。
基本設定 & 必要プラグインのインストール
1. Contact Form 7
今回主役のお問い合わせフォームのプラグイン。WordPressお問い合わせフォームプラグインの定番中の定番!
2. Application Passwords
WP REST APIでアクセスする際の認証に必要なアプリケーションパスワードを生成します。
アプリケーションパスワードの設定
Application Passwordsのプラグインを入れると、ユーザーの設定ページにApplication Passwordsの設定項目が追加されます。
任意の認証用ユーザー名を入力して [Add New]をクリック
一度だけパスワードが表示されるので、忘れずにコピーして一旦どこかに保存しておきましょう。Contact Form 7 の設定
ここで、フォームの項目、自動返信メールの設定、送信後のメッセージなどを設定します。
基本的には、普通にWordPressでContact Form 7を設定するのと同じです。
ただし、メールタグの名前は、後々JavaScriptで使うことを考えると、ケバブケースでなくキャメルケースにしておくと良いです。WP REST API にアクセスして確認
さて、ここまできたらREST APIでメールが送信できることを確認してみましょう。
HTTPクライアントにはPaw( https://paw.cloud/ )を使います。
https://{ドメイン名}/wp-json/contct-form-7/v1/contct-forms/{フォームID}/feedback/BodyにはForm URL-Encodedに設定して、フォームの内容を入力していきます。
AuthタブからBasic Authを選択し、先程設定したApplication Passwordsのユーザー名とパスワードをここに入力します。リクエストを送信して、無事にメールが届けばOKです。
フロントエンドの設定
今回のプロジェクトファイルはGitHubで公開しています。
https://github.com/hiropy0123/nuxt-contct-formNuxtのセットアップ
create-nuxt-appでプロジェクトを作成します。
https://github.com/nuxt/create-nuxt-app$ yarn create nuxt-app nuxt-contact-formこのように設定してみました。 ? Project name nuxt-contct-form ? Project description My extraordinary Nuxt.js project ? Use a custom server framework -> none ? Choose features to install -> Linter / Formatter, Prettier, Axios ? Use a custom UI framework -> bulma ? Use a custom test framework -> none ? Choose rendering mode -> Universal ? Author name -> '名前を入れる' ? Choose a package manager -> yarn追加でいくつかパッケージを追加していきます
便利な機能なので追加!
# Sass loader $ yarn add -D node-sass sass-loader # Pug loader $ yarn add -D pug pug-plain-loader # @nuxtjs/dotenv $ yarn add @nuxtjs/dotenv # lodash $ yarn add lodashお問い合わせページの作成
pagesディレクトリにお問い合わせページを作成します。
フォーム部分はコンポーネントとして、別ファイルにします。
また、今回は簡単なバリデーション機能と、確認画面(モーダル表示)と送信完了画面(モーダル)を作成します。
モーダル部分も別コンポーネントで作成します。pages/contact.vue<template lang="pug"> section.l-section .l-container contact-form(ref="form", @open-modal="openModal()") transition(name="modal", v-cloak) form-modal( v-if="isModal", @close-modal="closeModal()", @clear-data="clear()", @submit="submit()", :data-response="responseData" ) </template> <script> import ContactForm from '@/components/ContactForm' import FormModal from '@/components/FormModal' export default { components: { ContactForm, FormModal }, data() { return { isModal: false, responseData: null } }, methods: { openModal() { this.isModal = true }, closeModal() { this.isModal = false }, clear() { this.$refs.form.reset() }, submit() { const formData = this.convertJsontoUrlencoded(this.$store.state.formData) const USER = process.env.USER const PASSWORD = process.env.APPLICATION_PASSWORD // Base64に変換 const TOKEN = window.btoa(`${USER}:${PASSWORD}`) const axiosConfig = { headers: { 'Authorization': `Basic ${TOKEN}`, 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' } } this.$axios .post('/contct-form-7/v1/contct-forms/5/feedback/', formData) .then(response => { console.log(response) }) .catch(error => { console.log(error) }) }, // Conver to JSON Object to application/x-www-form-urlencoded convertJsontoUrlencoded(obj) { let str = []; for (let key in obj) { if (obj.hasOwnProperty(key)) { str.push(encodeURIComponent(key) + "=" + encodeURIComponent(obj[key])) } } return str.join("&"); } } } </script>components/ContactForm.vue<template lang="pug"> div.contact-form .form-content label.required 名前 input( v-model="formData.yourName" type="text" class="form-control" name="yourName" required autocomplete="name" @blur="touched.yourName = true" ) .input-hint span.error(v-show="touched.yourName && !formData.yourName") 必須項目です .form-content label.required メールアドレス input( v-model="formData.yourEmail" type="text" class="form-control" name="yourEmail" required autocomplete="email" @blur="touched.yourEmail = true" ) .input-hint span.error(v-show="touched.yourEmail && !formData.yourEmail") 必須項目です。 span.error(v-show="touched.yourEmail && !validEmail(formData.yourEmail)") メールアドレスを正しく入力してください。 .form-content label 題名 input( v-model="formData.subject" type="text" class="form-control" name="subject" ) .form-content label お問い合わせ内容 textarea( v-model="formData.message" class="form-control" name="message" rows="8" ) button(:disabled="hasError", @click="confirm") 確認画面へ </template> <script> import _ from 'lodash' const initialState = () => { return { formData: { yourName: '', yourEmail: '', subject: '', message: '' }, touched: { yourName: false, yourEmail: false }, valid: { yourName: false, yourEmail: false }, isModal: false } } export default { data() { return initialState() }, computed: { hasError() { return !this.validateForm() } }, methods: { // バリデーション validEmail(email) { const RegExp = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/ return RegExp.test(email) }, validateForm() { // 入力をトライしたかどうか const array = _.map(this.touched, item => { return item }) const allTouched = array.every(value => { return value === true }) if (this.formData.yourName) { this.valid.yourName = true } else { this.valid.yourName = false } if (this.formData.yourEmail) { this.valid.yourEmail = true } else { this.valid.yourEmail = false } // 入力されているかどうか const array2 = _.map(this.valid, item => { return item }) const allValid = array2.every(value => { return value === true }) return allTouched && allValid }, // Vuexに保存 storeForm() { const form = this.formData this.$store.commit('setFormData', form) }, // 確認ページ confirm() { this.storeForm() this.open() }, // モーダルを開く open() { this.$emit('open-modal') }, // モーダルを閉じる close() { this.$emit('close-modal') }, // dataを初期値に戻す reset() { Object.assign(this.$data, initialState()) } } } </script>確認画面と送信完了後のメッセージをモーダルで作成
FormModal.vue<template lang="pug"> .contact-confirm .overlay slot .modal h2(:class="myStatus") {{ dataResponse ? dataResponse.message : '入力内容をご確認ください。' }} .confirm(v-if="!dataResponse") dl dt 名前 dd {{ getFormData.yourName }} dl dt メールアドレス dd {{ getFormData.yourEmail }} dl dt 題名 dd {{ getFormData.subject }} dl dt お問い合わせ内容 dd(v-html="getFormData.message") .btn-submit(@click="send") 送信 .btn-return(@click="close") 修正する .response(v-if="dataResponse") .response-body(v-html="dataResponse.body") .btn-return(@click="close") 閉じる </template> <script> import { mapGetters } from 'vuex' export default { props: { dataResponse: { type: Object, required: false, default: null } }, computed: { myStatus() { if (this.dataResponse !== null) { return this.dataResponse.status } else { return '' } }, ...mapGetters(['getFormData']) }, methods: { close() { this.$store.dispatch('resetForm') this.$emit('close-modal') }, clear() { this.$emit('clear-data') }, send() { this.clear() this.$emit('submit') } } } </script>重要なポイント!
REST APIからフォームを送信するときの重要なポイントは2つあります。
1. フォームで送信するデータをJSON形式から、X-WWW-FROM-URLENCODEDの形式に変換する
2. headerにAuthorizationの情報を含めるaxiosの設定
先ほど、取得したApplication PasswordsとWordPressのAPIのURLをenvファイルに登録します。
.envWP_REST_API_BASE_URL=https://ドメイン名/wp-json WPUSER=ユーザー名 APPLICATION_PASSWORD="パスワード"この
.envファイルはプロジェクトの一番上のディレクトリに置いて、.gitignoreに登録して公開されないようにします。nuxt.config.jsrequire('dotenv').config() // 一番上にenvファイルを読み込む設定を追加 export default { // 諸々省略 // ~ modules: [ '@nuxtjs/axios', '@nuxtjs/dotenv' ], // 環境変数の読み込み env: { WP_REST_API_BASE_URL: process.env.WP_REST_API_BASE_URL, WPUSER: process.env.WPUSER, APPLICATION_PASSWORD: process.env.APPLICATION_PASSWORD }, /* ** Axios module configuration */ axios: { baseURL: process.env.WP_REST_API_BASE_URL // baseURLの設定 }, // ~ }フォームのデータを
application/jsonからapplication/x-www-form-urlencodedに変換するconvertJsontoUrlencoded(obj) { let str = []; for (let key in obj) { if (obj.hasOwnProperty(key)) { str.push(encodeURIComponent(key) + "=" + encodeURIComponent(obj[key])) } } return str.join("&"); }Authorizationの設定
submit() { const formData = this.convertJsontoUrlencoded(this.$store.state.formData) const USER = process.env.WPUSER const PASSWORD = process.env.APPLICATION_PASSWORD // Base64に変換 const TOKEN = window.btoa(`${USER}:${PASSWORD}`) const axiosConfig = { headers: { 'Authorization': `Basic ${TOKEN}`, 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' } } this.$axios .post('/contact-form-7/v1/contact-forms/5/feedback/', formData, axiosConfig) .then(response => { console.log(response) this.responseData = response.data }) .catch(error => { console.log(error) }) }submitメソッドの中で、
WPUSERとAPPLICATION_PASSWORDを呼び出して、TOKENを作成しています。
window.btoa()は文字列をBase64にエンコードしてくれます。送信確認
実際に送受信の確認をしてみます。
正常に送信されると、Contact Form 7で設定した送信完了のメッセージが、表示されます。
送信されるメール本文の内容やメールの送信先は、Contact Form 7で設定できます。まとめ
WordPress Contact Form 7 は利用している方も多いと思うのですが、フロントエンドをNuxtにすることで、フォーム入力の表現や確認画面の表現が自由にできるのがいいですね!



































