- 投稿日:2022-04-02T22:18:08+09:00
VueからWebAssembly(seed使用)を利用してみる。
この記事について 発端は、Rustを勉強する為に、ルービックキューブアプリをRust+WebAssemblyで作成し始めた事になります。 それから、勉強した事を記事にまとめつつ今に至ります。 Rust + seed でWebAssemblyを体験してみる。 Rustの参照渡しを使いこなすためにその1 Rustの参照渡しを使いこなすためにその2-Vecとfor- javaScriptとWebAssembly(seed使用)を相互連携してみる。 今回の記事では、Vueからwasmモジュールを使用してみます。なるべくストレスかけずに開発する構成も考えてみました。 目標 今回のゴールは以下のポイントです。 wasmモジュールを組み込んだVueのコンポーネントを作成する VueのUIコンポーネントからそのwasm使用のVueコンポーネントを使用する ホットデプロイを基本とする開発時構成にする 結果 先に結果です。今までseed(WebAssemblyフレームワーク)側でボタンを配置していたのを、Vue側に移動出来ました。まだまだUI的な改善点はありますが、VueのUIを使用して大分まともになりました。右側の2つのルービックキューブ描画部分(canvas要素)がseedで構築され、左側のボタン群がVueで構築されています。 参考: 前回までの画像 wasmモジュールを扱う構成 静的モジュールを使用したデプロイ時にはあまり問題にならないと思いますが、開発時には以下の(自分の好みとしての)考慮ポイントがあります。 Vueを開発モードで起動する時、wasmモジュールを読み込めるようにする必要がある wasmモジュールはVue側のリソースではなく、外部リソースとして扱っておきたい あまりローカル環境を汚したくない ここでは、docker-composeとnginxを使って構築してみます。 構成図イメージ フォルダ構成概要 nginx(directory) nginx.conf Dockerfile-nginx vue(directory) cubetrain(vueのプロジェクトdirectory) Dockerfile-vue wasm(seedプロジェクトdirectory) (各種ファイル) docker-compose.yml 主要ファイル詳細 docker-compose.yml version: '3.7' services: nginx: build: context: ./nginx dockerfile: Dockerfile-nginx ports: - 8880:80 volumes: - ./log/nginx/:/var/log/nginx/ - ./nginx/nginx.conf:/etc/nginx/nginx.conf - ./nginx/wasm:/wasm - ./nginx/vuedist:/vuedist depends_on: - vuefront container_name: cubetrain-nginx vuefront: build: context: ./vue dockerfile: Dockerfile-vue ports: - 8881:8080 volumes: - ./vue:/vue container_name: cubetrain-vuefront tty: true nginx.conf 主要部分抽出 # vue側のサーバーを定義しておきます upstream vuefront { server vuefront:8080; } include /etc/nginx/sites-enabled/*; server { listen 80 default_server; server_name localhost; charset utf-8; # wasmパスへアクセスがあった時、nginxコンテナの/wasmフォルダへつなぎます location /wasm { root /; } # vueのビルド成果物を使用する時は、nginxコンテナの/vuedistフォルダへつなぎます # location / { # root /vuedist; # } # wasm以外のパスの時はそのままvueの開発モードサービスへつなぎます location / { proxy_pass http://vuefront/; } } nginxとvueのDockerfileは特筆する事なく、ベースは以下の通りです。 Dockerfile-nginx FROM nginx Dockerfile-vue FROM node:17.8 WORKDIR /vue wasmモジュール作成 今回のポイントであるインターフェース部を構築します。前回の記事「javaScriptとWebAssembly(seed使用)を相互連携してみる。」を基本にします。既にseedの開発が終わり、インターフェース部分のみを変える事を想定します。 Cargo.toml lib.rsで使用するモジュールを追記しておきます。 [dependencies] enclose = "1.1.8" # 追記 lib.rs start関数はjavaScript側で明示的に実行するので、wasm_bindgen(start)になっているのをwasm_bindgenと指定します。 start関数で、javaScript側で呼びだす関数のインターフェースを返しておきます。複数のパラメーターを受け取る関数を作りたい所でしたが、自分の今の知識では、seedのサンプルにあったラッピング関数を改造する事が出来ませんでした。スペース区切りの単一文字列として受け取ったものを分割して使用する事にします。 set_config、rotateという二つの関数を用意します。設定変更用と回転処理用です。 また、start関数に引数を一つ追加します。wasmを結び付けるDOM要素のidを受け取り(引数名:targetid)、それを使用してApp::startを呼び出します。今まではappと固定値だった部分です。 #[wasm_bindgen] pub fn start(targetid: &str) -> Box<[JsValue]> { // Mount the `app` to the element with the `id` "app". let app = App::start(targetid, init, update, view); create_closures_for_js(&app) } # 引数のappを使用し、Messageイベントを実行する関数をラッピングして返却します fn create_closures_for_js(app: &App<Msg, Model, Node<Msg>>) -> Box<[JsValue]> { let set_config = wrap_in_permanent_closure(enc!((app) move |unitedstr: String| { let mut params = unitedstr.split_whitespace(); let typestr = params.next().unwrap().to_string(); let valuestr = params.next().unwrap().to_string(); # ・・seed側処理実行・・ })); let rotate = wrap_in_permanent_closure(enc!((app) move |unitedstr: String| { let mut params = unitedstr.split_whitespace(); let axisstr = params.next().unwrap().to_string(); let layerstr = params.next().unwrap().to_string(); let dirstr = params.next().unwrap().to_string(); # ・・seed側処理実行・・ })); vec![set_config, rotate].into_boxed_slice() } # この部分れはseedのサンプルをそのまま使用させてもらいます。 fn wrap_in_permanent_closure<T>(f: impl FnMut(T) + 'static) -> JsValue where T: wasm_bindgen::convert::FromWasmAbi + 'static, { // `Closure::new` isn't in `stable` Rust (yet) - it's a custom implementation from Seed. // If you need more flexibility, use `Closure::wrap`. let closure = Closure::new(f); let closure_as_js_value = closure.as_ref().clone(); // `forget` leaks `Closure` - we should use it only when // we want to call given `Closure` more than once. closure.forget(); closure_as_js_value } Vue側準備 以下コマンドdoker-compose起動した後、Vueのコンテナに入り、Vueプロジェクト作成などを行います。また、サービス起動時もコンテナに入って起動する形を取ります。 sudo docker-compose build sudo docker-compose up sudo docker exec -it cubetrain-vuefront bash vueプロジェクト生成に関しては各種手法があったり今回の重要ポイントではないので詳細説明は省きます。ただ、以後のソースを説明する上で重要なので、今回使用した設定、モジュールを記載します。 Vue3を使用 typescript使用 パッケージマネージャーにはyarnを使用 UIライブラリにはVuetifyを使用(※2022年4月現在、VuetifyのVue3版はβバージョンです) package.jsの配置 package.jsonではなく、wasmのビルド生成物の方です。wasmモジュール本体とjavaScriptのインターフェースを担っています。 このファイルが無いと、Vueのビルド時にエラーが出てしまうので、このファイルはVueプロジェクト側に配置する必要があります。今回はvue/cubetrain/src/wasm/フォルダ配下へ配置する事にします。 さらに、そのまま配置するとVue側lintでエラーが出てしまいます。ファイルの冒頭に以下の1行を追加する必要があります。 /* eslint-disable */ wasm側は、サービスとしては使用せず、モジュールとして使用する形になるので、wasmビルド時に一連の処理をするようにシェルファイルを作っておきます。まず上記1行を出力しておいて、元package.jsの中身を追記する形で出力するという手法です。そして、そのシェルを実行してpackage.jsをVue側に配置しておきます。ついでにnginxとマウントしているフォルダにもコピーしておきます。 wasm/build.sh VUE_OUTPUT_PATH='../vue/cubetrain/src/wasm' NGINX_OUTPUT_PATH='../nginx/wasm' cargo make build_release && \ echo '/* eslint-disable */' > ${VUE_OUTPUT_PATH}/package.js && \ cat pkg/package.js >> ${VUE_OUTPUT_PATH}/package.js && \ cp pkg/package_bg.wasm ${VUE_OUTPUT_PATH}/package_bg.wasm && \ cp pkg/package.js ${NGINX_OUTPUT_PATH}/package.js && \ cp pkg/package_bg.wasm ${NGINX_OUTPUT_PATH}/package_bg.wasm 同時に、vue/curetrain/.gitignoreファイルにsrc/wasmフォルダ記載を追記しておきます。 Vueコンポーネント作成 ここではWasmScreen.vueを作成します。先に全ソースを記載し、それぞれポイントを紹介していきます。 WasmScreen.vue <template> <div :id="id"></div> </template> <script lang="ts"> import { defineComponent, toRefs, onMounted, ref } from 'vue'; import init, { start } from '@/wasm/package.js'; export default defineComponent({ name: "WasmScreen", setup(props){ const { id } = toRefs(props) const interfaceSetConfig = ref<any>(() => {}); const interfaceRotate = ref<any>(() => {}); const setConfig = (type: string, val: number) => { const unitedstr = `${type} ${val}`; interfaceSetConfig.value(unitedstr); }; const rotate = (axis: string, layer: string, dir: string) => { const unitedstr = `${axis} ${layer} ${dir}`; interfaceRotate.value(unitedstr); }; const onMountedOperation = () => { init('/wasm/package_bg.wasm').then(() => { const [set_config, rotate] = start(id.value); interfaceSetConfig.value = set_config; interfaceRotate.value = rotate; }); } onMounted(onMountedOperation); return { setConfig, rotate } }, props: { id: {type: String, required: true} }, }) </script> import節 先のコマンドので配置したwasmモジュールとのインターフェースpackage.jsを読み込んでいます。こちらはnginx側のファイルでは無いのでwasmというフォルダでなくても良いはずですが、解りやすさの為wasmフォルダを作ってそこに配置する形を取っています。 import init, { start } from '@/wasm/package.js'; template部とprop部 コンポーネントとして使用する為、wasm(seed)を組み込むDOM要素をid付きで定義します。そしてそのidは固定でなく使用する側から指定できる様にしておきます。 <template> <div :id="id"></div> </template> <script lang="ts"> // 中略 export default defineComponent({ // 中略 props: { id: {type: String, required: true} }, } </script> ちなみに、ブラウザの開発者モードでwasm部分のDOM要素を見ると以下の様になっています。id指定に関しては後程説明します。 wasm関数つなぎ部分 wasm側で定義した2つの関数を後に使用する為に、格納用の変数宣言をしておきます。interfaceSetConfig、interfaceRotateがそれです。そして、onMountedのタイミングでwasmの初期化処理を行います。/wasm/package_bg.wasmを指定してwasmモジュールを読み込みます。ここで最初に説明した構成により、/wasmパスである為、nginxを通してnginxコンテナ内に配置されたwasmを読みに行きます。start関数の返り値を受け取り、最初に宣言した格納用変数に関数としての返り値、set_config、rotateを格納します。 // 中略 const interfaceSetConfig = ref<any>(() => {}); const interfaceRotate = ref<any>(() => {}); // 中略 const onMountedOperation = () => { init('/wasm/package_bg.wasm').then(() => { const [set_config, rotate] = start(id.value); interfaceSetConfig.value = set_config; interfaceRotate.value = rotate; }); } onMounted(onMountedOperation); // 中略 関数実行部 設定変更、回転処理それぞれの関数を定義します。受け取った複数の引数をスペース区切りの一つの文字列にして、wasm側関数を呼びます。 // 中略 const setConfig = (type: string, val: number) => { const unitedstr = `${type} ${val}`; interfaceSetConfig.value(unitedstr); }; const rotate = (axis: string, layer: string, dir: string) => { const unitedstr = `${axis} ${layer} ${dir}`; interfaceRotate.value(unitedstr); }; // 中略 Vueコンポーネント使用部 同様にまずは今のApp.vue全ソースを記載します。その後ポイントを紹介します。 App.vue <template> <v-app> <v-main> <v-container class="grey lighten-5"> <v-row> <v-col md="4"> <ControlPanel :defspeed=40 :defscramblestep=24 @controlAction="onControlAction" @rotateAction="onRotateAction" /> </v-col> <v-col md="8"> <WasmScreen id="wasmelemid" ref="wasm" /> </v-col> </v-row> </v-container> </v-main> </v-app> </template> <script lang="ts"> import { defineComponent, ref } from 'vue' import ControlPanel from './components/ControlPanel.vue' import WasmScreen from './components/WasmScreen.vue' export default defineComponent({ name: 'App', setup(){ const wasm = ref(); const onControlAction = (type: string, val: number) => { if (wasm.value != null) { wasm.value.setConfig(type, val); } }; const onRotateAction = (axis: string, layer: string, dir: string) => { if (wasm.value != null) { wasm.value.rotate(axis, layer, dir); } }; return { wasm, onControlAction, onRotateAction }; }, components: { ControlPanel, WasmScreen }, }) </script> wasmモジュール宣言部 作成したWasmScreenコンポーネントを配置します。プロパティとしてidを固定値で指定します。モジュールの関数を実行する為、ref指定を付けておきます。 App.vue <template> <!-- 中略 --> <WasmScreen id="wasmelemid" ref="wasm" /> <!-- 中略 --> </template> <script lang="ts"> // 中略 const wasm = ref(); // 中略 </script> wasmモジュール関数実行部 ボタン群はまた別コンポーネントになっていますが、そちらからemitで呼ばれている関数onControlAction、onRotateActionの中で、先に宣言しておいたwasm変数からwasmコンポーネントの関数を呼び出します。 App.vue <script lang="ts"> // 中略 const onControlAction = (type: string, val: number) => { if (wasm.value != null) { wasm.value.setConfig(type, val); } }; const onRotateAction = (axis: string, layer: string, dir: string) => { if (wasm.value != null) { wasm.value.rotate(axis, layer, dir); } }; // 中略 </script> アクセス docker-composeでは、vueのポートをホスト側8881につなげていますが、こちらにアクセスするとnginxを通りません。 http://localhost:8880にアクセスしてnginxを通します。 まとめ 今回の構成では、wasm側は変更すると都度ビルドする必要があります。ブラウザ側再読み込みでwasmモジュールを再読み込みする必要もあります。今回は無理に自動化せずに、ビルド用シェルを作成し、都度シェル実行する形にしています。ファイル変更を検知して実行するまでもないかなという感じです。 最初はseedなどのwasmフレームワークでテスト用ボタンやその他を配置して主ロジックをチェックして後はデザインという段階まできたらインターフェース部を作成して連携するという流れが良いのではと思います。 次に向けて Rustの勉強をする為に始めたこのアプリ開発ですが、今後はVue側でUIや便利機能などの開発が中心になりそうです。 ちなみに次は色々な機能を付けていくため、メニュー化や右ペインの活用をしていく予定です。
- 投稿日:2022-04-02T21:53:15+09:00
Vue3注目の新機能 Reactivity Transformとは?
初めに 最近Vueの情報収集をサボっていたのだが、どうやらいつの間にかReactivity Transformという新機能が追加されていたらしい。(僕が軽く調べた感じだと日本語の情報がほぼ無い) これはまだ実験的に導入されたもので、デフォルトで無効になっており、かつ今後破壊的な変更が入る可能性がある。 なので今すぐ学ぶ意味は薄いが、面白そうなのでちょっと触ってみる。 よければLGTMお願いします。 Reactivity Transformって何? 公式ドキュメント 簡単に言うとref()周りを短くシンプルに書けるエイリアス(公式はマクロと呼んでいる)を提供するものである。 例えば、数値等のプリミティブ値をリアクティブにする場合ref()を使うが、値を参照、変更するときいちいち.valueと書かなければいけないので、この部分が微妙に面倒であり直感的ではなかった。 vueのリアクティブシステムはProxyを利用しており、ランタイム上ではプリミティブ値をそのままリアクティブにするようなことはできないが、コンパイラでその問題を解決しようとしている。 これらのマクロを利用するにはimportは不要だが、デフォルトでoffになっているため、以下の設定が必要になる(vue3.2.25から利用可能) vite.config.js export default { plugins: [ vue({ reactivityTransform: true }) ] } TypeScript対応は型定義ファイルに /// <reference types="vue/macros-global" /> を追記するだけ。 どう使うか? さて実際にどう使うか見ていこう。 以下のサンプルコードは公式から引用。 script setup記法を使用。知らない方はぜひ私の記事をご覧ください。 <script setup> const count = ref(0) console.log(count.value) function increment() { count.value++ } </script> <template> <button @click="increment">{{ count }}</button> </template> これが <script setup> let count = $ref(0) console.log(count) function increment() { count++ } </script> <template> <button @click="increment">{{ count }}</button> </template> と書ける。 着目すべきは2点 ref()の代わりに$ref()を使っている $ref()から返却されたcountを直接変更、参照している まず、$ref()という見慣れない関数があるが、これは本物のjavascriptの関数ではなく、ビルド時にref()等のコードに置き換えられるマクロだ。 リアクティブ値に直接アクセスできるようになったので、より直感的になった。 ただし、変数宣言がconstからletになるため嫌う人は居そう(TypeScriptであれば型の一致は保証されるため大きな問題は無さそう?) ちなみに、こういったマクロは$refの他にも,$computed, $shallowRef, $customRef, $toRefが存在する。 $()を用いたrefオブジェクトの分割 refオブジェクトは通常の分割代入を使うとreactivityを失ってしまう。 ※reactivityとは、その値に変更を加えたとき、それに依存する値が自動的に更新されるような性質のことである。 よってtoRefsを使いReactivityを維持しながらプロパティを分割することができた。 そしてReactivity Transformでは$()を用いることにより、プレーンな値のまま分割されたプロパティに対してreactivityを付与することができる。 こちらも公式ドキュメントから引用 import { useMouse } from '@vueuse/core' const { x, y } = $(useMouse()) console.log(x, y) これは以下のコードにコンパイルされる import { toRef } from 'vue' import { useMouse } from '@vueuse/core' const __temp = useMouse(), x = toRef(__temp, 'x'), y = toRef(__temp, 'y') console.log(x.value, y.value) 分割代入された値がrefオブジェクトになっていることがわかる。 Reactivityの喪失 勘の良い人はなんとなく気づいているだろうが、これらのマクロで付与されたReactivityはいくつかのパターンで失われてしまうことがある。 関数へ渡されるパターン reactive変数が関数に渡される際にReactivityが失われるパターン. コードは公式から引用 function trackChange(x: Ref<number>) { watch(x, (x) => { console.log('x changed!') }) } let count = $ref(0) trackChange(count) // これは以下のようにコンパイルされる (関数定義省略) let count = ref(0) trackChange(count.value) これだとtrackChange内にはreactiveではないプレーンな値が渡されてしまい、reactivityが失われてしまう。 しかし、ちゃんとこれを以下のように修正すれば正常に動作する let count = $ref(0) trackChange($$(count)) 関数にわたす際に$$()で囲うことで、count.valueと展開されるのを回避できる。 関数から返却されるパターン 逆に関数内部からreactive変数を返却するときにもreactivityが失われてしまうことがある。 例によって公式から引用 function useMouse() { let x = $ref(0) let y = $ref(0) return { x, y } } これは以下のようにコンパイルされる (省略) return { x: x.value, y: y.value } const { count } = defineProps<{ count: number }>() passAsRef($$(count)) 当然これもvalueが露出してしまうためreactiveにはならない。 この場合も$$()を使用してこう書ける。 function useMouse() { let x = $ref(0) let y = $ref(0) // listen to mousemove... // fixed return $$({ x, y }) } パターンは違うが、.valueと展開しないように$$()でエスケープするだけ。 setup(props) { const __props_count = toRef(props, 'count') passAsRef(__props_count) } 最後に 実戦で使ってみなきゃ良し悪しはわかってこないかも? 正直わざわざコンパイル後のこと意識するくらいならこれまで通りでいい気がする? 参考
- 投稿日:2022-04-02T21:51:02+09:00
Vue.js 入門? # v3対応状況
はじめに ※ この文章はベータ版です。 コンポーネント集 Oruga 特定のCSSフレームワークに依存しないコンポーネント集。 CSSフレームワークへの対応は、テーマの形で分離されています。 現在、Bulmaテーマが提供されています、 Bootstrapテーマ、開発中(WIP)。 Buefy の作者による Buefy の後継プロジェクト。 現在のバージョン: v0.5.5 (Vue3.x 対応済) Buefy Bulma (CSSフレームワーク)ベースのコンポーネント集。 原作者により、Vue 3.x に対応しないことが宣言されています。 現在のバージョン: 未対応 (Vue3.x 対応なし) Vuetify マテリアル・デザインを採用したコンポーネント集。 特定のCSSフレームワークに依存していない(自前の CSS の)模様。 現在のバージョン: v3.0.0-beta.0 (Vue3.x 対応 進行中) BootstrapVue Bootstrap4 (CSSフレームワーク)ベースのコンポーネント集。 現在のバージョン: 未対応 (Vue3.x 対応予定あり?) https://github.com/bootstrap-vue/bootstrap-vue/issues/5196 ※ 精力的に Vue3.x 対応を進めてくれている方はウクライナ人であり、 彼の国は現在、ロシアによる侵攻の最中です。彼の無事を祈ります。 BootstrapVue 3 Bootstrap5 (CSSフレームワーク)ベースのコンポーネント集。 現在のバージョン: 0.1.9 (Vue3.x 対応) VueTailwind Tailwind CSS (CSSフレームワーク)ベースのコンポーネント集。 現在のバージョン: 未対応 (v3.x で対応予定) PrimeVue 複数のCSSフレームワーク対応のコンポーネント集。 CSSフレームワークへの対応は、テーマの形で分離されている模様。 現在、Bootstrap / TailwindCSS / Material の テーマが提供されています。 現在のバージョン: 3.12.4 (Vue3.x 対応済) BalmUI マテリアル・デザインを採用したコンポーネント集。 現在のバージョン:10.3.1 (Vue3.x 対応済) 番外: コンポーネント集 (CSS) daisyUI Tailwind CSS (CSSフレームワーク)ベースのコンポーネント集。
- 投稿日:2022-04-02T21:51:02+09:00
Vue.js 入門? # Vue3対応状況
はじめに ※ この文章はベータ版です。 コンポーネント集 Oruga 特定のCSSフレームワークに依存しないコンポーネント集。 CSSフレームワークへの対応は、テーマの形で分離されています。 現在、Bulmaテーマが提供されています。 Buefy の作者による Buefy の後継プロジェクト。 現在のバージョン: v0.5.5 (Vue3.x 対応済) Buefy Bulma (CSSフレームワーク)ベースのコンポーネント集。 原作者により、Vue 3.x に対応しないことが宣言されています。 Vuetify マテリアル・デザインを採用したコンポーネント集。 特定のCSSフレームワークに依存していない模様。 現在のバージョン: v3.0.0-beta.0 (Vue3.x 対応中) BootstrapVue Bootstrap4 (CSSフレームワーク)ベースのコンポーネント集。 現在のバージョン: 未対応 (Vue3.x 対応予定あり?) https://github.com/bootstrap-vue/bootstrap-vue/issues/5196 ※ 精力的に Vue3.x 対応を進めてくれている方はウクライナ人であり、 彼の国は現在、ロシアによる侵攻の最中です。彼の無事を祈ります。 BootstrapVue 3 Bootstrap5 (CSSフレームワーク)ベースのコンポーネント集。 現在のバージョン: 0.1.9 (Vue3.x 対応) PrimeVue 複数のCSSフレームワーク対応のコンポーネント集。 CSSフレームワークへの対応は、テーマの形で分離されている模様。 現在、Bootstrap / TailwindCSS / Material の テーマが提供されています。 現在のバージョン: 3.12.4 (Vue3.x 対応済) VueTailwind Tailwind CSS (CSSフレームワーク)ベースのコンポーネント集。 現在のバージョン: 未対応 (v3.x で対応予定) コンポーネント集 (CSS) daisyUI Tailwind CSS (CSSフレームワーク)ベースのコンポーネント集。
- 投稿日:2022-04-02T15:49:27+09:00
オブジェクト指向でコードを書きたい①
実際にオブジェクト指向で書く オブジェクト指向って結局なんの役にたつの? 上記記事でオブジェクト指向について自分なりにまとめたので、今度はそれをコードに反映させて見ようと思います。 具体的には、僕のポートフォリオのとても汚いコードを少しずつ直していく、という内容になります。 この記事はその第一弾。 修正元のコード 初回なので、今回は修正元のコードを公開するだけにとどめておきます。 修正するのは以下のvueのコード。 PostEditPage.vue <template> <div class="PostEditPage"> <form> <div v-if="errors.length != 0"> <ul v-for="e in errors" :key="e"> <li> <font color="red">{{ e }}</font> </li> </ul> </div> <h2>レシピ編集</h2> <div> <label>レシピ名</label><br /> <input v-model="post.rname" type="text" /> </div> <div> <label>レシピ情報</label><br /> <textarea v-model="post.rinformation"></textarea> </div> <div> <label>レシピイメージ</label><br /> <input type="file" name="rimage" @change="selectedFile" /> </div> <div> <label>材料</label><br /> <textarea v-model="post.ingredient"></textarea> </div> <div> <label>手順1</label><br /> <textarea v-model="post.procedure_1"></textarea> </div> <div> <label>手順2</label><br /> <textarea v-model="post.procedure_2"></textarea> </div> <div> <label>手順3</label><br /> <textarea v-model="post.procedure_3"></textarea> </div> <div class="form-row"> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>エネルギー</label><br /> <input v-model="post.Energy" type="number" step="0.01" />kcal </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>タンパク質</label><br /> <input v-model="post.Protein" type="number" step="0.01" />g </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>脂質</label><br /> <input v-model="post.Lipid" type="number" step="0.01" />g </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>糖質</label><br /> <input v-model="post.Carbohydrate" type="number" step="0.01" />g </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>食物繊維</label><br /> <input v-model="post.Dietary_fiber" type="number" step="0.01" />g </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>カリウム</label><br /> <input v-model="post.Potassium" type="number" step="0.01" />mg </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>カルシウム</label><br /> <input v-model="post.Calcium" type="number" step="0.01" />mg </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>鉄</label><br /> <input v-model="post.iron" type="number" step="0.01" />mg </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>亜鉛</label><br /> <input v-model="post.Zinc" type="number" step="0.01" />mg </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>ビタミンa</label><br /> <input v-model="post.VitaminA" type="number" step="0.01" />μg </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>ビタミンb1</label><br /> <input v-model="post.VitaminB1" type="number" step="0.01" />mg </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>ビタミンb2</label><br /> <input v-model="post.VitaminB2" type="number" step="0.01" />mg </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>ビタミンc</label><br /> <input v-model="post.VitaminC" type="number" step="0.01" />mg </div> <div class="co-xs-2 col-sm-2 col-md-2 col-lg-2"> <label>塩分相当量</label><br /> <input v-model="post.Salt_equivalent" type="number" step="0.01" />g </div> </div> <div v-show="show"> <div> <button class="button" type="button" @click="Reflect_Nutrients(post)"> 材料を反映 </button> <p>利用する材料の栄養合算</p> <table class="table table-sm"> <thead> <tr> <th scope="col">エネルギー</th> <th scope="col">タンパク質</th> <th scope="col">脂質</th> <th scope="col">糖質</th> <th scope="col">食物繊維</th> <th scope="col">カリウム</th> <th scope="col">カルシウム</th> </tr> </thead> <tbody> <tr> <td>{{ addNutrients.Energy }}kcal</td> <td>{{ addNutrients.Protein }}g</td> <td>{{ addNutrients.Lipid }}g</td> <td>{{ addNutrients.Carbohydrate }}g</td> <td>{{ addNutrients.Dietary_fiber }}g</td> <td>{{ addNutrients.Potassium }}mg</td> <td>{{ addNutrients.Calcium }}mg</td> </tr> </tbody> </table> <table class="table table-sm"> <thead> <tr> <th scope="col">鉄</th> <th scope="col">亜鉛</th> <th scope="col">ビタミンa</th> <th scope="col">ビタミンb1</th> <th scope="col">ビタミンb2</th> <th scope="col">ビタミンc</th> <th scope="col">塩分相当量</th> </tr> </thead> <tbody> <tr> <td>{{ addNutrients.iron }}mg</td> <td>{{ addNutrients.Zinc }}mg</td> <td>{{ addNutrients.VitaminA }}μg</td> <td>{{ addNutrients.VitaminB1 }}mg</td> <td>{{ addNutrients.VitaminB2 }}mg</td> <td>{{ addNutrients.VitaminC }}mg</td> <td>{{ addNutrients.Salt_equivalent }}g</td> </tr> </tbody> </table> </div> <div> <p>利用する材料</p> <ul v-for="(item, index) in ingrd_sum" v-bind:class="['q_' + index]" :key="item.id" > <li>{{ item.rname }}</li> <input v-model="$data['q_' + index]" /> </ul> </div> <div> <input type="text" v-model="keyword" /> <button class="button" type="button" @click="filteredPosts"> 検索 </button> <ul v-for="post in posts" :key="post.id"> <li>{{ post.rname }}</li> <button class="button" type="button" @click="add_ingredient(post)"> 材料に追加 </button> </ul> </div> </div> <button type="button" class="button" @click="show = !show"> 既存のレシピを材料に加える </button> </form> <br /> <button class="button" v-on:click.preventDefault="updatePost"> Commit </button> </div> </template> <script> import axios from "axios"; export default { data: function () { return { show: false, q_0: "1", q_1: "1", q_2: "1", q_3: "1", q_4: "1", q_5: "1", q_6: "1", q_7: "1", q_8: "1", q_9: "1", q_10: "1", post: { rname: "", rinformation: "", rimage: "", ingredient: "", procedure_1: "", procedure_2: "", procedure_3: "", Energy: "", Protein: "", Lipid: "", Carbohydrate: "", Dietary_fiber: "", Potassium: "", Calcium: "", iron: "", Zinc: "", VitaminA: "", VitaminB1: "", VitaminB2: "", VitaminC: "", Salt_equivalent: "", }, posts: [], ingrd_sum: [], keyword: "", errors: "", uploadFile: null, }; }, mounted() { const url = location.pathname; const id = url.replace(/[^0-9]/g, ""); console.log(`this.post_1: ${JSON.stringify(this.post)}`); axios .get(`/api/v1/posts/${id}.json`) .then((response) => (this.post = response.data)); console.log(`this.post_2: ${JSON.stringify(this.post)}`); }, computed: { addNutrients: function () { let sum_nutrients = { Energy: "0", Protein: "0", Lipid: "0", Carbohydrate: "0", Dietary_fiber: "0", Potassium: "0", Calcium: "0", iron: "0", Zinc: "0", VitaminA: "0", VitaminB1: "0", VitaminB2: "0", VitaminC: "0", Salt_equivalent: "0", }; let sn_keys = Object.keys(sum_nutrients); for (let i in this.ingrd_sum) { console.log(`i: ${i}`); console.log(`this.ingrd_sum[i]: ${JSON.stringify(this.ingrd_sum[i])}`); let ingrd = this.ingrd_sum[i]; console.log(`ingrd: ${JSON.stringify(ingrd)}`); console.log(`sum_nutrients: ${JSON.stringify(sum_nutrients)}`); if (i == 0) { var bfr_num3 = this.q_0; } else if (i == 1) { var bfr_num3 = this.q_1; } else if (i == 2) { var bfr_num3 = this.q_2; } else if (i == 3) { var bfr_num3 = this.q_3; } else if (i == 4) { var bfr_num3 = this.q_4; } else if (i == 5) { var bfr_num3 = this.q_5; } else if (i == 6) { var bfr_num3 = this.q_6; } else if (i == 7) { var bfr_num3 = this.q_7; } else if (i == 8) { var bfr_num3 = this.q_8; } else if (i == 9) { var bfr_num3 = this.q_9; } else if (i == 10) { var bfr_num3 = this.q_10; } else { var bfr_num3 = 1; } console.log(`bfr_num3: ${JSON.stringify(bfr_num3)}`); for (let i = 0; i < 14; i++) { console.log(`sum_nutrients: ${JSON.stringify(sum_nutrients)}`); console.log(`i: ${i}`); let sn_key = sn_keys[i]; console.log(`sn_key: ${sn_key}`); let num1 = parseFloat(sum_nutrients[sn_key]); let num2 = parseFloat(ingrd[sn_key]); let num3 = parseFloat(bfr_num3); if (Number.isNaN(num2)) { num2 = 0; } if (num3 < 0 || 1000 < num3) { num3 = 1; } else { num2 = num2 * num3; console.log(`num2x: ${JSON.stringify(num2)}`); } console.log(`num1: ${JSON.stringify(num1)}`); console.log(`num2: ${JSON.stringify(num2)}`); sum_nutrients[sn_key] = num1 + num2; console.log( `sum_nutrients[sn_key]: ${JSON.stringify(sum_nutrients[sn_key])}` ); } } return sum_nutrients; }, }, methods: { selectedFile(e) { console.log(e.target.files[0]); e.preventDefault(); this.uploadFile = e.target.files[0]; // fileにはreadonly制約があり、v-modelは使えない。代わりにchangeイベントが推奨されている }, add_ingredient: function (post) { this.ingrd_sum.push(post); }, Reflect_Nutrients: function (post) { console.log(`this.post.Energy: ${this.post.Energy}`); console.log(`this.sum_nutrients: ${JSON.stringify(this.sum_nutrients)}`); this.post.Energy = this.addNutrients.Energy; this.post.Protein = this.addNutrients.Protein; this.post.Lipid = this.addNutrients.Lipid; this.post.Carbohydrate = this.addNutrients.Carbohydrate; this.post.Dietary_fiber = this.addNutrients.Dietary_fiber; this.post.Potassium = this.addNutrients.Potassium; this.post.Calcium = this.addNutrients.Calcium; this.post.iron = this.addNutrients.iron; this.post.Zinc = this.addNutrients.Zinc; this.post.VitaminA = this.addNutrients.VitaminA; this.post.VitaminB1 = this.addNutrients.VitaminB1; this.post.VitaminB2 = this.addNutrients.VitaminB2; this.post.VitaminC = this.addNutrients.VitaminC; this.post.Salt_equivalent = this.addNutrients.Salt_equivalent; }, filteredPosts: function (keyword) { if (this.keyword === "") { posts = null; } else axios .get("/api/v1/posts/refer/" + this.keyword) .then((response) => (this.posts = response.data)); }, updatePost: function () { const data = new FormData(); // multipart/form-data形式のため、new FormData()を使う data.append("rname", this.post.rname); // file形式以外も送信可能 data.append("rinformation", this.post.rinformation); // file形式以外も送信可能 data.append("ingredient", this.post.ingredient); // file形式以外も送信可能 data.append("procedure_1", this.post.procedure_1); // file形式以外も送信可能 data.append("procedure_2", this.post.procedure_2); // file形式以外も送信可能 data.append("procedure_3", this.post.procedure_3); // file形式以外も送信可能 data.append("rimage", this.uploadFile); data.append("Energy", this.post.Energy); // file形式以外も送信可能 data.append("Protein", this.post.Protein); // file形式以外も送信可能 data.append("Lipid", this.post.Lipid); // file形式以外も送信可能 data.append("Carbohydrate", this.post.Carbohydrate); // file形式以外も送信可能 data.append("Dietary_fiber", this.post.Dietary_fiber); // file形式以外も送信可能 data.append("Potassium", this.post.Potassium); // file形式以外も送信可能 data.append("Calcium", this.post.Calcium); // file形式以外も送信可能 data.append("iron", this.post.iron); // file形式以外も送信可能 data.append("Zinc", this.post.Zinc); // file形式以外も送信可能 data.append("VitaminA", this.post.VitaminA); // file形式以外も送信可能 data.append("VitaminB1", this.post.VitaminB1); // file形式以外も送信可能 data.append("VitaminB2", this.post.VitaminB2); // file形式以外も送信可能 data.append("VitaminC", this.post.VitaminC); // file形式以外も送信可能 data.append("Salt_equivalent", this.post.VitaminC); // file形式以外も送信可能 const url = location.pathname; const id = url.replace(/[^0-9]/g, ""); axios .patch(`/api/v1/posts/${id}`, data) .then((response) => { let e = response.data; window.location = `/posts/${id}`; }) .catch((error) => { console.error(error); if (error.response.data && error.response.data.errors) { this.errors = error.response.data.errors; } }); }, }, }; </script>
- 投稿日:2022-04-02T11:56:52+09:00
[Vue]元教師が教師の業務効率化のためにランダム指名アプリを作りました
サービス紹介 座席数を指定して、生徒を指名できる、シンプルなアプリです。 使い方 座席数を指定して、後は指名ボタンを押すだけ! きっかけ きっかけは自分自身が生徒だった時に、授業を受けている際に「あれ、なんか前の席の人ばっかり当てられてるな〜」と感じたことから始まりました。 そして、実際に2年ほど教師をしていたのですが、前の席の人はやはり当てやすいのです。 出典 https://www.paylessimages.jp/detail.php?id=gf1420736473 一回一回の授業ではそこまで差は出ませんが、一年も続けると席が固定の場合、とても不公平になるかと思いました。 そこで、生徒をランダムに指名してくれるアプリを作りました! このアプリを使えば不公平がないかつ、誰が当たるかわからない緊張感もある、楽しい授業がしやすいかなと感じます。 使用技術 vue.js(2.0) Firebase Vuetify ハマったところ Vueのコンポーネント志向を活用しようと、トップページと、列と、一つ一つのセルという三つのコンポーネントを作成しました。 HelloWorld.vue <v-row class="row py-0 px-0" v-for="i in changeHeight" :key="i" color="blue darken-2"> <Row :mathWidth="mathWidth" :rowId="i" :propRandomId="propRandomId"/> </v-row> Row.vue <v-col class="pa-0" justify="center" v-for="i in changeWidth" :key="i" > <Cell :id="rowId + '_' + i" :ref="rowId+'_'+i"> </Cell> </v-col> 縦横の席数を受け渡して全体を表示しています。 こちらの受け渡しはpropsを使用しているのですが、うまく受け渡せなかったり、予期せぬ挙動が生まれたりと、そこそこ大変でした。 storeを使うべきか、propsで良いのかという判断がなかなか難しいです。 また、ランダムに指定した席ひとつだけ赤くするという処理も、一つ一つのCellに 1_1 1_2 1_3 2_1 2_2 2_3 とインデックスを付ける処理ですが、 Row.vue watch: { propRandomId: function () { for(let i = 0; i < this.mathWidth; i++){ this.$refs[this.rowId+"_"+(i+1)][0].$el.children[0].style.backgroundColor="lightgray"; } if(!this.$refs[this.propRandomId]){ return } this.$refs[this.propRandomId][0].$el.children[0].style.backgroundColor="red"; }, }, $refsを使ってかなりむりやりやっています。 もっとスマートにかけたらよりいいのかなーとか思ったりします。 今後の改良点 もし反響が大きければ より素敵なアニメーションの実装 席に生徒名を設定できる機能 一度当たった人は除外して再抽選 あたりの機能は実装したいなと考えています。 LGTMやツイートの拡散などしていただけると本当にありがたいです、、、 おまけ Twitterで一緒に個人開発してくれる人、一緒にプログラミングを勉強してくれる人を募集しています! 結構発信しているので、是非ともフォローをお願いいたします。 また、開発を手厚くサポートしてくれたメンターさんには本当に頭が上がりません、。 ありがとうございました。 サービス 参考