- 投稿日:2020-12-07T21:46:59+09:00
【Vue.js】基本的なディレクティブまとめ
はじめに
Vue.js
で使用する基本的なディレクティブをまとめました。
Vue.js v3.0の公式ドキュメントを参考としています。V-onやV-bindは様々な使用法があるため、詳細は今後の記事で書きます。
V-text
<span v-text="message"></span> <!-- 両者同様 --> <span>{{ message }}</span> Hello Vue!以下のように文章の一部をプロパティで表示したい場合は
”Mustache” 構文(二重中括弧)を利用
することが推奨されています。<span>Message: {{ message }}</span>const app = new Vue({ el: '#app', data: { message: 'Hello Vue!', }, });V-html
DOM 要素の内側を innerHTML として書き換えます。
v-html には XSS の危険性があるため信頼できるコンテンツだけに利用します。<div v-html="html"></div>var app = new Vue({ el: '#app', data: { html: 'Hello <strong style="color: red">Vue!</strong>', }, });V-show
参照する値が true として評価され場合は表示し、false として評価される場合は display:none 等のスタイルが付いて非表示になります。
<h1 v-show="hello">Hello!</h1>const greeting = new Vue({ el: '#app', data: { hello: 1, }, }); greeting.hello = 1; // Hello!V-if
バインドした値が true 評価であれば DOM 要素が生成され、false であれば破棄されます。
<h1 v-show="hello">Hello!</h1>const greeting = new Vue({ el: '#app', data: { hello: 1, }, }); greeting.hello = 1;
v-if
とv-show
の違いv-if は初期表示において
false
の場合、何もしません。条件付きブロックは、条件が最初にtrue
になるまで描画されません。一方、
v-show
ではは要素は初期条件に関わらず常に描画され、シンプルなCSSベースの切り替え
として常に要素が DOM に保存されます。一般的に、
v-if
はより高い切り替えコストを持っているのに対して、v-show
はより高い初期描画コストを持っています。 そのため、切り替えの頻度が低ければv-if
切り替えの頻度が高ければv-show
という使い分けをします。V-else
v-if
の後続分岐処理としてv-else
を使用できます。<div v-if="Math.random() > 0.5">Hello!</div> <div v-else>Morning!</div>V-else-if
<div v-if="type === 'A'">A</div> <div v-else-if="type === 'B'">B</div> <div v-else-if="type === 'C'">C</div> <div v-else>Not A/B/C</div>V-for
このように v-for を用いて numbers の各要素を仮変数(エイリアス)num として取り出して、num を li 要素の内部に入れて表示させることができます。
<div id="app"> <ul> <li v-for="num in numbers">{{ num }}</li> </ul> </div>new Vue({ el: "#app", data: { numbers: [ 1, 2, 3, 4, 5 ] });V-on
DOM要素にイベントリスナーを登録できます。
<div id="app" v-on="click: alert">ClickHere!</div>new Vue({ el: "#app", methods: { alert: function(){ alert('clicked!');} });V-bind
aタグのhref属性やimgタグのsrc属性などを動的に変更することができます。
<img v-bind:src="imageSrc" /> <!-- 省略記法 --> <img :src="imageSrc" />new Vue({ el: "#app", data: { imageSrc: "http://example/example"V-model
HTMLのinput要素やselect要素などのユーザーが入力した値を受け取りたい場合、v-bindディレクティブを用いることで実現しますが、
v-model
を使うことでより簡潔に書くことができます。v-bindとv-onを使った場合<div id="app"> 名前をここに入力する <input v-bind:value="name" v-on:change="name = $event.target.value" /> </div>v-modelを使った場合<div id="app"> 名前をここに入力する <input v-model:value="name"/> </div>new Vue({ el: "#app", data: { name: '' } })参考
- 投稿日:2020-12-07T09:48:34+09:00
【Vue3】Composition APIを使ったVuexの代替
はじめに
Vue3から使えるようになるComposition APIでは、リアクティブなデータの管理はVueコンポーネントから解放されて、それぞれのロジックをパーツとして分解しComposition(合成)できるようになりました。
これは、グローバルなリアクティブな状態の管理ができるようになったことを意味します。ともすれば、今まで上位管理の手法としてメジャーな存在であったVuexにCompsition APIはとって変わる存在になりえるのではないのでしょうか?
Compositon APIを利用した、小さなストアの管理を見ていきます。ストアの作成
はじめにストアを作成します。
reactive
メソッドを利用して、リアクティブな状態を作成しました。ここでは、Vuexの作法に従ってstate
は直接更新できないようにreadonly
にしています。
state
の値を更新するためには、increment()
かdecrement()
メソッドを利用します。src/store/index.tsimport { InjectionKey, reactive, readonly } from 'vue' import { Store } from '@/types/store' const state = reactive({ count: 0 }) const increment = () => state.count++ const decrement = () => state.count-- export default { state: readonly(state), increment, decrement } export const key: InjectionKey<Store> = Symbol('key')さらに、provide/injectをするために必要なキーを用意します。キーには文字列かSymbolで定義できますが基本はSymbolです。(Symbolは、一意な値を返すデータ型です。)
InjectionKey
をジェネリクスで型指定をすると、provide/injectionをした際に型検査が効くようになります。Storeの型定義も定義しておきます。src/types/store.d.tsexport interface Store { state: { readonly count: number; }; increment: () => number; decrement: () => number; }ルートコンポーネントでストアをprovideする
ストアをグローバルで利用したいので、ルートコンポーネントからprovideします。
provide
は(key, value)
を受け取り、provideされた値は子コンポーネントからキーを用いてinject
することで取り出すことができます。src/main.tsimport { createApp, provide } from 'vue' import App from './App.vue' import store, { key } from '@/store' createApp({ ...App, setup () { provide(key, store) } }).mount('#app')コンポーネントからinjectでストアにアクセス
それでは、provideされたストアにinjectでアクセスしてみましょう。
値はinject(key)
とキーを指定して取り出します。取り出した値の型はStore(InjectionKeyのジェネリクスで指定した型) | undefined
ですから、型ガードを用いて確認する必要があります。(誤ったキーを渡した場合や、自身より上の階層でprovideされていなかった場合にundefined
が返されます。)Counter.vue<template> <div>{{ store.state.count }}</div> <button @click="store.increment">Increment</button> <button @click="store.decrement">Dncrement</button> </template> <script lang="ts"> import { computed, defineComponent, inject } from 'vue' import { key } from '@/store' export default defineComponent({ setup () { const store = inject(key) if (!store) { throw new Error('') } return { store } } }) </script>終わりに
今回は、Vuexの代替を目的としてストアを作成したので、グローバルな状態で定義しましたが、provideはルートコンポーネント以外の場所からもできるので、範囲を限定してストアを定義することも可能です。Vuexはグローバルに定義せざるをえなかったのですから、良い点と言えるのではないでしょうか。
さらに、Vuexの持っていた型課題を解決しただけでなく、抽象的な型に依存することにも成功しています。Vue2.2から存在していたもののプラグインやライブラリ以外への使用は推奨されていなかったprovide/injectionですが、データやメソッドがコンポーネントに縛られなくなったので、真価を発揮することが可能になりました。
ただし、Vuexを利用し続けることにも依然メリットは存在します。
Vue Devtoolsを利用したVuexのデバッグはすべてのミューテーションが追跡されており、の履歴が時系列順に表示されたり、Devtool上で値を更新できたりと魅力的です。また、vuex-persistedstateなどの豊富なライブラリも存在します。これらを手放すには、時期尚早ともいえるでしょう。
参考
Should You Use Composition API as a Replacement for Vuex?
Vue Composition API を使ったストアパターンと TypeScript の組み合わせはどのくらいスケールするか?
Vue 3.0時代の状態管理
Vue Composition API + TypeScriptで DI(依存性の注入), DIP(依存性逆転の原則) を実装してみる
- 投稿日:2020-12-07T08:00:05+09:00
Vue.jsと物理演算とElectronで仕事中にデスクトップでお寿司をつまめるようになったのでソースと解説【クソアプリ】
この記事はクソアプリ Advent Calendar 2020の7日目です。昨日は@fujit33さんの【待ってました】ラーメンにコショウをかけるためのアプリを作りました!でした
... オチまで秀逸
作ったおすし
![]()
リモートワーク中のちょっとした待ち時間、みなさんどうしてますか?
ダウンロードが遅い時・ビルドが長い時・イラレがクラッシュした時...![]()
「
ちょっと手持ち無沙汰だけどTwitter見るほどでも(仕事しろ)」...そんな時、軽くお寿司つまんだりしたくなりませんか?
なりますよね?この記事を見たあなたもきっと今すぐお寿司をつまみたい気持ちに心が支配されたはず。
そんな時にこのアプリを起動します。
https://github.com/yuneco/osushi-desktop#download5.良い積みができたらスクショをTwitterに流しましょう(隙あらば宣伝
なぜ作ったか
![]()
繁忙期で残業続きの1週間が終わり、ようやく迎えた12月5日土曜日(おととい)...
今日気づいた一番怖いこと報告しますー
— ゆき (@yuneco) December 5, 2020
(完全に忘れてた) pic.twitter.com/0n4u5vZCtTあと2日wwww もうお寿司食べるしかないじゃないですかwww
技術と解説
![]()
ここからは少しだけ真面目に解説。
ソースコード:https://github.com/yuneco/osushi-desktop使ったフレームワーク・ツールなど
- Electron: webアプリをappやexeにできる魔法使い。...と見せかけてブラウザー(Chromium)丸ごと抱き込んで動かす実は筋肉系。力 is パワー
![]()
- Vue.js: webアプリがサクッと作れる偉い子。たまに「でもそれreactの後追いだよね?」って言われて泣く。今回はVue3 + composition-apiです
- matter.js: おすしが落ちたり積まれたりするために必要な物理演算をやってくれる天才。冬場はマシンを温めるのにも最適。
- TypeScript: 理屈っぽくて面倒なTSちゃんでだけど、クソアプリみたいに勢いで書きなぐる時にはむしろ頼りになる
- electron-builder: Vue-CLIで使えるプラグイン。Vueのプロジェクトに一発でElectronを導入してくれる神の遣い。正直何も理解しないでとりあえずコマンド叩いたら使えた
![]()
- icon-gen: 配布用のアイコンをまとめて作ってくれる気の利く子
- Adobe Illustrator on iPad: おすしの絵を描くよ!便利だけどバグも多いよ!
Step1: Vueのプロジェクトを作る
Vue-CLIの
vue create
を使って、普通に好きな感じの設定で作ります。
ただし、Routerを使いたい場合はHistoryMode使う?
の質問にNoで答えてハッシュモードにする必要あり。多分気をつけるのはそれくらいStep2: Electronの導入
中身の開発に入る前に、そのままElectronを導入します。
cd 作ったVueプロジェクト vue add electron-builder
バージョンとか聞かれると思うけど、適当に新しいの選らんどけばOK
終わったら↓でデバッグ起動。見慣れたVueのHelloWorldが
Electron
って名前の独立したアプリで表示されるはず。npm run electron:serveStep3: ウインドウを透過させる
ここら辺から中身の話。
今回のおすしアプリはウインドウの背景を透過させるのがマストです。まずそこからやってみます。透過自体は簡単で、プロジェクトのルートに生成されている
background.ts
ってファイルに設定を追加するだけ。background.tsasync function createWindow() { // Create the browser window. const win = new BrowserWindow({ width: 800, height: 600, transparent: true, // ✨追加 frame: false, // ✨追加 resizable: false, // ✨追加 backgroundColor: '#00FFFFFF', // ✨追加 hasShadow: false, // ✨追加 alwaysOnTop: true, // ✨追加 webPreferences: { enableRemoteModule: true, // ✨追加 nodeIntegration: true } }) win.setIgnoreMouseEvents(true, { forward: true }); // ...略 }これで画面は表示されるけど背景は完全に透過して、かつクリックしてもすり抜けてデスクトップや他のアプリを触れるようになります。
各設定項目の意味は大体名前の通りなんだけど、1箇所
hasShadow
だけ注意。これがデフォルトのtrue
のままだとアニメーションの残像が残ってチラツキが出るのでfalse
が良いです。影をつけたい場合はCSS側でやるのが吉。あ、あとFAQなのでググったら出てくるけど、devtoolが出てると透過が無効になるので、最初にdevtoolを別窓で出すようにしておきましょう。
Step4: 透過させつつコンテンツだけはクリックさせる
で、ここまでは簡単だった。1,2時間でできてこれなら余裕じゃーんって思ってた土曜の午後。
なんだけどこの次がちょっと関門。...全部透過させちゃったから何もクリックできないのwww
何かの設定でできるでしょ 余裕余裕...って思ってたらできないらしい...まじか
...でググった結果、
Electronと透過ウィンドウ - 特定の要素ではマウスイベントを受け取る
どうやら
- クリックしたい要素に
mouseenter
した → Electronのクリック透過を無効に- クリックしたい要素から
mouseleave
した → Electronのクリック透過を再度有効にしないといけないとのこと。。
せっかくなのでVue3らしくcomposition-apiで実装します。
/src/compositions/useClick.tsimport { onMounted, onBeforeUnmount, Ref } from 'vue' // eslint-disable-next-line @typescript-eslint/no-var-requires const Electron = require('electron') const onErnter = (ev: MouseEvent) => { ev.preventDefault() Electron.remote.getCurrentWindow().setIgnoreMouseEvents(false) } const onLeave = (ev: MouseEvent) => { ev.preventDefault() Electron.remote .getCurrentWindow() .setIgnoreMouseEvents(true, { forward: true }) } const resolveRef = (elRef: Ref) => { const value = elRef.value if (!value) { return null } return value as HTMLElement } const useClick = (elRef: Ref) => { onMounted(() => { const targetDom = resolveRef(elRef) targetDom?.addEventListener('mouseenter', onErnter) targetDom?.addEventListener('mouseleave', onLeave) }) onBeforeUnmount(() => { const targetDom = resolveRef(elRef) targetDom?.removeEventListener('mouseenter', onErnter) targetDom?.removeEventListener('mouseleave', onLeave) }) } export default useClickこれを使って
Clickable
コンポーネントを作って.../src/components/Clickable.vue<template> <div class="ClickableRoot" ref="el"> <slot /> </div> </template> <script lang="ts"> import { defineComponent, ref } from 'vue' import useClick from '../compositions/useClick' export default defineComponent({ setup() { const el = ref(null) useClick(el) return { el } } }) </script>クリックに反応させたい要素をラップすればOK
<template> <Clickable> <button>おせるよ!</button> </Clickable> </template>今回は単純に四角形の当たり判定しかとってないけど、もっと自由な形で正確な当たり判定が必要ならSVG使うしかないかな...と
Step5: おすしを表示する
とりあえずおすしのような何かを表示します。
OK...どう見てもMAGUROですね
Step6. おすしに物理演算を適用
まがりなりにもおすしが出たので、次に物理法則をお寿司に適用します。去年ネタで作った●●WebGL(PIXI.js + glsl)と物理演算(matter.js)で可愛い絵本風タピオカ作ったので解説●●のソースをがっつりコピペして土台を作ります。
今回はCanvas(PixiJS)ではなくHTML(Vue)で画面を表示するので、matter.jsからの更新をうけとって、物理演算世界のおすし(ただの長方形)の座標や角度が変わっていたら、その値をVue側にも反映させます。
/src/components/FlyingSushi.vue#L48
/src/components/FlyingSushi.vueonMounted(() => { // 表示するおすしの座標 state.sushiPos = new Pos(props.initialPos.x, props.initialPos.y) // 物理世界におすし(長方形)を投入 props.world?.addRect( props.initialPos.x, props.initialPos.y, 90, 35, // 物理世界で座標が更新された時のコールバック xyr => { state.sushiPos = new Pos(xyr.x, xyr.y, r2a(xyr.r)) } ) })ここまででおすしを積めるようになりました。
Step7. 寿司レーンをつくる
これも特に解説はいらないのでパス。
SushiRail
コンポーネント(寿司レーン)を作り、その中で一定時間ごとにTurnDish
コンポーネント(皿)を生成して左から右にアニメーションさせます。スクロールアウトしたあたりで適当に削除するのを忘れずに。Step8. 可愛くする
最後にダミーだったおすしの画像をちゃんとしたものに差し替えます。
今回は10月に正式リリースしたばかりのiPad版Adobe Illustratorを使っておすしを描いて、SVGで書き出します。1
ついでに/src/logics/SushiAssets.tsあたりにすしネタの型定義とかも追加します。
/src/logics/SushiAssets.ts/** すしネタ */ export type SushiNeta = | 'uni' | 'toro' | 'tamago' | 'salmon' | 'neko' | 'maguro' | 'kohada' | 'ikura' | 'ika' | 'ebi' /** お皿の色 */ export type DishColor = 'dishAka' | 'dishAo' | 'dishGin' | 'dishKin'Step9. ビルド
正直リリースしたところでこのクソアプリ使う人もいないと思うんだけど、せっかくなのでビルドもします。
クソアプリといえどもアイコンついて単体アプリの形になるとやっぱり気分が上がりますね。アイコンの作り方は【自作デスクトップアプリ】Electronにアイコンを設定する方法【Vue.js】を参照。
今回はクソアプリなので署名とかはしません。ちゃんと公開する場合(特にMac)は、署名つけてAppleの公証も通さないと普通のダブルクリックで起動できない2ので注意。
まとめ
- お手軽にデスクトップアプリを作りたい時の選択肢としてElectron + Vueはいい感じ。
- おすしは良いものです
明日(12/8)は @namosuke さんです!
- 投稿日:2020-12-07T07:04:31+09:00
Vue.jsの基礎をまなぼう!
あいさつ
初めての人は初めまして!知っている人はこんにちは!
中学生バックエンドPGのAtieです!
今回はVue.jsについて学んできたのでアウトプットします!
「え?バックエンドがVue.jsをなんで勉強してるの?」
これはフレームワークに慣れておくためとSPAを開発したかったからです
実際Vue.jsを学んだ感想は少し難しかったです
しかしコードがシンプルだったので読みやすく書きやすかったです
では!環境構築
まずはVue.jsの環境構築をしていきます
まずは必要なファイルを作っていきます
- main.js
- index.html
- style.css
この3つのファイルを作ってください
次にVue.jsを使えるようにします
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="css/style.css"> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>Vue.jsを使うには以下の1行を追加するだけで使えるようになります
便利で簡単ですね!index.html<script src="https://cdn.jsdelivr.net/npm/vue"></script>この行をhtmlのbodyタグの最後の行に入れます
双方向データバインディング
Vue.jsには「双方向データバインディング」ができるという特徴があります
双方データバインディングですがデータバインディングとはUIとデータを結びつけるという意味で双方向というのはdataを更新すればUIが更新されて逆にUIが更新されればdataが更新されるという意味です
たとえばTwitterを例に挙げてみましょう
Twitterのハートの部分を押すと色がピンクになりハートの数が表示されるようになれます
これはUI(ハートの部分)が更新されることでデータ(ハートの数)が更新されています
この双方向データバインディングができることでSPAが実現可能ですでは実際にしてみましょう
まずはVue.jsで制御する要素を作りますindex.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div id="main"> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>divタグにmainというidを付けました
今回はこのmainという要素の中をVue.jsで制御していきます
次にjsを書いていきます
まずは即時関数でエラーチェックをしてjsからmainを使えるようにしますmian.js(function () { 'use strict'; const vm = new Vue({ el: '#main' }) })();UIに結び付くモデルはよくView Modelと言われているので略してvmとしました
そしてnewでVueオブジェクトを作成します
どの領域かを結びつけるかをelementsの略であるelで指定します
cssのように#をつけてidを指定します
これでjsから扱えるようになりました
ではこのモデルにdataを保持してもらいます
dataのなかにnameというキーでAtieという値を保持してもらいますmain.js(function () { 'use strict'; const vm = new Vue({ el: '#main', data: { comment: "name", } }) })();では表示させてみます
表示するには二重波カッコで囲う必要がありますindex.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div id="main"> <p>{{ name }}</p> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>これで値が表示されてます
次はUIからデータに変更できるようにしてみます
inputたぐにv-model="name"とすることでnameと入力フォームが結びつくようになります
即時に変更されていることがわかりますこの二重波カッコですがjsの式をそのまま書くことができます
たとえば入力された文字を大文字にするためにname.toUpperCaseと書くことができますToDoListアプリを作る
ではVue.jsの基礎を抑えたのでToDoListアプリを作っていきます
まずはToDoListを保存する配列を作ります
todosというキーで配列を保存しますmian.js(function () { 'use strict'; const vm = new Vue({ el: '#main', data: { todos: [ 'todo 1', 'todo 2', 'todo 3' ], } }) })();次にhtmlに反映させますがただ単にliタグの中身を二重波カッコで囲てしまうと追加や削除するときにリロードを挟んでしまうので挟まないようにliタグを配列のループでその数だけ表示するようにします
そのためにはv-forでv-for="todo in todos"とします
こうすることでtodoにtodosの値が一つずつ入っていきその分ループします
そしてliタグの中身をtodoの値を表示するようにすればループされながら配列の中身が一つずつ表示されるようになります
これで表示する仕組みが整いましたちなみにですがv-のように始まる特殊な属性をディレクティブと呼びます
次にToDoの追加をできるようにします
そのためにformを追加しますindex.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div id="main"> <ul> <li v-for="todo in todos">{{ todo }}</li> </ul> <form> <input type="text" v-model="newTodo"> <input type="submit" value="Add Todo"> </form> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>v-modelを使って結び付けていきます
submitされた時のイベントを設定していきます
イベントを設定するにはv-onとする必要があります
v-onはよく使うので@と略することができますindex.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div id="main"> <ul> <li v-for="todo in todos">{{ todo }}</li> </ul> <form v-on:submit="addTodo"> <input type="text" v-model="newTodo"> <input type="submit" value="Add Todo"> </form> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>ではsubmitされた時のイベントを設定していきます
methodsというキーにメソッドを設定していきます
data内のデータにはthisでアクセスすることができますmain.js(function () { 'use strict'; const vm = new Vue({ el: '#main', data: { newItem: '', todos: [ 'todo 1', 'todo 2', 'todo 3' ], methods: { addTodo: function() { this.todos.push(this.newItem); }, }, }, }) })();このように書くことができます
しかしこのままではformがsubmitされてページが移行してしまうのでうまくいいきません
これを防ぐためには@submit.preventとすることで防ぐことができますこれでうまくいきます
追加した後にフォームに値が残ってしまうのでnewItemを空にしておきます
main.js(function () { 'use strict'; const vm = new Vue({ el: '#main', data: { newItem: '', todos: [ 'todo 1', 'todo 2', 'todo 3' ], methods: { addTodo: function() { this.todos.push(this.newItem); this.newItem = ''; }, }, }, }) })();ToDoの削除
次にToDoの削除をできるようにしていきます
原理としてはtodosの配列から削除すればいいので簡単ですspanでxを作っていきます
この場合todosの何番目を削除すればいいのかわからないのでこのようにしてindexに数字が入るようにします
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div id="main"> <ul> <li v-for="(todo, index) in todos">{{ todo }} <span @click="deletItem">[X]</span></li> </ul> <form @submit.prevent="addTodo"> <input type="text" v-model="newTodo"> <input type="submit" value="Add Todo"> </form> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>では削除するメソッドを作っていきます
といっても簡単ですspliceでindex番目から1番目を消すだけです
簡単簡単♪main.js(function () { 'use strict'; const vm = new Vue({ el: '#main', data: { newItem: '', todos: [ 'todo 1', 'todo 2', 'todo 3' ], methods: { addTodo: function() { this.todos.push(this.newItem); this.newItem = ''; }, deletItem: function(index) { this.todos.splice(index, 1); this.newItem = ''; }, }, }, }) })();これで削除の仕組みができました
完了状態を管理する
では次に完了状態を管理できるようにします
完了状態を管理するためにtodosをオブジェクトにしてtitleとisDoneで管理しますmain.js(function () { 'use strict'; const vm = new Vue({ el: '#main', data: { newItem: '', todos: [{ title: 'task 1', isDone: false }, { title: 'task 2', isDone: false }, { title: 'task 3', isDone: true }], methods: { addTodo: function() { this.todos.push(Item); this.newItem = ''; }, deletItem: function(index) { this.todos.splice(index, 1); this.newItem = ''; }, }, }, }) })();ただこのままでは表示がおかしくなるのでhtmlも変えます
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div id="main"> <ul> <li v-for="(todo, index) in todos">{{ todo.title }} <span @click="deletItem">[X]</span></li> </ul> <form @submit.prevent="addTodo"> <input type="text" v-model="newTodo"> <input type="submit" value="Add Todo"> </form> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>次に完了状態を可視化していきます
まずは以下のようにhtmlを変えます
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div id="main"> <ul> <li v-for="(todo, index) in todos">{{ todo.title }} <span @click="deletItem">[X]</span></li> </ul> <form @submit.prevent="addTodo"> <input type="checkbox" v-model="todo.isDone"> <input type="text" v-model="newTodo"> <input type="submit" value="Add Todo"> </form> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>次にisDoneの状態によってチェックの表示状態を変えたいのですが...
便利なことにisDoneがtrueの時にチェックがついてくれますチェックがついた項目はdoneというclassをつけてあげてあげます
データにおうじてclassを付け替えるにはv-bind:classをつけます
v-bindもv-onと同じくよく使われるので:で略してつけることができますmain.js<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div id="main"> <ul> <input type="checkbox" v-model="todo.isDone"> <span :class="{done :todo.isDone}" <li v-for="(todo, index) in todos">{{ todo.title }}<span @click="deletItem">[X]</span> </li> </ul> <form @submit.prevent="addTodo"> <input type="text" v-model="newTodo"> <input type="submit" value="Add Todo"> </form> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>次にToDoListがなかったらなにか表示させてみましょう
そのためには条件分岐をする必要があるのですが条件分岐をするにはv-ifというディレクティブがあります
今回はToDoListがなかったら「No todolist」と表示させます
通常ならいつもどおりliのところに入れて条件分岐させるのですがv-ifとv-forではv-forのほうが優先されてしまうのでulのほうに書きますmain.js<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/styles.css"> </head> <body> <div id="app" class="container"> <h1>My Todos</h1> <ul v-if="todos.length"> <li v-for="(todo, index) in todos"> <input type="checkbox" v-model="todo.isDone"> <span :class="{done: todo.isDone}">{{ todo.title }}</span> <span @click="deleteItem(index)" class="command">[x]</span> </li> </ul> <ul v-else> <p>No todolist</p> </ul> <form @submit.prevent="addItem"> <input type="text" v-model="newItem"> <input type="submit" value="Add"> </form> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>todos.lengthがtrueだったらv-ifの処理が実行されてv-elseの場合はメッセージを表示するようになっています
v-ifを使わないで書く書き方がありますがそっちのほうがすっきりしているしているのでそちらを使います
v-showというディレクティブです
v-showはv-ifと同じように条件を入れますindex.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/styles.css"> </head> <body> <div id="app" class="container"> <h1>My Todos</h1> <ul> <li v-for="(todo, index) in todos"> <input type="checkbox" v-model="todo.isDone"> <span :class="{done: todo.isDone}">{{ todo.title }}</span> <span @click="deleteItem(index)" class="command">[x]</span> </li> <li v-show="!todos.length">No todos</li> </ul> <form @submit.prevent="addItem"> <input type="text" v-model="newItem"> <input type="submit" value="Add"> </form> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>今回はtodos.lengthがfalseの時に実行してほしいので!を使ってnot論理回路にしました
ToDoListの数を表示する
次にToDoListの数と終わった数を表示させます
Vue.jsは算術プロパティを使うことができるので使っていきますcomputedというキーを使ってデータから動的にプロパティを計算してくれる算術プロパティがあるので使っていきます
remainingとしてあげてデータから自動的にremainingを算出してプロパティにしてあげます
今回はisDoneがfalseの項目を調べたいのでjsのfilterという命令を使ってみます
filter関数を引数に取るのでfunction(todo)としつつtodoのisDoneがfalse
つまり残っているタスクをreturnすればそれをフィルターしてitemにまだ終わっていないタスクを入れていけばわかります
今回まだ終わってないタスクの件数を調べたいのでlengthを返してあげればremainingにはisDoneがfalseの件数はいってくるはずですmain.js(function() { 'use strict'; var vm = new Vue({ el: '#app', data: { newItem: '', todos: [{ title: 'task 1', isDone: false }, { title: 'task 2', isDone: false }, { title: 'task 3', isDone: true }] }, methods: { addItem: function() { var item = { title: this.newItem, isDone: false }; this.todos.push(item); this.newItem = ''; }, deleteItem: function(index) { this.todos.splice(index, 1); }, computed: { remaining: function() { const items = this.todos.filter(function() { return !todo.isDone; }); return items.length; } } } }); })();少しややこしいですがよく見るとただitemsにtodo.isDoneがfalseのを入れていてその下でitemsの数を返しています
main.js<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/styles.css"> </head> <body> <div id="app" class="container"> <h1> My Todos <span class="info">({{ remaining }} / {{ todos.length }})</span> </h1> <ul> <li v-for="(todo, index) in todos"> <input type="checkbox" v-model="todo.isDone"> <span :class="{done: todo.isDone}">{{ todo.title }}</span> <span @click="deleteItem(index)" class="command">[x]</span> </li> <li v-show="!todos.length">No todos</li> </ul> <form @submit.prevent="addItem"> <input type="text" v-model="newItem"> <input type="submit" value="Add"> </form> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>あとはspanタグで表示できるようにしておきました
完了したタスクの一括削除
次に完了したタスクの一括削除をしていきます
purgeというボタンをつけて@clickでpurgeという処理をさせますpurgeというメソッドを作っていきます
まずthis.todosにremainingを割り当てることで終わった数ではなくて終わった配列そのものを返すようにします
remainingを表示するところも直す必要がありますmain.js(function() { 'use strict'; var vm = new Vue({ el: '#app', data: { newItem: '', todos: [{ title: 'task 1', isDone: false }, { title: 'task 2', isDone: false }, { title: 'task 3', isDone: true }] }, methods: { addItem: function() { var item = { title: this.newItem, isDone: false }; this.todos.push(item); this.newItem = ''; }, deleteItem: function(index) { this.todos.splice(index, 1); }, computed: { remaining: function() { return this.todos.filter(function(todo) { return !todo.isDone; }) } }, purge: function() { if (!confirm('delet finished?')) { return; } this.todos = this.remaining; } } }); })();index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>My Vue App</title> <link rel="stylesheet" href="css/styles.css"> </head> <body> <div id="app" class="container"> <h1> My Todos <span class="info">({{ remaining.length }} / {{ todos.length }})</span> </h1> <ul> <li v-for="(todo, index) in todos"> <input type="checkbox" v-model="todo.isDone"> <span :class="{done: todo.isDone}">{{ todo.title }}</span> <span @click="deleteItem(index)" class="command">[x]</span> </li> <li v-show="!todos.length">No todos</li> </ul> <form @submit.prevent="addItem"> <input type="text" v-model="newItem"> <input type="submit" value="Add"> </form> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="js/main.js"></script> </body> </html>LocalStrageでデータの永続化
次にLocalStorageでデータの永続化をしてみます
データの保存はtodosに変更が加えられたときに保存の処理を実行したいのでdeep watcherを使っていきます
watchだけだと配列の中身の変更まで監視してくれないのでdeepオプションをオンにするためにdeep: trueとする必要がありますmain.js(function() { 'use strict'; var vm = new Vue({ el: '#app', data: { newItem: '', todos: [{ title: 'task 1', isDone: false }, { title: 'task 2', isDone: false }, { title: 'task 3', isDone: true }] }, watch: { todos: { handler: function() { localStorage.setItem('todos', JSON.stringify(this.todos)); } }, deep: true }, methods: { addItem: function() { var item = { title: this.newItem, isDone: false }; this.todos.push(item); this.newItem = ''; }, deleteItem: function(index) { this.todos.splice(index, 1); }, computed: { remaining: function() { return this.todos.filter(function(todo) { return !todo.isDone; }) } }, purge: function() { if (!confirm('delet finished?')) { return; } this.todos = this.remaining; } } }); })();データが保存できたので取り出してみます
this.todosに対してjsonデータをparseしつつlocalStrageからtodoのキーでデータをgetItemすればいいです
ついでにthis.todosの配列を空にしておきますmain.js(function() { 'use strict'; var vm = new Vue({ el: '#app', data: { newItem: '', todos: [] }, watch: { todos: { handler: function() { localStorage.setItem('todos', JSON.stringify(this.todos)); } }, deep: true }, methods: { addItem: function() { var item = { title: this.newItem, isDone: false }; this.todos.push(item); this.newItem = ''; }, deleteItem: function(index) { this.todos.splice(index, 1); }, mounted: function() { this.todos = JSON.parse(localStorage.getItem('todos')) || []; }, computed: { remaining: function() { return this.todos.filter(function(todo) { return !todo.isDone; }) } }, purge: function() { if (!confirm('delet finished?')) { return; } this.todos = this.remaining; } } }); })();もし何も保存されていなければエラーになるのでそれを防ぐためになかった場合は空の配列を返すようにしました
これで一通りのVue.jsの基礎をまなべました
最後に
Vue.jsの基礎をこの記事にギュッと詰め込みました!
コンポーネントはまた別の記事で解説します
最後まで読んでいただきありがとうございました!
Twitterしています!→AtieのTwitter
では!また次回の記事で!
- 投稿日:2020-12-07T03:33:47+09:00
【Vue CLI】Cannot read property 'hoge' of undefined への対応
はじめに
Cannot read property 'hoge' of undefined
Vue.jsを用いた開発をしている人なら、誰もが一度は見たことのあるエラーです。エラーの原因が多岐に渡り、Vueのフレームワークを使い慣れていない人にとっては原因の特定が難しいのではないかと感じています。筆者も最初は戸惑いました。
そこで、ある程度Vue.jsを書けるようになってきた自分なりに、このエラーと直面した際に考えるべき原因と、対処法をまとめてみました。
Vue.js(特にVue CLI)を用いた際に注目して書いています。
Cannot read property 'hoge' of undefined とは
コード上ではエラーが出ないのに、いざ実行するとコンソールにこんなものがでて、うまく動かないことがあります。
Cannnot read property 'hoge' of undefined日本語にすると、「undefinedのhogeというプロパティが読めないよ」という意味です。
筆者も最初勘違いしていたのですが、この場合undefined
となっているのはhoge
ではなく、hogeの一つ前のオブジェクトです。例えば、piyo.hoge
というコードを書いていてこのエラーが出たなら、問題があるのはhoge
ではなくpiyo
だということになります。英語を勉強しないとダメですね。
原因
Vue CLIでこのエラーが出る場合、大抵は以下のどれかが原因だと思います。
piyo
にデータが入っていない場合
piyo
にデータが入っていない場合、当然piyo.hoge
にもデータは存在せず、上記のエラーが生じます。コンポーネント間で変数を共有しようとしている。
vuexやpropsなどを用いない場合は、コンポーネント間で変数は共有されません。
従って、あるvueファイルで作った変数を他のvueファイルで使おうとするとすると上記のエラーが出ます。関数を実行する順番を間違えている。
piyo
に値を入力する関数は書いているものの、その関数を呼び出す前に使用してしまう場合です。
同一ファイルにすべて書いている場合はコードの実行順が分かりやすいですが、他のコンポーネントをimportして使用することが多いVue CLIでは分かりにくく、混乱しやすいのではないでしょうか。まずは、importされている順番を確認しましょう。非同期処理を考慮せずに書いている。
非同期処理とは、プロジェクトがAPIなどにアクセスし、データを取ってくる間に、他の部分のコードが進んでしまうことです。上記の「関数を実行する順番を間違えている。」の仲間です。
piyo.hoge
は存在するが、正しく呼び出せていない場合ちゃんと値を代入することは出来ているのだから、正しく呼び出しましょう。ネットに転がっている普通のJavaScriptのサンプルコードをVue.jsっぽく書き直そうとしているうちに訳が分からなくなってしまい、このエラーとなる場合が多い気がします。
thisの挙動を考慮していない。
this
を用いてdataやcomputedの値を参照書いている場合に限りますが、これが原因で苦しめられる人は初学者の方に特に多そうです。
JavaScriptはthisの挙動がアロー関数の場合と通常関数の場合で異なるため、dataにアクセスしようと思ってthis
を使ったのに思わぬ挙動をしていることがあります。
個人的には、アロー関数が使えるところではアロー関数を使うように心がけるとエラーが減ると思っています。スペルミス
単純に変数のスペルを間違えてしまった場合もこのエラーが出ることがあります。
灯台下暗しとはこのことです。エラーの原因が分かった時には、嬉しさと悔しさが一緒にきて複雑です。原因が似ているエラー
①hoge is not a function
hoge is not a function
Cannot read property 'hoge' of undefined
はhoge
がオブジェクトだった場合に起こるものですが、hoge
が関数だった場合、上のようなエラーが出ます。
エラーへのアプローチ方法としては全く同じです。②Cannot read property hoge of null
Cannot read property hoge of null
piyo.hoge
が存在した時に、piyoが無い(null)場合、こうなります。
例えば、データが一つも入ってない場合などが挙げられます。
基本的なエラーへのアプローチはCannot read property 'hoge' of undefined
と一緒で大丈夫でしょう。終わりに
Vue CLIに限らず、どんな言語やフレームワークでもエラーとの戦いは辛く厳しいものですが、エラーに当たったときに「新しい知見を得た!」と思ってポジティブに捉えて精進していきたいです。
参考文献
- 投稿日:2020-12-07T02:55:53+09:00
ハッカソンで【vue.jsによるvue.jsのためのオンラインジャッジサービス】を開発した
前書き
先日jphacks2020に参加して、
【frontEngine】というサービスを開発しました。
ファイナリストまで行き、その後あまり良い結果にならず、
ハッカソンとしての反省点などもありますが、
今回に関しては、frontEngineで実装された機能がどのように動いているのかを解説していきます。今回のものを作成して感じたことは、ジャッジシステムを搭載するよりも、
userからのvue.jsのコードを受け取って、(セキュリティ上)安全にvue.jsを表示できる様になる等プログラミング教材として展開していくと、良いのではないかと考えております。また、リアクションがどの程度もらえるかわかっていないので、
リアクションがあれば、どんどん記事を精錬させていく所存です。(質問もガンガンください!!)知らない人もたくさんいると思うので、どういう機能があるかといいますと、
プロジェクト単位(router設定やページ登録)で表現できる!!
デザイン審査もできる(progateみたいにCSSをみているんじゃなくて、CSSの要素 && 要素間の位置をみている)!!
userが書いたコードをそのままプレビューすることができる!!
ジャッジシステムで採点してくれる!!
レーティングが反映される!!(この記事では解説しません)概要
今回解説する機能群
frontEngineのデモ
— front engine (@EngineFront) December 7, 2020プロジェクト単位拡張 pic.twitter.com/quwAXAVTef
— front engine (@EngineFront) December 6, 2020・ Vue.jsのTemplate部分解析
に挟まれている部分に対して、
こちら側で処理しやすいように解析します。・ Vue.jsのscript部分解析
に挟まれている部分に対して、
jsのように自由に実行できる様に行います。
(ここの部分に)・ Vue.jsのstyle部分解析
に挟まれている部分に対して、
dom側に適用しやすいようにします・ 上記三つを組み合わせて、vue.js単一ファイルとして機能させる
v-forだったり、v-ifだったりを機能させ、出力できる様にします。・ プレビュー機能
$mount等で機能させます。・ プロジェクト単位拡張
現状単一ファイルでしか、読み取れない問題をrouter/index.jsや各ページ単位の問題のように拡張します。・ ジャッジシステム/スタイルジャッジシステム
正直ここが甘い作りになっていましたが、できる限りやったものを載せます他にもターミナル機能やレーティングシステムなどもありますが、
それらに関しては私以外のチームメンバーが頑張ってくれたので、チームメンバーが記事を書いてくだされば、リンクを載せます。Vue.jsのTemplate部分解析
主にここでソースコードがあります。
domProcess
まず、説明する前に
hanoi
を任意の問題のホームのソースコード入力欄に入れてみて、どのような加工がされるのかをみていきます。
実際に入力するとhanoi{ "open": true, "close": false, "name": "div", "others": [ { "left": "class", "right": "exam4", "directive": false, "type": "variable", "variableType": "String", "variables": [ "exam4" ] }, { "left": "class", "right": "exam4", "directive": false, "type": "variable", "variableType": "String", "variables": [ "exam4" ] } ], "class": { "left": "class", "right": "exam4", "directive": false, "type": "variable", "variableType": "String", "variables": [ "exam4" ] }, "unique": 0, "depth": 0, "children": [ { "open": true, "close": false, "name": "answer-card", "others": [ { "left": "class", "right": "hanoi", "directive": false, "type": "variable", "variableType": "String", "variables": [ "hanoi" ] }, { "left": "@click", "right": "utusu('left')", "directive": false, "type": "function", "functionTarget": "utusu", "functionArgument": [ "'left'" ] }, { "left": "@click", "right": "utusu('left')", "directive": false, "type": "function", "functionTarget": "utusu", "functionArgument": [ "'left'" ] } ], "class": { "left": "class", "right": "hanoi", "directive": false, "type": "variable", "variableType": "String", "variables": [ "hanoi" ] }, "parentId": 0, "unique": 1, "depth": 1, "children": [ { "open": true, "close": false, "name": "answer-card", "others": [ { "left": "v-for", "right": "left", "directive": true, "target": { "value": "value", "index": "index" }, "type": "variable", "variableType": "global" }, { "left": "@click", "right": "trans(value, 'left')", "directive": false, "type": "function", "functionTarget": "trans", "functionArgument": [ "value", " 'left'" ] }, { "left": "key", "right": "index", "directive": true, "type": "variable", "variableType": "global" }, { "left": "key", "right": "index", "directive": true, "type": "variable", "variableType": "global" } ], "v-for": { "left": "v-for", "right": "left", "directive": true, "target": { "value": "value", "index": "index" }, "type": "variable", "variableType": "global" }, "parentId": 1, "unique": 2, "depth": 2, "children": [ { "value": "{{value}}", "reserves": [ { "start": 0, "end": 8, "text": "value", "textRawValue": "value", "type": "variable", "variableType": "global" } ], "parentId": 2, "unique": 3, "depth": 3, "name": "reserveText" } ] } ] }, { "open": true, "close": false, "name": "answer-card", "others": [ { "left": "class", "right": "hanoi", "directive": false, "type": "variable", "variableType": "String", "variables": [ "hanoi" ] }, { "left": "@click", "right": "utusu('center')", "directive": false, "type": "function", "functionTarget": "utusu", "functionArgument": [ "'center'" ] }, { "left": "@click", "right": "utusu('center')", "directive": false, "type": "function", "functionTarget": "utusu", "functionArgument": [ "'center'" ] } ], "class": { "left": "class", "right": "hanoi", "directive": false, "type": "variable", "variableType": "String", "variables": [ "hanoi" ] }, "parentId": 0, "unique": 6, "depth": 1, "children": [ { "open": true, "close": false, "name": "answer-card", "others": [ { "left": "v-for", "right": "center", "directive": true, "target": { "value": "value", "index": "index" }, "type": "variable", "variableType": "global" }, { "left": "@click", "right": "trans(value, 'center')", "directive": false, "type": "function", "functionTarget": "trans", "functionArgument": [ "value", " 'center'" ] }, { "left": "key", "right": "index", "directive": true, "type": "variable", "variableType": "global" }, { "left": "key", "right": "index", "directive": true, "type": "variable", "variableType": "global" } ], "v-for": { "left": "v-for", "right": "center", "directive": true, "target": { "value": "value", "index": "index" }, "type": "variable", "variableType": "global" }, "parentId": 6, "unique": 7, "depth": 2, "children": [ { "value": "{{value}}", "reserves": [ { "start": 0, "end": 8, "text": "value", "textRawValue": "value", "type": "variable", "variableType": "global" } ], "parentId": 7, "unique": 8, "depth": 3, "name": "reserveText" } ] } ] }, { "open": true, "close": false, "name": "answer-card", "others": [ { "left": "class", "right": "hanoi", "directive": false, "type": "variable", "variableType": "String", "variables": [ "hanoi" ] }, { "left": "@click", "right": "utusu('right')", "directive": false, "type": "function", "functionTarget": "utusu", "functionArgument": [ "'right'" ] }, { "left": "@click", "right": "utusu('right')", "directive": false, "type": "function", "functionTarget": "utusu", "functionArgument": [ "'right'" ] } ], "class": { "left": "class", "right": "hanoi", "directive": false, "type": "variable", "variableType": "String", "variables": [ "hanoi" ] }, "parentId": 0, "unique": 11, "depth": 1, "children": [ { "open": true, "close": false, "name": "answer-card", "others": [ { "left": "v-for", "right": "right", "directive": true, "target": { "value": "value", "index": "index" }, "type": "variable", "variableType": "global" }, { "left": "@click", "right": "trans(value, 'right')", "directive": false, "type": "function", "functionTarget": "trans", "functionArgument": [ "value", " 'right'" ] }, { "left": "key", "right": "index", "directive": true, "type": "variable", "variableType": "global" }, { "left": "key", "right": "index", "directive": true, "type": "variable", "variableType": "global" } ], "v-for": { "left": "v-for", "right": "right", "directive": true, "target": { "value": "value", "index": "index" }, "type": "variable", "variableType": "global" }, "parentId": 11, "unique": 12, "depth": 2, "children": [ { "value": "{{value}}", "reserves": [ { "start": 0, "end": 8, "text": "value", "textRawValue": "value", "type": "variable", "variableType": "global" } ], "parentId": 12, "unique": 13, "depth": 3, "name": "reserveText" } ] } ] } ] }このようになりました。
みてわかる通り、AST分析を行なっております。ここで説明のために、
一つのモノ(open,close,name,others,children等を持っている一要素)に対して、
部品と呼ぶことにします。基本的にゴリゴリっと'<'とか'/>'等を検知して、
BFSやらDFSを行なっております。
Text要素か、dom要素で大きくパターンが変わるのですが、
まず、dom要素からみていきます。
dom要素では、基本的に、':key='だったり'key='だったり'active'等の三パターンに分かれます。
それらの要素をotherとして各要素のothersに格納します。
(=を基準としてみて、左側の属性をleft,右側の属性をrightとしています。)
それに伴い、:であれば、directiveとし、それ以外は文字列型格納として処理します。
(同様に=でない要素に関してはtrueとして格納します)
また、functionかそうでないかは()があるかどうかで、判断できるので、functionであれば、
argumentも別枠として格納しておきます。次にdom要素として、v-forは、記法が特殊で、left,rightで格納する以外に、前処理してないと扱えたものではないので、ofもしくはinの右側を、通常のrightの部分に格納し、変数部分((value, index) in items のitems,index部分)に関しては、targetというところに格納しておきます。
次にText要素ですが、'{{}}'か生のtextのどちからに分かれると思います。
ただ、もし、{{}}や生のTextが混同しているとしても、dom要素に触れるまで('<name ~...'を見つけるまでは)は、同じ部品として対処していきます。
その部品の中で、あらたにreservesを定義し、またnameを'reserveText'とします。
reservesの中では、{{}}の要素か生のTextかを区別します。
{{で始まれば,}}があるまでは、同一の要素とみなし、生のTextは{{が始まるまでは、生のTextとして処理します。
(startやendなども一応記録として載せていますが、type要素以外domPropery.jsがよしなにしてくれるようにしてます。)Vue.jsのscript部分解析
主に下記3つがメインファイルになります
moduleProcess
utility
execScript
script解析に関しては、汎用性が高いと思いますので、npmライブラリ化しようかと考えております。
ので詳しい処理の流れよりもざっくりどのようになっているかを解説致します。
まず、MainProcessから<script></script>内部がmoduleProcessに渡されます。
最初に、module単位でBabelを使いAST化し、各ブロック(data,props,methods,computedなど)に渡され、各々のモノが実行されます。
babelを使ってもAST化されるだけで実行や、変数の変換などは行えません。(仮に行えてもuserからの入力jsをそのまま実行できたらセキュリティ上やばいですよね)
そこで今回は、javascriptの上で動くjavascriptに仕様等が似ている言語のインタプリタを作成していきます。今回では、propsは別処理としておいているので、dataからみていくことにします。
data
dataProcess実際にdataProcessの中をみていきます。
dataProcess.jsimport { CheckProperty, getProperty } from './utility.js' import { global } from '../moduleProcess.js' export default function (body) { const output = {} for (const property of body.body.body[0].argument.properties) { const getter = getProperty(property) output[property.key.name] = getter } return output }引数として、babelでAST化されたdataの中身が渡され、それをfor文で回していきます。
ここでgetPropertyという超便利funcが呼び出されてますが、これは渡されたものを許可された(こちら側で設定した)範囲でjsの値として返してくれます。(実際にconsoleなどは定義してないので、呼び出されることはありません)
key名: value
というdata型で定義されているものを、outputというオブジェクトに定義されているkey名をkeyとして、getPropertyを使いASTから正常な値となったものを該当するkeyのvalueとして格納し、moduleに返します。methods
次に、methodsProcessをみていきます。
methodsProcess.jsexport default function (body) { const output = {} for (const property of body.value.properties) { output[property.key.name] = property.value if (output[property.key.name]) { output[property.key.name].func = true } } return output }methodsをみていくと、dataに比べgetPropertyのような、ASTを扱える値に変換するような物がなく、
直接ASTを代入しています。
実際に,上記で使ったhanoiのファイルを流し込んでやると、
hanoiが持っているtransとutusuがoutputに格納されるはずなのでみてみます。hanoiのmethods{trans: Node, utusu: Node} trans: Node {type: "FunctionExpression", start: 356, end: 749, loc: SourceLocation, range: undefined, …} utusu: Node {type: "FunctionExpression", start: 762, end: 927, loc: SourceLocation, range: undefined, …} __proto__: Objectとなり、試しに展開してみると、
hanoiのmethodsをjson展開{ "trans": { "type": "FunctionExpression", "start": 356, "end": 749, "loc": { "start": { "line": 20, "column": 11 }, "end": { "line": 31, "column": 5 } }, "id": null, "generator": false, "async": false, "params": [ { "type": "Identifier", "start": 366, "end": 371, "loc": { "start": { "line": 20, "column": 21 }, "end": { "line": 20, "column": 26 }, "identifierName": "value" }, "name": "value" }, { "type": "Identifier", "start": 373, "end": 379, "loc": { "start": { "line": 20, "column": 28 }, "end": { "line": 20, "column": 34 }, "identifierName": "houkou" }, "name": "houkou" } ], "body": { "type": "BlockStatement", "start": 381, ...省略... "directives": [] }, "func": true }, "utusu": { "type": "FunctionExpression", "start": 762, "end": 927, "loc": { "start": { "line": 32, "column": 11 ...省略... }, "right": { "type": "BooleanLiteral", "start": 908, "end": 913, "loc": { "start": { "line": 35, "column": 26 }, "end": { "line": 35, "column": 31 } }, "value": false } } } ], "directives": [] }, "alternate": null } ], "directives": [] }, "func": true } }と、まともに表現することができないほど(したらページが大変なことになってしまう)AST化が完全にされているファイルを格納しています。
methodsが呼ばれるタイミングを考えてみると、DOMで呼ばれるか、もしくはscript内で、
this参照で呼び出しにいく時だと思いますので、その時に便利メソッドgetScriptを呼び出すことで、実行するという仕様にします。getScript/execScript
次に、便利メソッドのスタメンであるgetScript/execScriptを紹介します。
実際の中身の詳しい解説に関しては要望があれば致しますが、今回はとりあえずどの様な場面で使われて、どの様な仕組みなのかについて解説致します。
基本的にはgetScriptを使うことにより、他funcからの呼び出しを違和感なく済ませることができます。getScriptfunction getScript (body, array, preLocal) { return execScript(body, array, preLocal).returnArguments }また、execScriptが処理できる領域は
functionfunction (a,b,c) { //中身 }このようなブロック単位が存在するfunctionを上手く処理することができます。
さて、次に引数に注目すると、body,array,preLocalという3つを受け取っていることがわかります。
・bodyはmethodsがそのままのASTを保存していたように、bodyではmethodsに格納した様なASTを受け取ります。
・arrayはfunctionの引数を配列型で受け取り、body.params(上記で示した例のa,b,cの部分)をkeyとして、このブロック単位のfunctionが持つローカル変数として格納しますarray格納let local = {} if (array && body.params) { for (let i = 0; i < body.params.length; i++) { local[body.params[i].name] = array[i] } }・preLocalは、親block単位要素を引継ぎます。
これがどういうことかと言うと、私が実装しているif文の処理を参考に解説致します。IfStatementcase 'IfStatement': let targetGO = access.body[i] let targetDo = null while (true) { if (!targetGO.hasOwnProperty('test')) { // else // -> つまりifでないものが全てとおる targetDo = targetGO break } let resultBool = isBool(targetGO.test, local) if (!resultBool) { // false if (targetGO.alternate) { targetGO = targetGO.alternate } else { break } } else { // true targetDo = targetGO.consequent break } } if (targetDo) { let get = execScript(targetDo, array, local) Object.keys(get.returnLocal || {}).forEach(key => { local[key] = get.returnLocal[key] }) } breakこの処理では、
if(targetDo){}
の中身が、実行されうるif文のblockと考えてください。
そうすると、我々は一番上のblock要素のfunctionの中にいますが、if文の中の結果の変数などは、if文から出ると、棄却されなければいけません。そこを考えると、if文や、for文やwhile文などのblock要素も、一つ下のexecScriptで処理すべき領域と考えます。
そうすると、一つ下の階層(functionから考えて、if文やfor文等)は上の階層の変数などを変更しうる可能性があるので、preLocalの場所に自分のlocal情報を渡しexecScriptlet local = {} if (preLocal) { local = Object.assign(local, preLocal) }親のlocalの情報を参照できる様にします。
次に、break文などで終了する場合や単純に、処理が末端まで行えた場合、また親側の処理に戻りますので、ifStatementObject.keys(get.returnLocal || {}).forEach(key => { local[key] = get.returnLocal[key] })とし、親側で親側で渡したlocalに対して、更新をかけます。
この更新を行うことにより、子の変数の棄却や、その他の子block内の処理をスムーズに行えます。これら以外にも、
for文の実装などは変数の都度更新など行っておりますので、よかったらみてくださると嬉しいです。
(isBoolやcalculationやその他の処理もみてくれると嬉しいです。)forStatementcase 'ForStatement': const target = access.body[i] const initTarget = target.init.declarations[0] const initName = initTarget.id.name let initIndex = calculation(initTarget.init, local) const readyupdate = target.update let updateCalculation = target.update.right if (!target.update.right) { // argument? updateCalculation = target.update } let updateName = '' if (readyupdate.left) { updateName = readyupdate.left.name } else { // argument updateName = readyupdate.argument.name } const readyBool = target.test while (isBool(readyBool, { ...local, [initName]: initIndex })) { let get = execScript(target.body, array, { ...local, [initName]: initIndex }) Object.keys(get.returnLocal || {}).forEach(key => { if (key !== initName) { local[key] = get.returnLocal[key] } }) if (get.returnOrder === 'break') { break } // updateFunc if (initName === updateName) { initIndex = calculation(updateCalculation, { ...local, [initName]: initIndex }) if (initIndex === false) { console.error('why false!?', updateCalculation, initIndex) break } } else { local[updateName] = calculation(updateCalculation, { ...local, [initName]: initIndex }) } } breakgetProperty/CheckProperty
便利メソッド二つ目のgetProperty/CheckPropertyです。
CheckPropertyは最初の方に作成して、のちにgetPropertyと併用する形となっているので、仕様上少しの違いが生じています(例えば、後述するglobalを自分の子供以降をみてglobalを使っていたり...)
CheckPropertyは、Object,Array,Int,String,Boolなどに対して、値として返してくれるfuncです。
一方getPropertyは、呼び出された場所にあるlocal(上述したfunctionのlocalだったり)や、オブジェクトのキーにアクセスしたり、こちら側で登録したモノ(今回で言えば、ObjectやNumberなど)にアクセスしたり、functionだったらgetScriptとして実行したり、global(vue.js単一ファイル上に存在するthisでアクセスできる場所)を扱ってそうでなければCheckpropertyを呼び出すfunctionです。module
もう一度moduleProcessをみてみます。
methodsや、dataやcomputedなどが、先ほど少し話したglobalに格納されていきます。
this参照した時や、domからのアクセスの時にgetPropertyを通して、globalから取り出すことで最適なfunctionだったり、パラメータを取得することができます。Vue.jsのstyle部分解析
styleProcess
Vue.jsのstyleとはここでは、vue.jsの<style scpoed></style>内部を指すこととします。
(やっていることは特殊なことはないので、特定ファイルのcssファイルなどにも応用できるかと思います。)さてここで、vue.jsには
v-bind:style
というものがあります。
これを使えば、後述するプレビュー機能でも、cssに対して特別な設定をすることなく、オブジェクトを渡したら描画することができそうです。styleProcess.jsif (val == ' ' || val == '\n' || val == '/s' || val == '↵') { continue } if (val === '{') { let targetInput = 'tag' switch (target[0]) { case '.': targetInput = 'class' target.shift() break case '#': targetInput = 'id' target.shift() break } let targetVal = target.join('') output[targetInput][targetVal] = {} i++ let outputKey = [] let value = [] let coron = false for (;i < style.length; i++) { if (style[i] == '}') { // とりあえずclassの階層構造は無視 break } if (style[i] != ';') { if (style[i] == ':') { coron = true continue } if (!coron) { if (style[i] == ' ' || style[i] == '\n' || style[i] == '/s' || style[i] == '↵') { continue } outputKey.push(style[i]) } else { value.push(style[i]) } } else { output[targetInput][targetVal][outputKey.join('')] = value.join('') outputKey = [] value = [] coron = false } } target = [] } else { target.push(val) }短いので掲載しましたが、domProcessの簡略版の様な、
文字列を探索し、オブジェクトに直していきます。上記三つを組み合わせて、vue.js単一ファイルとして機能させる
ジャッジシステム、プレビュー機能と少し被りますが、
ここでは、プレビュー画面に載せても機能する用に一度ジャッジシステム -> domへのparseを紹介していきます。まず、最初にジャッジシステムを通さず、templateをAST化したものを読み込むpureDomPreviewParseをみていきます。
pureDomPreviewParse.jsfunction pureDomPreviewParse (domTree, fileName) { console.log('getDomPreviewParse', domTree) const output = [] let stack = [domTree] const parentParam = {} output.push('<div id="previewDOM">') while (stack.length > 0) { const take = stack.pop() const parseDom = {} const yoyaku = {} if (take.hasOwnProperty('class')) { let targetDom = [] targetDom.push('\'' + fileName + '\'') if (take.class.hasOwnProperty('variables')) { take.class.variables.forEach(key => { targetDom.push('\'' + key + '\'') }) } parseDom['v-bind:style'] = 'classEvent(' + targetDom.join(',') + ')' } if (take.hasOwnProperty('v-for')) { const targetValue = [] if (take['v-for'].target.hasOwnProperty('value')) { targetValue.push(take['v-for'].target.value) yoyaku[take['v-for'].target.value] = 'value' } if (take['v-for'].target.hasOwnProperty('index')) { targetValue.push(take['v-for'].target.index) yoyaku[take['v-for'].target.index] = 'index' } let targetInput = '' const targetDom = [] if (take['v-for'].type) { targetDom.push('\'' + take['v-for'].right + '\'') targetDom.push('\'' + fileName + '\'') Object.keys(parentParam).forEach(key => { targetDom.push('{' + key + ': ' + key + '}') }) targetInput = 'this.domEvent(' + targetDom.join(',') + ')' } // const targetOutput = '(' + targetValue.join(',') + ')' + ' of ' + parseDom['v-for'] = '(' + targetValue.join(',') + ') of ' + targetInput } if (take.hasOwnProperty('others')) { // 現状v-forとclassを分けたらいけるか...? for (let i = 0; i < take.others.length; i++) { if (take.others[i].left === 'v-for' || take.others[i].left === 'class' || take.others[i].left === 'href') { continue } let key = take.others[i].left if (take.others[i].directive) { key = ':' + key } let targetInput = '' const targetDom = [] if (take.others[i].type) { let otherRight = take.others[i].right otherRight = otherRight.replace(/\'/g, '\\\'') targetDom.push('\'' + otherRight + '\'') targetDom.push('\'' + fileName + '\'') // targetDom.push(Object.keys(parentParam)) Object.keys(parentParam).forEach(key => { targetDom.push('{' + key + ': ' + key + '}') }) targetInput = 'domEvent(' + targetDom.join(',') + ')' } parseDom[key] = targetInput } } if (take.name === 'reserveText' && take.reserves) { const textOutput = [] for (let i = 0; i < take.reserves.length; i++) { const reserveVal = take.reserves[i] if (reserveVal.type === 'direct') { textOutput.push(reserveVal.textRawValue) } else { const targetDom = [] targetDom.push('\'' + reserveVal.textRawValue + '\'') targetDom.push('\'' + fileName + '\'') // targetDom.push(Object.keys(parentParam)) Object.keys(parentParam).forEach(key => { targetDom.push('{' + key + ': ' + key + '}') }) textOutput.push('{{ domEvent(' + targetDom.join(',') + ') }}') } } parseDom.reserveText = textOutput.join('') } // --各々の作用 if (!parseDom.hasOwnProperty('reserveText')) { const pushOutput = [] for (let i = 0; i < Object.keys(parseDom).length; i++) { const key = Object.keys(parseDom)[i] pushOutput.push(key + '="' + parseDom[key] + '"') } let endBlock = '>' if (!take.open && !take.enClose) { endBlock = '/>' } let startBlock = '<' if (take.enClose) { startBlock = '</' } if (take.closeParams && take.closeParams.length > 0) { take.closeParams.forEach(key => { delete parentParam[key] }) } output.push(startBlock + take.name + ' ' + pushOutput.join(' ') + endBlock) } else { output.push(parseDom.reserveText) } // --parseしてpush if (take.open) { const enCloseTag = {} enCloseTag.name = take.name enCloseTag.enClose = true stack.push(enCloseTag) if (Object.keys(yoyaku).length > 0) { enCloseTag.closeParams = [] Object.keys(yoyaku).forEach(key => { parentParam[key] = yoyaku[key] enCloseTag.closeParams.push(key) }) } if (take.hasOwnProperty('children')) { for (let i = take.children.length - 1; i >= 0; i--) { const childVal = take.children[i] stack.push(childVal) } } } // --子供に対する作用 // --whileEnd } output.push('</div>') return output.join('') }基本的には、userがコーディングした、templateをそのままparseしなおせば良いのですが、
scriptだったり、global(style解析)にアクセスするようにしないといけません。
そのため、各々がもっている@clickや、v-for、classなどの対象としている変数をアクセスできるように、
domEvent
やclassEvent
などに変更する(他にもparseEventや、routerEventなどがあります)ことで、のちにプレビューする時に、$vueに、PreviewField.vuedomEvent: function (order, path, userAction, ...arg) { let toParam = Object.assign({}, global) arg.forEach(x => { toParam = Object.assign(toParam, x) }) const domPro = domProperty(order, toParam) if (userAction) { this.outputDom = domPreviewParse(saveDomTree, path) this.previewParse() } return domPro }, classEvent: function (path, ...orders) { // class名を受け取る let outputObj = {} orders.forEach(key => { outputObj = Object.assign(outputObj, globalStyle[path].class[key]) }) return outputObj }, parseEvent: function (param) { const splits = param.split('[') const gets = splits[0] const index = Number(splits[1].split(']')[0]) if (global[gets]) { return global[gets][index] } else { return false } }, routerEvent: function (param) { this.$emit('router-change', param) },などをわたすことで、globalと、template部分との疎通を測ります。
(userActionなどはこの後後述します。)
実際にこれをプレビュー機能に載せようとすると、一つ問題があります。
基本的には、レンダリングやglobalなどの疎通はできるのですが、v-forの内部に@click
などを設定すると、
与えられている要素(例えば配列をv-forで回していて、その配列の中の一つの要素をclickイベントに一つずつ与えてる処理)に対しては、v-forの内部の要素全てクリックしても、最後の要素(配列の最後)のclickeventが発火されます。(詳しい原因はわかってないのですが、参照渡しでdomが渡されていると仮定すれば、最後の要素が参照渡し的に代入されたとすれば、全ての要素が最後の要素になるのは仕方ないのかなと思います (:key
などありますしね...))そこで、v-for,v-ifやtemplateの動的レンダリングをこちら側で処理することを考えます。
ここで、MainProcessをみてみます。
MainProcess.jswhile (targets.length > 0) { const ifBool = true const getTar = targets.pop() const tar = Object.assign({}, getTar) if (tar.params) { // console.lo('targets!!', tar.params, tar) } tooru = false // -- v-for if (tar['v-for']) { const target = tar['v-for'] if (target.type === 'variable' || target.type === 'function') { let data = domProperty(target.right, tar.params) // とりあえずdataはArray想定 本来ではObjectも考えないといけないよ if (Array.isArray(data)) { for (let i = data.length - 1; i >= 0; i--) { // tar.params = {} let nextTarget = Object.assign({}, tar) const params = {} const textParams = {} const keys = Object.values(target.target) params[keys[0]] = data[i] textParams[keys[0]] = String(keys[0] + '[' + i + ']') console.log('chh', data[i], data, target) if (keys.length === 2) { params[keys[1]] = i textParams[keys[1]] = i } nextTarget.paramIndex = i nextTarget.paramValue = data[i] nextTarget.params = Object.assign({}, params) nextTarget.textParams = Object.assign({}, params) // console.lo('nextTarget', nextTarget) delete nextTarget['v-for'] targets.push(nextTarget) } } else if (typeof data === 'object') { // obj const keys = Object.keys(data) data = Object.values(data) for (let i = data.length - 1; i >= 0; i--) { // tar.params = {} let nextTarget = Object.assign({}, tar) const params = {} const textParams = {} const keys = Object.values(target.target) params[keys[0]] = data[i] console.log('chh', data[i], data) // textParams[key[0]] = if (keys.length === 2) { params[keys[1]] = keys[i] } nextTarget.paramIndex = keys[i] nextTarget.paramValue = data[i] nextTarget.params = Object.assign({}, params) // console.lo('nextTarget', nextTarget) delete nextTarget['v-for'] targets.push(nextTarget) } } continue } else { // func } } // 8-- v-for // v-if if (tar['v-if']) { let data = !!domProperty(tar['v-if'].right, tar.params) if (!data) { continue } } // 8-- v-if parseOutput.push(tar) if (tar.name === 'reserveText') { // console.log('tarValue:none', tar) const output = [] for (let reserve of Object.values(tar.reserves)) { const strValueStart = tar.value.substr(0, reserve.start) const strValueEnd = tar.value.substr(reserve.end + 1, tar.value.length) if (reserve.type === 'function') { const get = domProperty(reserve.textRawValue, tar.params) // とりあえずglobalのみ対応 const args = [] const toStr = String(get) output.push(toStr) } else if (reserve.type === 'variable') { const get = domProperty(reserve.textRawValue, tar.params) let toStr = String(get) // tar.value = strValueStart + toStr + strValueEnd output.push(toStr) // console.log('pppRRR', reserve, tar.value, global) } else if (reserve.type === 'direct') { output.push(reserve.text) } } tar.value = output.join('') } if (option && option.mode === 'answerDOM') { if (option.existString) { if (tar.answer && tar.name === 'reserveText') { // とりあえずexistStringなので.... // console.log('tarValue', tar, targetIndex) if (typeof lastOutput[outputIndex] !== 'string') { lastOutput[outputIndex] = '' } lastOutput[outputIndex] = lastOutput[outputIndex] + tar.value } else if (tar.answer) { // console.log('tarValue:without', tar) } // -- lastPropagate // 子供に伝播 if (tar.name === 'br') { outputIndex++ } if (tar.open) { let closeObject = {} closeObject.open = false closeObject.close = true closeObject.name = tar.name closeObject.unique = tar.unique closeObject.depth = tar.depth targets.push(closeObject) } let pushChildren = tar.children || [] for (let i = pushChildren.length - 1; i >= 0; i--) { const value = pushChildren[i] let nextObject = {} nextObject = Object.assign({}, value) if (tar.hasOwnProperty('params')) { nextObject.params = Object.assign({}, tar.params) } if (tar.hasOwnProperty('textParams')) { nextObject.textParams = Object.assign({}, tar.textParams) } if (tar.hasOwnProperty('paramIndex')) { nextObject.paramIndex = tar.paramIndex } if (tar.hasOwnProperty('answer')) { nextObject.answer = tar.answer } if (tar.name === 'answer') { nextObject.answer = true nextObject.answerIndex = i } // console.log('cheek', nextObject) targets.push(nextObject) // -- lastPrpagate } } } }MainProcess(ジャッジシステムをする場所)では、vueのv-forやv-ifが機能しないので、
こちら側で、処理する必要があるので、その機能を少し拝借します。
v-forに注目すると、単純にfor文で回している以外に、params,textParams,parseParamsと、valueとindexを
ASTの中に忍ばせます。こうすることで、次にdomPreviewParseをみてみると、
domPreviewParse.jsfunction domPreviewParse (domTree, fileName) { const output = [] const parentParam = {} saveDomTree = domTree output.push('<div id="previewDOM">') const runVueCode = runVueDom(domTree) // ループが起きると困るので、userアクション(v-on:clickとか@clickとか)の時に最描画するようにする for (let i = 0; i < runVueCode.length; i++) { const take = runVueCode[i] const parseDom = {} const yoyaku = {} if (take.name === 'router-link') { take.routerPush = true parseDom.routerPush = '@router' } if (take.hasOwnProperty('class')) { let targetDom = [] targetDom.push('\'' + fileName + '\'') if (take.class.hasOwnProperty('variables')) { take.class.variables.forEach(key => { targetDom.push('\'' + key + '\'') }) } parseDom['v-bind:style'] = 'classEvent(' + targetDom.join(',') + ')' } if (take.hasOwnProperty('others')) { // 現状v-forとclassを分けたらいけるか...? for (let i = 0; i < take.others.length; i++) { if (take.others[i].left === 'v-for' || take.others[i].left === 'class' || take.others[i].left === 'href') { continue } let key = take.others[i].left if (take.others[i].directive) { key = ':' + key } let targetInput = '' const targetDom = [] if (take.others[i].type) { let otherRight = take.others[i].right otherRight = otherRight.replace(/\'/g, '\\\'') targetDom.push('\'' + otherRight + '\'') targetDom.push('\'' + fileName + '\'') if (key.indexOf('click') >= 0) { targetDom.push('true') } else { targetDom.push('false') } // targetDom.push(Object.keys(parentParam)) if (take.parseParams) { Object.keys(take.parseParams).forEach(key => { let value = take.parseParams[key] const valueType = typeof value if (valueType !== 'number' && valueType !== 'boolean' && valueType !== 'object' && value && (!value.indexOf('[') > 0 && !value.indexOf(']') > 0)) { value = '\'' + value + '\'' } if (typeof value === 'string' && value.indexOf('[') > 0) { value = 'parseEvent(' + '\'' + value + '\'' + ')' } if (valueType === 'object') { const output = [] let stack = [...Object.keys(value)] // while (stack.length > 0) { // const takeKey = stack.pop() // output.push(takeKey + ': ') // output.push(value[takeKey] + ', ') // } for (let i = 0; i < stack.length; i++) { output.push(stack[i] + ': ') let outVal = value[stack[i]] const typeVal = typeof outVal if (typeof outVal === 'string') { outVal = '\'' + outVal + '\' ' } if (i !== stack.length - 1) { output.push(outVal + ', ') } else { output.push(outVal) } } value = output.join('') value = '\{ ' + value + '\}' } targetDom.push('\{' + key + ': ' + value + '\}') }) } targetInput = 'domEvent(' + targetDom.join(',') + ')' } parseDom[key] = targetInput } } if (take.name === 'reserveText' && take.reserves) { const textOutput = [] for (let i = 0; i < take.reserves.length; i++) { const reserveVal = take.reserves[i] if (reserveVal.type === 'direct') { textOutput.push(reserveVal.textRawValue) } else { const targetDom = [] targetDom.push('\'' + reserveVal.textRawValue + '\'') targetDom.push('\'' + fileName + '\'') targetDom.push('false') if (take.parseParams) { Object.keys(take.parseParams).forEach(key => { let value = take.parseParams[key] const valueType = typeof value if (valueType !== 'number' && valueType !== 'boolean' && valueType !== 'object' && value && (!value.indexOf('[') > 0 && !value.indexOf(']') > 0)) { value = '\'' + value + '\'' } if (typeof value === 'string' && value.indexOf('[') > 0) { value = 'parseEvent(' + '\'' + value + '\'' + ')' } if (valueType === 'object') { const output = [] let stack = [...Object.keys(value)] output.push('{ ') for (let i = 0; i < stack.length; i++) { output.push(stack[i] + ': ') let outVal = value[stack[i]] const typeVal = typeof outVal if (typeof outVal === 'string') { outVal = '\'' + outVal + '\'' } if (i !== stack.length - 1) { output.push(outVal + ', ') } else { output.push(outVal) } } output.push(' }') value = output.join('') } targetDom.push('\{' + key + ': ' + value + '\}') }) } textOutput.push('\{\{ domEvent\(' + targetDom.join(', ') + '\) \}\}') } } parseDom.reserveText = textOutput.join('') } // --各々の作用 if (!parseDom.hasOwnProperty('reserveText')) { const pushOutput = [] for (let i = 0; i < Object.keys(parseDom).length; i++) { const key = Object.keys(parseDom)[i] if (key === 'routerPush') { let toParam = {} if (Object.keys(parseDom).indexOf(':to') >= 0) { toParam = parseDom[':to'] } if (Object.keys(parseDom).indexOf('to') >= 0) { toParam = parseDom['to'] } if (Object.keys(toParam).length == 0) { continue } pushOutput.push('@click' + '="' + 'routerEvent(' + toParam + ')' + '"') continue } pushOutput.push(key + '="' + parseDom[key] + '"') } let endBlock = '>' if (!take.open && !take.close) { endBlock = '/>' } let startBlock = '<' if (take.close) { startBlock = '</' } output.push(startBlock + take.name + ' ' + pushOutput.join(' ') + endBlock) } else { output.push(parseDom.reserveText) } } console.log('output', output) output.push('</div>') return output.join('') }先ほどと違い、parseParamsがあるので、それを引数として渡すことで、確実に値渡しとなり、正常に描画されます。
ただ、v-for,v-ifを扱ってないのでvueと同じ様に動的レンダリングするためには、こちら側でレンダリングをさせるようにしないといけません。
レンダリング自体は、globalの値が変更されたものをMainProcess -> domPreviewParseに潜らせれば、レンダリングするので、それをいつ発火させるかです。
今回の場合は、他からの要因である発火イベントで発火するようにしたい(でないと、制約をつけるかしないとレンダリングループしますからね)ので、(今回の場合は)clickイベントとなるような、@click
やv-on:click
にたいして、clickif (key.indexOf('click') >= 0) { targetDom.push('true') } else { targetDom.push('false') }domEventの第三引数を予約する形にし、ここがtrueの場合処理が終わり次第描画するようにすることで、
元々表現したかった、userのvue.jsコードでの安全な描画ができるようになりました。プレビュー機能
デモ:
Homeに流し込んだモノ
プレビュー機能 pic.twitter.com/WVUvBHPayA
— front engine (@EngineFront) December 6, 2020PreviewField
上記でたいたいの大枠は説明したので、補足として説明いたします。previewParsepreviewParse: function () { const getDDD = this.outputDom const self = this const domEvent = this.domEvent const classEvent = this.classEvent const parseEvent = this.parseEvent const routerEvent = this.routerEvent const testSumple = getDDD bootstrapImports() let newPreviewDom = Vue.component('newPreviewDom', { template: getDDD, methods: { domEvent: domEvent, classEvent: classEvent, parseEvent: parseEvent, routerEvent: routerEvent }, components: { Answer, PreviewCard, AnswerCard, 'router-link': Item, ...importBootstrap } }) let vm = new Vue({ Answer, render: h => h(newPreviewDom) }) // this.pushPreview = newPreviewDom const targetDomChange = document.getElementById(this.uniqueKey).children[0] vm.$mount(targetDomChange) this.$emit('vueDom', vm.$el, vm.$el.children) }色々調べると、vue.jsの再$mountの様な記事はあるのですが、
あまりこれと言った記事はでませんでした。
(例えばvueではなく、vueに生の仮想DOMでない生のDOMを載せるモノや、少し古いモノや今回の意図した挙動ではないものが散見しているように思えます)(多分ですが、userのvue.jsをある程度安全に最描画させるという方針でないと、上記のv-for参照渡し問題や、jsのevalセキュリティヤバい問題と衝突するからなのではというのと、vue.jsはそれだけでリッチなので今回の様なプロダクトを目指さなければ他の機能で代用できることが原因かと思われます。)
なので、私と同じ様な問題に衝突した人がいれば、今回のpreview画面は参考になると思われますし、質問があれば是非答えたいと思います。vue.jsが#appにmountしているように、新たに描画するものにid指定でmountしたいと考えます。
また、今回プロジェクト単位拡張がありますので、idを固定値で指定してしまうと、最初のモノしかレンダリングされないので、propsとして引き受ける様にします。templateには、上記で行ったvue.jsを再現できるコードにparseしたものを載せ、
methodsには、domEvent,classEvent,parseEvent,routerEventを用意し、これらを踏み台として、globalへのアクセスを可能とさせます。
componentsには、ジャッジシステムのためのAnswer,PreviewCard,AnswerCardと、userがプロジェクト単位拡張問題で書くであろう、'router-view'をこちら側のコンポーネントと紐付けます。
そしてさらにbootstrapを扱えた方がなにかと良いと思ったので、全てをコンポーネントとして読み込むことで、(Vue.use機能は作成したjsインタプリターには機能として載せてないので)表現しています。プロジェクト単位拡張
デモ:
Homeに流し込んだソースコード
Exam5Detailに流し込んだソースコード
router設定に流し込んだソースコード
プロジェクト単位拡張 pic.twitter.com/quwAXAVTef
— front engine (@EngineFront) December 6, 2020一週間の開発期間の後、jphacksでオンラインジャッジシステムを作っているtrack様からこの様なFBを頂きました。
個人的には割と好きなんですが、システムが使えるかどうかという観点で見ると、フロントエンドエンジニア向け製品というにはちょっと遠いですかね。システムとしては、Vue.js (のコンポーネント) のレンダリング結果を評価しているように見えます。逆に言えば、今のシステムはそれしかできなさそう。もちろん、そうした評価もフロントエンド開発における自動テストの一部を担ってはいるのですが、一部でしかなく。
具体的には画面操作を行った時の遷移だとかを評価できそうには見えないので、フロントエンドエンジニアの評価って話としてはまだ風呂敷広げすぎかな、と
ユーザの書いたコードをVueのコンパイラに食わせてUnitTestしているだけだと思う。基本的なアイデアはtrackとまるっきり一緒です。frontendの評価システムとして最近よくあるのは画面のスナップショット取って正解画像との比較をするっていうやり方ですね。progateとかもやってるんじゃなかったかな。
unit(単体)テストやら、画面遷移やら出来ないし、この先も出来ないんじゃないの? (意訳())
じゃ作ってやろうと思いプロジェクト単位の問題を作ることを決心しました。
vue-routerの設定が出来る。それらを使い遷移することが出来る
ができれば単体間の相互作用と、遷移が存在することができれば上記のFBは解決できそうだと感じたので、
これをプロジェクト単位拡張と定義します(実際にはこれら以外にもターミナル機能つけたり、ブラウザのようなタブを増やすことができるようにもしました)プロジェクト単位に拡張したので、全てのプロジェクト単位の情報を入れる箱が必要なので、
earthというものを定義します globalはもう使っちゃってたので(ぇProjectProcess.jslet earth = { pages: {}, targetURL: '/', baseURL: 'localhost:8080', router: {}, earchParam: {}, designChecker: {} } // プロジェクト単位pagesには、
ProjectProcess.jsfunction pageAdd (pageName, template, script, style, domTree, pure, global) { earth.pages[pageName] = {} if (template) { earth.pages[pageName].template = template } if (script) { earth.pages[pageName].script = script } if (style) { earth.pages[pageName].style = style } if (pure) { earth.pages[pageName].pure = pure } if (domTree) { earth.pages[pageName].domTree = domTree } if (global) { earth.pages[pageName].global = global } earth.pages[pageName].pageName = pageName if (!earth.pages[pageName].hasOwnProperty('url')) { earth.pages[pageName].url = '' } console.log('done:PageAdd!!', earth) }新たに提出画面に、ページ追加タブを追加し、
template ~ globalまでの、情報を格納できるようにします。
また、のちにrouterからurlが紐付けられると思うので、それを受け入れられる様にurlを空で初期化しときます。targetURLは、現在プロジェクト単位問題で表示しているエンドポイントを格納しときます。
baseURLは、今回はlocalhost:8080で初期化しときますが、一応変更できる様に、変数として持っときます。
次にrouterの設定ですが、
routerProcess.jsのrouterProcessに流し込みます。routerProcessfunction routerProcess (text) { const script = text const routerAst = routerCreateAST(script) const astList = routerAst.program.body const router = {} console.log('astList', astList) const pages = Object.assign({}, earth.pages) const toPages = {} // とりあえず文字列として渡す? Object.keys(pages).forEach(key => { toPages[key] = key }) toPages.Home = 'Home' toPages.ProblemList = 'ProblemList' toPages.baseURL = earth.baseURL for (let i = 0; i < astList.length; i++) { const value = astList[i] console.log('value', value) if (value.type === 'VariableDeclaration') { for (let i = 0; i < value.declarations.length; i++) { let decVal = value.declarations[i] let valInit = decVal.init if (valInit.type === 'NewExpression') { // maybeargument one? valInit = valInit.arguments[0] let childRouter = {} console.log('valInit', valInit, value) for (let i = 0; i < valInit.properties.length; i++) { if (valInit.properties[i].key.name === valInit.properties[i].value.name && valInit.properties[i].value.type === 'Identifier') { // routes? const getRouteKey = valInit.properties[i].key.name childRouter = Object.assign(childRouter, getProperty(valInit.properties[i].value, toPages)) childRouter[getRouteKey] = routerPushFunc(childRouter[getRouteKey]) continue } childRouter[valInit.properties[i].key.name] = getProperty(valInit.properties[i].value, toPages) } console.log('router', childRouter, decVal.id.name) router[decVal.id.name] = childRouter continue } const valRoute = getProperty(valInit, toPages) console.log('decVal.id.name', decVal.id.name, valRoute, decVal) router[decVal.id.name] = valRoute const toRouter = Object.assign({}, router) toPages[decVal.id.name] = toRouter } } else if (value.type === 'ExportDefaultDeclaration') { earth[value.declaration.name] = router[value.declaration.name] if (value.declaration.name === 'router') { // routerの時に特殊な処理 const routerVal = router.router if (routerVal.routes && routerVal.routes.component) { for (let i = 0; i < Object.keys(routerVal.routes.component).length; i++) { const key = Object.keys(routerVal.routes.component)[i] if (earth.pages[key]) { const endpoint = routerVal.routes.component[key].path earth.pages[key].endpoint = endpoint earth.pages[key].url = routerVal.base + endpoint render() } } } outputRouterString = outputRouterInfo() } } } } function routerPushFunc (arg) { const path = {} const name = {} const component = {} let pure = [] pure = [...arg] for (let i = 0; i < arg.length; i++) { const val = arg[i] if (arg[i].hasOwnProperty('name')) { name[val.name] = val } if (arg[i].hasOwnProperty('path')) { path[val.path] = val } if (arg[i].hasOwnProperty('component')) { component[val.component] = val } } return { path: path, name: name, component: component, pure: pure } }今まで同様textベースで貰ったものを、型に落とし込み、
その後、earthのrouterに流し込みます。
また、export default routerのようにexportされる作りになるはずなので、
これが実行されたら、各々の登録されているページにrouterで設定したrouter情報を紐付けます。実際のページ遷移がどのように行われているかというと、
オンラインジャッジする時には、レンダリングはしていないので、こちら側でターゲットとしているページを変更すれば良いだけなので解説しませんが、実際にレンダリングを行っているプレビューがどのように行われているかは下記のようになります。ProblemDetail.vuerouterChange: function (param) { // name か pathか調べる const keys = Object.keys(param) let params = {} if (keys.indexOf('params')) { params.$route = {} params.$route.params = param.params } if (keys.indexOf('name') >= 0 && keys.indexOf('path') >= 0) { console.error('both name and path exist.') return false } else if (keys.indexOf('name') >= 0) { if (earth && earth.router && earth.router.routes && earth.router.routes.name[param.name]) { const routeInfo = earth.router.routes.name[param.name] const componentName = routeInfo.component if (earth.pages[componentName]) { this.sumpleTest(earth.pages[componentName].pure, params) } } } else if (keys.indexOf('path') >= 0) { } }router-linkなどは上記三つを組み合わせて、vue.js単一ファイルとして機能させるで、routerEventが設定されるようになってますので、それで上のコードが発火される様になっています。
上のコードは、まずそれがどのような飛び方をするか調べています。nameで飛ぶのかpathで飛ぶのか調べたのち(ハッカソンなのでnameしか実装してないですが、pathも同様に行えば可能)routerInfoでrouterに格納している情報を取り出し、次に表示するページを準備します。
またvueでのrouterでの遷移は特定のitemを受け渡すことが可能ですので、if (keys.indexOf('params')) { params.$route = {} params.$route.params = param.params }としてparamsを渡すことで、相手page側のglobalへ、あげたい情報を流し込むことができます。(これでthis.$route 系の情報を引っ張ることができるようになります。)
(MainProcess汎用的に作っててよかったです...)これでデモの様に、
ホームに流し込んだソースコードだけで機能するが、
routerとページ追加をして初めて遷移し、$route.paramsの情報を取得できるようになる
を表現することができました。ジャッジシステム/スタイルジャッジシステム
デモ:
Homeに流し込んだモノ
デザインチェック pic.twitter.com/P8409jpxkE
— front engine (@EngineFront) December 6, 2020まず、ジャッジシステムから、みていきます。
最初の一週間で仕上げにいかなければいけなかったので(ファイナリスト選抜があるので)、
文字列の探索のみ実装しています。(それ以降はプレビュー機能やプロジェクト拡張などがあるので、一ヶ月ではこちらに力を割ける時間がなかったです。)MainProcess.jsif (option && option.mode === 'answerDOM') { if (option.existString) { if (tar.answer && tar.name === 'reserveText') { // とりあえずexistStringなので.... // console.log('tarValue', tar, targetIndex) if (typeof lastOutput[outputIndex] !== 'string') { lastOutput[outputIndex] = '' } lastOutput[outputIndex] = lastOutput[outputIndex] + tar.value } else if (tar.answer) { // console.log('tarValue:without', tar) } // -- lastPropagate // 子供に伝播 if (tar.name === 'br') { outputIndex++ } if (tar.open) { let closeObject = {} closeObject.open = false closeObject.close = true closeObject.name = tar.name closeObject.unique = tar.unique closeObject.depth = tar.depth targets.push(closeObject) } let pushChildren = tar.children || [] for (let i = pushChildren.length - 1; i >= 0; i--) { const value = pushChildren[i] let nextObject = {} nextObject = Object.assign({}, value) if (tar.hasOwnProperty('params')) { nextObject.params = Object.assign({}, tar.params) } if (tar.hasOwnProperty('textParams')) { nextObject.textParams = Object.assign({}, tar.textParams) } if (tar.hasOwnProperty('paramIndex')) { nextObject.paramIndex = tar.paramIndex } if (tar.hasOwnProperty('answer')) { nextObject.answer = tar.answer } if (tar.name === 'answer') { nextObject.answer = true nextObject.answerIndex = i } // console.log('cheek', nextObject) targets.push(nextObject) // -- lastPrpagate } } } }domとして、answerで挟まれているものを見つけ、
それらに対して文字列を見つけます。また正解のものが配列の場合、
があることで、
次のindexのものに進んで見つけます。
このようにして、複数のテストケースに対して正答しているかを調べます。次にデザインチェックに対してみていきます。
emitDomemitDom: function () { // console.log('previewDom', value, value.children, value.children[0]) const value = this.checkStyleDom console.log('previewDom:func', value.children[0], value.children[0].children[1].children[0].getBoundingClientRect(), value.children[0].getBoundingClientRect(), [value.children[0]]) console.log('preview:style', value.children[0].children[0].children[0].getBoundingClientRect(), value.children[0].children[1].children[0].getBoundingClientRect(), value.children[0].children[2].children[0].getBoundingClientRect()) let targetStyle = this.getExam.examInfo let targetBool = true if (targetStyle && targetStyle.option && targetStyle.option.styleCheck) { targetStyle = targetStyle.option.styleCheck } else { this.checked = true this.clickFlug = true return true } if (!targetStyle.hasOwnProperty('children')) { // bugでroot層だけchildrenがないパターン(必要なのに)ないパターンがある targetStyle.children = {} Object.keys(targetStyle).forEach(key => { if (key !== 'count' && key !== 'style' && key !== 'children') { targetStyle.children[key] = targetStyle[key] } }) } let que = [targetStyle] let domQue = [value.children[0]] while (que.length > 0) { // 正答判定 let take = que.shift() let countDomTake = [] if (take.count > 0) { for (let i = 0; i < take.count; i++) { countDomTake.push(domQue.shift()) } } console.log('ccck', take, countDomTake) const diffStyleCheck = {} const diffStyles = [] let NextChild = countDomTake[0] for (let i = 0; i < countDomTake.length; i++) { let domTake = countDomTake[i] let domStyle = domTake.getBoundingClientRect() let domRawStyle = countDomTake[i].style if (!take.hasOwnProperty('name')) { // noname } else { // nameつき if (take.name === 'AnswerCard') { domTake = countDomTake[i].children[0] NextChild = countDomTake[0].children[0] } } console.log('countDomTake', domTake, domStyle) diffStyles.push(domStyle) if (take.hasOwnProperty('style')) { for (let parentKey of Object.keys(take.style)) { // _区切りでor判定とする console.log('take.style', parentKey, take.style) const splitKeys = parentKey.split('_') let splitBool = [] for (let i = 0; i < splitKeys.length; i++) { const key = splitKeys[i] for (let subKey of Object.keys(take.style[key])) { console.log('subKey', subKey, domStyle, key) if (subKey === 'max' || subKey === 'min') { // 幅指定 if (subKey.match('max')) { // minの時だけ判定 continue } if (domStyle[key]) { // 他に依存しない if (take.style[key].min <= domStyle[key] && domStyle[key] <= take.style[key].max) { continue } else { splitBool.push(false) } } else { // 他要素と依存関係にあるstylecheck diffStyleCheck[parentKey] = true } } else if (!(subKey === domRawStyle[key])) { // absolute指定 if (key === 'overflow') { // 例外処理 console.log('overflow', key) const upperSubKey = subKey.toUpperCase() if (domRawStyle[key + upperSubKey]) { console.log('overflow', domRawStyle[key + upperSubKey]) } else { console.log('absolute指定:アウト', subKey, domRawStyle[key], [domRawStyle], [countDomTake[i]]) splitBool.push(false) this.checkData.reason = "absolute指定:アウト" this.clickFlug = true this.reason = "absolute指定:アウト" } } else { console.log('absolute指定:アウト', subKey, domRawStyle[key], [domRawStyle], [countDomTake[i]]) splitBool.push(false) this.checkData.reason = "absolute指定:アウト" this.clickFlug = true this.reason = "absolute指定:アウト" } } else { // trueをいれとく splitBool.push(true) } } let continueBool = false for (let take of splitBool) { if (take) { continueBool = true break } } if (continueBool || splitBool.length == 0) { continue } // false this.checked = false console.log('style:False', splitBool, take, [domTake]) this.clickFlug = true return false } } } } if (diffStyles.length > 0) { let xDiffs = [...diffStyles] let yDiffs = [...diffStyles] for (let i = 0; i < xDiffs.length; i++) { xDiffs[i].index = i yDiffs[i].index = i } xDiffs.sort((a, b) => a.x - b.x) yDiffs.sort((a, b) => a.y - b.y) for (let i = 1; i < xDiffs.length; i++) { const xDiff = xDiffs[i].x - (xDiffs[i - 1].x + xDiffs[i - 1].width) const yDiff = yDiffs[i].y - (yDiffs[i - 1].y + yDiffs[i - 1].height) xDiffs[i - 1].xDiffRight = xDiff // 右側との差 xDiffs[i].xDiffLeft = xDiff // 左側との差 yDiffs[i - 1].yDiffBottom = yDiff // 下側との差 yDiffs[i].yDiffTop = yDiff // 上側との差 } let orders = Object.keys(diffStyleCheck) console.log('orders', orders, diffStyles, countDomTake) for (let order of orders) { let splitOrders = order.split('_') const splitBool = [] console.log('order', order, xDiffs) for (let key of splitOrders) { const max = take.style[order].max const min = take.style[order].min console.log('checcker', min, max, key) switch (key) { case 'padding': case 'margin': // とりあえずこれらをまとめてお互いの距離感として処理する // とりあえず左右だけ見るようにする -> 縦軸も一応取得してるから、見たい時は違う命令で let marginCheck = true console.log('paddingOrMargin', xDiffs, key) for (let i = 0; i < xDiffs.length; i++) { console.log('xDiffs', xDiffs[i], xDiffs[i].xDiffLeft) if (xDiffs[i].xDiffLeft || typeof xDiffs[i].xDiffLeft === 'number') { console.log('xDiffLeft', xDiffs[i]) if (!(min <= xDiffs[i].xDiffLeft && xDiffs[i].xDiffLeft <= max)) { console.log('style:DiffFalseLeft', key, xDiffs[i], min, max, xDiffs[i].xDiffLeft) marginCheck = false break } } if (xDiffs[i].xDiffRight || typeof xDiffs[i].xDiffRight === 'number') { console.log('xDiffRight', xDiffs[i]) if (!(min <= xDiffs[i].xDiffRight && xDiffs[i].xDiffRight <= max)) { console.log('style:DiffFalseRight', key, xDiffs[i], min, max, xDiffs[i].xDiffRight) marginCheck = false break } } } splitBool.push(marginCheck) break } } let checkSplitBool = false splitBool.forEach(flag => { if (flag) { checkSplitBool = true } }) if (!checkSplitBool && splitBool.length > 0) { this.checked = false this.clickFlug = true return false } } } if (NextChild && NextChild.children) { domQue.push(...NextChild.children) } else { } if (take.hasOwnProperty('children')) { console.log('take.children', take.children) que.push(...Object.values(take.children)) } } for (let child of value.children[0].children) { console.log('previewDom:dom', child.children[0], child.children[0].getBoundingClientRect()) } this.previewDom = value this.checkFlug = true this.clickFlug = true },デザインチェックボタンでイベント発火し、emitDomが呼ばれます。
ここでは、その問題に紐付けられたスタイルチェック用の情報と照らし合わせにいきますが、
ここでみている点は3つです。・一つはそれが固有に持たなければ不正解になる要素
(overlayやdisplayなど)
にたいして、そのdomをみて、持っているかどうかを判定します(設定されたものをみるのではなく、レンダリングされたものをみます)・次に他に依存しないサイズ指定があるもの
(widthやheight)に対して、それがある指定されているサイズに対して、クリアしているかどうかをみます
(例えばwidthやheightの場合、データベースには minとmaxが設定されています。その間に要素のサイズが入ってるかどうかをみます)・次に他に依存するもの
(paddingやmargin)などに対して、それらの間の距離をみて判断します。
v-forなどで連鎖している要素に対しての、差の情報がminとmaxで入っているので
レンダリングされた情報から取得します。padding-margincase 'padding': case 'margin': // とりあえずこれらをまとめてお互いの距離感として処理する // とりあえず左右だけ見るようにする -> 縦軸も一応取得してるから、見たい時は違う命令で let marginCheck = true console.log('paddingOrMargin', xDiffs, key) for (let i = 0; i < xDiffs.length; i++) { console.log('xDiffs', xDiffs[i], xDiffs[i].xDiffLeft) if (xDiffs[i].xDiffLeft || typeof xDiffs[i].xDiffLeft === 'number') { console.log('xDiffLeft', xDiffs[i]) if (!(min <= xDiffs[i].xDiffLeft && xDiffs[i].xDiffLeft <= max)) { console.log('style:DiffFalseLeft', key, xDiffs[i], min, max, xDiffs[i].xDiffLeft) marginCheck = false break } } if (xDiffs[i].xDiffRight || typeof xDiffs[i].xDiffRight === 'number') { console.log('xDiffRight', xDiffs[i]) if (!(min <= xDiffs[i].xDiffRight && xDiffs[i].xDiffRight <= max)) { console.log('style:DiffFalseRight', key, xDiffs[i], min, max, xDiffs[i].xDiffRight) marginCheck = false break } } } splitBool.push(marginCheck) break }jphacksの審査員や、チームメンバーすらも
これらの情報の取得がレンダリング後の結果ではなく、
単純にcssをみていると勘違いしていたので、ここだけは強く主張したいです()
(まあ普通に考えたら、cssの方が楽だからそっちで実装したとおもわれるよね..)後書き
作成したサービスに対してはある程度説明できたので、ここからは
ハッカソンとしての反省点などを書こうと思います。
また、僕が作ったもの以外もターミナル機能や、マイページ機能、レーティング機能など素晴らしいものがあるので、チームメンバーがそれらに対する記事などを書いてもらったら載せようかと思います。また再度になりますが、リアクションもらえたり質問をもらえたりすると嬉しいのでLGTMや質問ガンガンもらえたら嬉しいです。よろしくお願いします。
反省点
まず僕が担当した部分担当していない部分を総括しても、よく完成したプロダクトだと思いますし、
技術力という点に対してもhackdayを取ったサービスや、そのほかの賞を受賞したプロダクトとひけをとらないと思います。
ただ確かな反省点は二点ほどあります。ほかのプロダクトと比較して華がない
製品としてほかのものは最新技術を看板にしたものや、
プレゼン込みでプレゼンが華やかになるように製品を作って行った様に思えます。
僕たちのプロダクトはプレゼンで華やかにするのは現状難しいものだと思うので、もう少し上記の様なソースコードの解説や中身をみなくても、わかるようなものをAwardDayで仕上げるべきだったと思います。jphacksスポンサー企業との接点がない
僕たちの製品はファイナリストに出場したのですが、その前のHackDay(開発期間一週間でプレゼンする)では、
賞などにかすりもせず、ファイナリストに出場する形となりました。
ファイナリストの出場条件はコード審査(jphacks側での)なので通ったかと思います。
ただ、AwardDayでは、コード審査はなく、プレゼン審査のみとなります。そうなるとスポンサー企業の企業賞というのは効力をもつようになります。
コード審査の場合はjphacks側のみしかみてないので、ファイナリストを他との兼ね合いを考えずにできると思いますがプレゼン審査なので、ある程度他が審議したものの評価もいれないといけません。
そうなると、複数の特定企業に刺さるプロダクトを目指す方向性に、HackDayが終了し次第、うつるべきだったと今では感じます。
僕たちは、貰ったFBを反映する形にしたのですがよくよく考えたら、track開発チーム様は審査員ではないので、そちらのFBを反映するよりも、もう少しペルソナやターゲット層(今回はスポンサー様)に向けるべきだったなと感じます。
ハッカソンというのは、ビジネスソン(isuconとかと違いプロダクトを作りそれを評価するというファジィなものなので)よりな背景もあって然るべきとも思うので、今回は上記のプロダクトとしての華や、ターゲット層の誤りなどビジネス的な観点が今回の大きな敗因だと思います。
- 投稿日:2020-12-07T00:32:18+09:00
Nuxt.js+Vuetifyで新規作成したPWA対応済みWebアプリをAmplify Consoleで公開
Nuxt.js+Vuetifyで新規作成したPWA対応済みWebアプリをAmplify Consoleで公開
この記事はサーバーレスWebアプリ『にゃーにゃーマップ』を開発して得た知見を振り返り定着させるためのハンズオン記事の1つです。
はじめに
にゃーにゃーマップのシリーズ記事1発目です。
まずはWebアプリのプロジェクトを新規作成し、それをそのままAmplify Consoleを利用して公開するところまでを書きます。
VuetifyというUIフレームワークを利用して楽にマテリアルデザインに沿ったUIを手に入れたり、PWAのための設定も行います。コンテンツ
Nuxt.jsプロジェクト新規作成
今回パッケージマネージャは
yarn
を利用します。インストールされていない場合はまずインストールしましょう。bash: yarn: command not found $ npm install -g yarn $ yarn -v 1.22.10Nuxt.jsのWebアプリを新規作成するために
yarn create nuxt-app <project-name>
というコマンドを実行するのですが、
いくつかの質問に答えていくだけで、ベースとなるWebアプリの作成は完了してしまいます。
今回作成するプロジェクトの名前はsample-map-webapp
とします。$ yarn create nuxt-app sample-map-webapp yarn create v1.22.10 [1/4] Resolving packages... warning create-nuxt-app > sao > micromatch > snapdragon > source-map-resolve > resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated warning create-nuxt-app > sao > micromatch > snapdragon > source-map-resolve > urix@0.1.0: Please see https://github.com/lydell/urix#deprecated [2/4] Fetching packages... [3/4] Linking dependencies... [4/4] Building fresh packages... success Installed "create-nuxt-app@3.4.0" with binaries: - create-nuxt-app create-nuxt-app v3.4.0 ✨ Generating Nuxt.js project in sample-map-webapp ? Project name: sample-map-webapp ? Programming language: JavaScript ? Package manager: Yarn ? UI framework: Vuetify.js ? Nuxt.js modules: Axios, Progressive Web App (PWA) ? Linting tools: ESLint, Prettier, StyleLint ? Testing framework: None ? Rendering mode: Single Page App ? Deployment target: Static (Static/JAMStack hosting) ? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Continuous integration: None ? Version control system: Git yarn run v1.22.10 $ eslint --ext .js,.vue --ignore-path .gitignore . --fix Done in 4.21s. yarn run v1.22.10 $ stylelint **/*.{vue,css} --ignore-path .gitignore --fix Done in 1.11s. ? Successfully created project sample-map-webapp To get started: cd sample-map-webapp yarn dev To build & start for production: cd sample-map-webapp yarn build yarn start Done in 67.23s.
? UI framework
でVuetify.js
を選択したり、
? Nuxt.js modules
でProgressive Web App (PWA)
を選択するだけで、
今回の目的をおおよそ達成できてしまいます。記事にするのも憚られるほどのお手軽さですね。作成したWebアプリの実行
作成されたプロジェクトフォルダに移動して
yarn dev
コマンドを実行することで、開発用のWebサーバーを起動して動作させることができます。$ cd sample-map-webapp $ yarn dev yarn run v1.22.10 $ nuxt ℹ NuxtJS collects completely anonymous data about usage. 06:10:16 This will help us improving Nuxt developer experience over the time. Read more on https://git.io/nuxt-telemetry ? Are you interested in participation? Yes ╭───────────────────────────────────────╮ │ │ │ Nuxt @ v2.14.9 │ │ │ │ ▸ Environment: development │ │ ▸ Rendering: client-side │ │ ▸ Target: static │ │ │ │ Listening: http://localhost:8080/ │ │ │ ╰───────────────────────────────────────╯ ℹ Preparing project for development 06:11:33 ℹ Initial build may take a while 06:11:33 ✔ Builder initialized 06:11:33 ✔ Nuxt files generated 06:11:33 ✔ Client Compiled successfully in 13.98s ℹ Waiting for file changes 06:11:50 ℹ Memory usage: 433 MB (RSS: 565 MB) 06:11:50 ℹ Listening on: http://localhost:8080/ 06:11:50こちらが実行されたWebアプリです。
何も実装していないのに、2つのページやヘッダー、フッター、ハンバーガーメニューなどが既に実装されてます。
Vuetifyのおかげで見栄えも既にいい感じです。Amplify Consoleを利用して公開する
新規作成したWebアプリを何も手を加えずこのまま公開します。
GitHubリポジトリの作成とプッシュ
そのためにまずは、GitHubなどにリポジトリを作成して先ほど作成したプロジェクトをプッシュしておきます。
ブランチはmain
とは別にdevelop
も作っておきましょう。せっかくなので、開発環境と本番環境を分けて構築するためです。Amplify Console に新規アプリ作成
AWS Console > AWS Amplify > New app ボタン > Host web app
From your existing code で 利用しているコード管理システムを選択する。ここではGitHubを選択して進めます。
目的のブランチ(ここではmain
)を選択して次へ。
続いてビルド設定の構成。本当はここでビルドの設定を編集する必要があるのですが、ひとまずこのまま次へ進めます。
最後の確認画面。保存してデプロイしましょう。
プロビジョン、ビルド、デプロイ、検証、が順に進んでいく様子を確認できる画面へ遷移します。
デプロイや検証が完了したら、赤枠で囲った場所のリンクをクリックすると、デプロイされたWebアプリが表示されます。
が、なんかうまく表示できてません。Welcome
Your app will appear here once you complete your first deployment.っていう画面が表示されてます。先ほど
yarn dev
で表示した画面と違いますよね。
先ほど「ビルドの設定を編集する必要があるのですが」と書きましたが、それを設定してちゃんと表示されることを確認しましょう。
左側のメニューからアプリの設定 > ビルドの設定
を選択して ビルド設定の追加にある編集ボタンを押下します。version: 1 frontend: phases: preBuild: commands: - yarn install build: commands: - yarn run build artifacts: # IMPORTANT - Please verify your build output directory baseDirectory: / files: - '**/*' cache: paths: - node_modules/**/*これを、
version: 1 frontend: phases: preBuild: commands: - yarn install build: commands: - yarn run generate artifacts: # IMPORTANT - Please verify your build output directory baseDirectory: /dist files: - '**/*' cache: paths: - node_modules/**/*こうする。差分はこちら。
build commands をyarn run build
からyarn run generate
へ変更するのと、bseDirectoryを/
から/dist
へ変更しています。この設定変更をしてから再デプロイし、改めて先ほどのリンクを表示してみてください。
ちゃんと期待した画面が表示されましたね!めでたい!本番環境とは別に開発環境を作る
今回、mainブランチに対してデプロイをしました。本番環境です。
mainブランチとは別にdevelopブランチを作成しましたので、そちらを開発環境としてデプロイしましょう。違いが分かるようにdevelopブランチに「開発環境です」みたいな文字が表示されるようにしてコミット&プッシュしておくと分かりやすくていいです。
すべてのアプリ > sample-map-webapp にある「ブランチの接続」ボタンを押下。
リポジトリブランチの追加でdevelopブランチを選択します。
保存してデプロイを押下すると、
main(本番環境)とは別にdevelop(開発環境)がデプロイされました。
デプロイが済んだらdevelop側のリンクへアクセスして確認してみてください。
お手軽にブランチ毎の環境が用意できていい感じですね。今回は予めブランチを指定して環境を構築しておく形でやりましたが、新しいブランチが作成されたら自動的に環境を構築してくれるような設定があったりもするようです。チーム開発でマージ前の動作確認がしやすくなっていいですね。さすがAmplify Console!
独自ドメインやサブドメインも簡単に割り当てられます。それについては過去に書いたこちらの記事をご案内。
お名前.comで取得した独自ドメインのサブドメインをAmplify Consoleで割り当てる
お名前.comで取得した独自ドメインのサブドメインだけでなくルートドメインも割り当てるPWA対応
Nuxt.jsの新規プロジェクト作成時に
? Nuxt.js modules
でProgressive Web App (PWA)
を選択しました。
するとなんという事でしょう。すでにPWA対応が済んでいるではありませんか。
先ほどデプロイしたサイトへスマホ(Android)でアクセスして、数回画面更新してみてください。
ほら。名前や色などは、
nuxt.config.js
のmanifest
タグで指定できます。
アイコンはstatic/icon.png
を更新すれば変えられます。nuxt.config.js: // Modules (https://go.nuxtjs.dev/config-modules) modules: [ // https://go.nuxtjs.dev/axios '@nuxtjs/axios', // https://go.nuxtjs.dev/pwa '@nuxtjs/pwa', ], manifest: { name: 'サンプルマップWebアプリ', description: 'マップベースWebアプリのサンプルです。', theme_color: '#ff00ff', background_color: '#0000ff', display: 'standalone', Scope: '/', start_url: '/', splash_pages: null }, :あとがき
『モザイク』ではVue CLIでcreateしたプロジェクトをベースに作成したのですが、それに比べてNuxt.jsのお手軽さといったら、、。
当時時間をかけて対応したり解決した様々な事柄(こちらの記事やこちらの記事)が、当然のように最初から備わっていて、もう戻れない。
そしてAmplify Consoleも、本当に、、便利すぎて、、、もう戻りたくない!
- 投稿日:2020-12-07T00:03:45+09:00
新しいマシンにVue.js Reactの環境をインストールする方法
自分用に新しいマシンにVue.jsやReactをインストールする方法をメモとして残す
マニュアルインストールの方法は別にまとめる(予定)Macに『HomeBrew』をインストールする
- 何故インストールするのか?
- CUIベースで環境構築がしやすくなる
- 『$ brew install xxx』でCUIツールをインストールできる
- 『$ brew cask install xxx』でGUIツールをインストールできる
HomeBrewインストール手順
- 1. HomeBrewのHPを開く
- 2. Terminalを開く
- 3. インストールコマンドを実行する
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"brewコマンドでMacの開発環境を構築していく
- Google Chrome
$ brew cask install google-chrome
- VSCode
$ brew cask install visual-studio-code
- IntelliJ
- IntelliJのHPを開く (Ultimate版は有料なので、適宜検討する)
https://www.jetbrains.com/ja-jp/idea/download/#section=macNode.jsをインストールする
- 1. Terminalを開く
- 2. 下記コマンドを実行する (この時は最新のnodeのバージョンがインストールされる)
$ brew install node
- 3. インストールされたNodeのバージョンを確認する
$ node -v ex). v12.14.1
- 4. npmコマンドが実行できるか確認する (試しにバージョン情報を確認する)
$ npm -v ex). 6.14.8
- 5. nodeのバージョン管理コマンドをインストールする (今回は『n』を使用する)
$ npm install -g n
- 6. nコマンドが実行できるか確認する (試しにバージョン情報を確認する)
$ n -V ex). 6.7.0nについて
- Nodeのバージョンを管理することができる
- Nodeのバージョンを確認する
- https://nodejs.org/ja/download/releases/
- nodeの最新バージョンをインストールする
$ n latest
- インストール済みのNodeのバージョンを確認する
$ n ls
- Nodeのバージョンを切り替える
- 実行後にインストール済みのNodeのバージョンが表示されるので変更したいバージョンを選択する
$ nVue.jsの環境構築方法
vue/cliコマンドを実行できるようにする
- 1. @vue/cliのHPを開く
- 2. Terminalを開く
- 3. @vue/cliをインストールする
$ npm install -g @vue/cli
- ※1 バージョンを指定してインストールする場合は『@』をつける (Version 4.5.7をインストールする場合)
$ npm install -g @vue/cli@4.5.7
- ※2 updateの仕方 (updateするときもinstallを実行することにより最新バージョンを取得できる)
$ npm install -g @vue/cli
- インストール済みの@vue/cliのバージョンを確認する
$ npm -g ls | grep @vue/cli ex). @vue/cli@4.5.7Vueのプロジェクトを作成する
- 1. Terminalを開く
- 2. プロジェクトを新規に作成する (今回はプロジェクト名を『sample』とする)
$ vue create sampleReactの環境構築方法
create-react-appコマンドを実行できるようにする
- 1. create-react-appのHPを開く
- 2. Terminalを開く
- 3. create-react-appをインストールする
$ npm install -g create-react-app
- ※1 バージョンを指定してインストールする場合は『@』をつける (Version 3.4.1をインストール)
$ npm install -g create-react-app@3.4.1
- ※2 updateの仕方 (updateするときもinstallを実行することにより最新バージョンを取得できる)
$ npm install -g create-react-app
- 4. インストール済みのcreate-react-appのバージョンを確認する
$ npm -g ls | grep create-react-app ex). create-react-app@3.4.1Reactのプロジェクトを作成する
- 1. Terminalを開く
- 2. プロジェクトを新規に作成する (今回はプロジェクト名を『sample』とする)
$ create-react-app sample
- 投稿日:2020-12-07T00:01:19+09:00
TypeScript のユニオン型で様々な状態のオブジェクトを一緒くたに扱うには
この記事について
株式会社Re:Buildアドベントカレンダー Advent Calendar 2020 の 7日目の記事です。
業務委託という立場ではありますが、書いていいよ、と許可をいただいたので、参加させていただきました。
当社の技術スタックとしては Laravel + Nuxt.js (TypeScript) が多く、今年はたくさん TypeScript を書きましたが、本記事では、その中で少し大変だった、ユニオン型を使って複数の異なる型を一緒くたに扱うためにどうしたか、というのをかいつまんで解説します。
具体的には、概念的には同じ型ですが、状態によってプロパティがなかったり追加になったりして、構造的には異なる型のオブジェクトをユニオン型を使って同列に扱えるようにします。
はじめに
ユニオン型とは
https://typescript-jp.gitbook.io/deep-dive/type-system#yunionunion-type
型を
|
でつないで、関数の引数などでいずれの型でも受け取れるようにするものです。お題
記事(Post)のプロパティを同一のページ上で、編集・追加・削除できる UI になっていて、いずれの状態のオブジェクトも同じ型で扱いたい。具体的には、追加されたオブジェクトには id がない、削除されたオブジェクトには
isDeleting
という論理値のプロパティがある、という違いがあります。これらをリストにして保存用 API に送ってまとめて更新する、という流れです。基本の Post 型
declare module Response { interface Post { id: number title: string author: string } // 他にもたくさんあるけど省略 }API をモックした関数
const fetchPosts = (): Promise<Response.Post[]> => { return new Promise((resolve) => { const posts: Response.Post[] = [ { id: 1, title: '記事1', author: 'nunulk' }, { id: 2, title: '記事2', author: 'nunulk' }, { id: 3, title: '記事3', author: 'nunulk' }, ] resolve(posts) }) }リクエストボディのイメージ
[ { "id": 1, "title": "記事1", "author": "nunulk" }, { "id": 2, "title": "削除された記事", "author": "nunulk", "isDeleting": true }, { "title": "新規作成された記事", "author": "nunulk" }, ]ユニオン型で様々な状態のオブジェクトを一緒くたに扱う
前述のとおり、同一画面上で、編集・追加・削除を行い、それらすべてを同一の型として扱えるようにします。
// 既存のデータ type EditingPost = Response.Post // 追加された(id が存在しないか、存在しても null である) type CreatingPost = Omit<EditingPost, 'id'> & { id?: null } // 削除された(isDeleting というプロパティが true である) type DeletingPost = EditingPost & { isDeleting: true } type Post = EditingPost | CreatingPost | DeletingPostコンポーネント定義と外部インタフェースはこんなかんじです。
export default Vue.extend({ data: () => ({ posts: [] as Post[], }), async created () { // 既存のデータを取得する this.posts = await fetchPosts() }, methods: { onSubmit () { // 保存用の API を呼び出す savePosts(this.posts) }, }, })各操作と配列要素の置き換え
新規作成
新規作成の場合は、単純にオブジェクトを
Array.push()
すればいいです(プロパティ名の間違いなどあればコンパイルエラーになります)。/* createForm: { title: '' as string, author: '' as string, }, */ this.posts.push(this.createForm)削除
削除の場合はちょっと複雑で、新規作成されたものがサーバに保存される前に削除された場合はそのまま
this.posts
から消してしまいます。Vue.set に型情報を渡してやることで第3引数の型アサーションができるようになります。// index は削除される要素の添字 const post = this.posts[index] if (post.id) { this.$set<DeletingPost>(this.posts, index, { ...post, isDeleting: true }) } else { this.posts.splice(index, 1) }編集
編集の場合は、既存の posts を直接変えても、別で data に定義してもいいですが、型は変わらないので省略します。
各状態を型ガードで識別する
配列の各要素がどの状態(型)であるかを識別したいシチュエーションがあるかもしれません。その場合は、型ガードを使って識別します。
const isCreating = (post: Post): post is CreatingPost => typeof post.id !== 'number' const isDeleting = (post: Post): post is DeletingPost => 'isDeleting' in post && post.isDeleting === true const isEditing = (post: Post): post is EditingPost => !isCreating(post) && !isDeleting(post) this.posts.forEach((post: Post) => { if (isCreating(post)) { console.log('creating', post) } else if (isDeleting(post)) { console.log('deleting', post) } else if (isEditing(post)) { console.log('editing', post) } })おわりに
いかがでしたでしょうか。
- ベースとなる型から Omit や 交差型を使って異なる状態の型を導出する
- それらをユニオン型で統合する
- 型ガードを使って型を判別する
といった方法で、様々な状態のオブジェクトを一緒くたに扱う方法をご紹介しました。
コードに間違いなどあればコメント欄にてご指摘いただけると助かります。