20201124のvue.jsに関する記事は14件です。

初プルリクで心がボロボロにならないためのセルフチェック観点をまとめてみた

この記事はモチベーションクラウドシリーズアドベントカレンダー2020の3日目の記事です。

自己紹介

初めまして!こんにちは!19年新卒の小宮と申します!
エンジニア歴は約1年程度で、バックエンドやQAも関わりつつ、
現在メインはフロントエンドエンジニアとして日々開発業務に勤しんでおります!

この記事を書こうと思った背景

この話は本当にあった怖い話です...
image.png
上のキャプチャは自分が初めて出した大きめのPRです。ご覧いただけますでしょうか?
Conversation 73という恐ろしい文字列が・・・

自分がこの73コメント分(一部CodeCovなどの自動コメントも含まれますが)、
先輩エンジニアの時間を奪っているんだと勝手に責任感を感じて、
いそいそといただいたコメントに対応していたのが1年前だと思うと、時の流れを早く感じます。

上記のような現象、「自分も同じ経験をしたことがあるなあ・・・」という方も
一定数いらっしゃるのではないでしょうか?
この現象が起きる理由として、パッと思いつくだけでも

  • その開発現場のコードのお作法を知らない
  • 動けばいいや!という精神でコーディングしてしまう
  • ここまで考慮できてますよ!というアピールで不必要なコードを書いてしまう

などと、他にもいっぱいあるんじゃないかなあと思っております。

今回、アドベントカレンダーで執筆の機会をいただけたので、一年前の自分のように
先輩の時間を奪って申し訳ねえ!というやるせない気持ちになる駆け出しエンジニアが
少しでも減る一助になれれば良いなあと思い、本記事を執筆しました!

具体的なセルフチェックチェック観点

言語共通

命名

コードというのは「書くこと」より「読む/読まれること」の方が圧倒的に多いのです!
命名に関してチェックするべき観点のほんの一例を紹介します!

naming.js
// 略語について
number // Good! なるべく略語は使わない!
no // More! 「いいえ」のNoと勘違いする可能性も0じゃないですね

// 真偽値について
isParent // isで始めたり、
hasChild // 動詞で始めたり、
alreadyAppeared // 副詞+動詞の形だと真偽値を返してくれることが察しやすいですね!

// メソッドや変数名について(例えばECサイトでカートへ追加するメソッド名)
const add = function() { return ** } // 何をどこに加えるのかが分かりにくい。
const addToCart = function() { return ** } // ToCartを入れるだけでも何やっているか分かりやすくなったですね!

本当はまだまだあると思いますが、いったんこれくらいで!
ちなみに僕のエンジニアの師匠のさらにその師匠が
エンジニアは、名前をつけるのがお仕事です。」と言っていたようです。

あと、命名の際はDeepL(Google翻訳よりも精度高い気がします)を使って、
日本語で何をしたいメソッドなのか言語化してから翻訳かけたりしています!

まとめられる箇所をまとめられていない

コピペを何度も繰り返しているところはメソッドに切り出してまとめましょう!
よくDRY(Don't Repeat Yourself)って言われたりしますね!

dry.js
console.log('hi! taro')
console.log('hi! hanako')
console.log('hi! hiroshi') // こんな風にいちいち呼び出さずに

const nameCall = function(name) { console.log(`hi! ${name}`) } // メソッドでくくっちゃいましょう!
nameCall('taro')
nameCall('hanako')
nameCall('hiroshi') 

DRYを徹底することのメリットとして、

  • コード量が減る
    • 上記の例だと実感しづらいかもしれませんが、これは本当!笑
  • 後から修正が入った時に修正が必要な箇所が少なくなる
    • 上記の例だと「hi!」と挨拶していたのが、「hello!」になった時、nameCallだけ編集すればOK!
  • コードが読みやすくなる
    • 上記の例だと「あ、これは名前を呼んでいるんだな!」とメソッド名から推察しやすくなりますね!

不必要なコードも書いてしまう

よほどのことがない限り、今後これを使うことになるだろうから、今やっちゃおう!と
今必要ないコードを加えることはお節介になる可能性が高いです!
よくYAGNI(You ain't gonna need it)って言われたりしますね!

yagni.js
// 例えば多言語対応することになり、先に英語版からリリースして、来年に中国版をリリースしよう!となったとき、
// 先んじて中国語の定数も定義しようというのはお節介になる可能性が高いです!!
export const LANGUAGE_TYPES = {
  JA: {
    KEY: 'ja',
    TEXT: '日本語'
  },
  EN: {
    KEY: 'en',
    TEXT: '英語'
  },
// ZH: { いらない!!
//   KEY: 'zh',
//   TEXT: '中国語'
// },
}

YAGNIを徹底することのメリットとして、

  • 負債になる可能性のあるコードを残さない
    • 上記の例だと中国語版のリリースが白紙になった場合、完全に負債になりますね!
  • 読み手の「?」を減らす
    • 上記の例だと中国語版のリリースが白紙になってから1年ぐらい経ち、あなたがそのプロジェクトを外れていた場合、「え、これって必要なコードなんですか?」と後から読む人の混乱を招く可能性がありますね!
  • 後から実装する人がバグを出してしまう可能性を減らす
    • 上記の例だと違う人が中国語を担当したときに、「あ、中国語も一緒にやってくれたんだ!」と安心してしまい、対応するべき箇所全てに対応するのを失念してしまう可能性がありますね!

スコープを意識しましょう

スコープとは、あなたが定義した変数や関数が参照される範囲のことです!
無駄に広いスコープで変数を定義すると、読み手に不親切な上に、不具合の温床になったりするので要注意です!

scope.js
const scope = "Wide Scope"
function getNarrowScope() {
  const scope = "Narrow Scope"
  return scope
}
function getWideScope() {
  return scope
}
console.log(getNarrowScope()) // Narrow Scope
console.log(getWideScope()) // Wide Scope

スコープを意識することのメリットとしては、

  • コードを疎結合に保てる
    • 上記の例だと"Wide Scope"の値を持つ方の変数scopeとgetWideScopeが相互に依存関係にありますね!コードは疎結合に保つことが正義です!
  • 読みやすくなる
    • 上記の例だとまだ読めますが、これが数百行のコードになった時、"Wide Scope"の値を持つ方の変数scopeを100行下で呼び出していたりしたら、読み手にかける負担が大きくなりますね!
  • 書きやすくなる
    • 実際にコードを書くときも、意識するべきことが減るので、実装する際においても考えることが減ると思われます!

マジックナンバー

マジックナンバーとは、意味を持つ数字を、直接書いて、読み手が
「なんだこれ?」となってしまうような数字のことです。

magicNumber.js
// 例えば100円ショップでお買い物をする時
const price = 100 * 1.10 // 1.10が何のことだか分からない!

const tax = 1.10
const price = 100 * tax

マジックナンバーを撲滅することのメリットとしては、DRYの説明で書いたのと同じで、

  • コード量が減る
    • 上記の例でも実感しづらいかもしれませんが、これは本当!笑
  • 後から修正が入った時に修正が必要な箇所が少なくなる
    • 上記の例だと消費税が20%になった時(やだなぁ...)、taxだけ編集すればOK!
  • コードが読みやすくなる
    • 上記の例だと「あ、これは消費税のことだな!」と定数名から推察しやすくなりますね!

インデントやtypo

これらは基本的なことでかつ、すぐ見つけられることなので、もう一度見直しましょう!
インデントを見やすくしたり、typoらしきものを色付けしてくれる拡張機能も多くのエディターで備えてくれているので、
それらも是非活用しましょう!(以下例はVSCodeのCode Spell Checker)
image.png

JavaScript

console.logの消し忘れ

開発中に書いていたconsole.logを消し忘れていないかは要チェックですね!
これをやったときはだいぶ死にたくなりました!

変数の定義

こちらはES6で新しくできるようになったことですが、変数の宣言にvar以外が使えるようになりました
参考 : var/let/constの使い分けのメモ
(良書でも古いものだと未だにvarが使われたりしますが、今やvarは使わない!と言っても過言ではないです)
他にもES6は便利なので、極力活用しましょう!(ここで書くとボリューミーなので、今回は省略)

variables.js
var name = "taro" // varは使わない!!!

const name = "taro" // 真っ先に使うべきはconst

// 名前が「田中ジェイソン太郎」というミドルネーム持ちの人がいて、海外ではジェイソンと呼ばれていて、
// どうしても再代入が必要な場合だけletを使いましょう!
let name
if (country === "japan") {
  name = "taro"
} else {
  name = "Jason"
}

変数定義にこだわることで、以下のメリットがあります!

  • ファイル内で変数にスコープを持たせられる
  • 読み手にとって親切
    • 例えばletで変数が定義されていた場合、読み手は「あ、これはどこかで再代入されるのか!」と予測しながら読み進められる

CSS

カラーパレット

文字の色や、背景色を定義するとき、
カラーパレットを使える場合はそちらを活用しましょう!!

background-color: #bf2926; // 直接カラーコードを書かない!

$PASSIONATE_RED: #bf2926; // 共通で使えるカラーパレットがどこかで定義されているかどうか確認して
background-color: $PASSIONATE_RED; // ある場合はそのカラーパレットに割り当てられた定数を使いましょう!

カラーパレットを一貫して使うことのメリットとしては以下が挙げられると思います

  • プロダクトに使われているそれぞれの色に役割を持たせやすくなる
    • レッドはエラー、グリーンは成功、オレンジは注意などと、命名と共に定義すると色に役割を割り当てやすくなりますね!
  • どんな色だかイメージしやすくなる
    • 例えば#bf2926;が何の色だかぱっと見で判るほど記憶力の良い人はいないと思いますが、$PASSIONATE_REDなら「情熱的な赤い色なんだろうな〜」とイメージできやすくなりますね!

8の倍数ルール

marginやheight、font-sizeなどの大きさを8や4の倍数で統一している可能性があります!
メリット等は以下の記事などを読むとわかりやすいので、是非ご一読を!
参考 : 8の倍数ルールでデザインする理由とメリット・デメリット

最後に

夢中で思いつく限りのことを書いてみました。
(本当はvue.js特有のことなども書こうと思ったんですが、力尽きましたw)

コードレビューに出す内容って相手への敬意の現れだなと思っていて、
正直、上記の内容を一回指摘されるぐらいなら全然OKだと思う(新人だし仕方ないよ!ってなると思う)んですが、
一度指摘されたことは次のPRでは絶対指摘されないぞ!という心意気が一番大事かなと思います!!

ではみなさん良い12月を!!
僕は月末までに彼女を作ります!!!

おまけ(参考資料)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

これは自分がエンジニアになったときに職場の先輩から真っ先に読め!と言われた本です。
当時は「はいはい、なるほどね〜」くらいだったんですが、今改めて読み返すと、
「当時の自分全然わかってなかったんやな...」と奥の深さを知れるエンジニアの登竜門的な本

プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の原理原則

一番最初にあげたリーダブルコードより新しいもので、今回自分が挙げたYAGNIとかDRYとか、
そのほかにもエンジニアの道標?心得?的なのがたくさん載ってます。
リーダブルコードと違って、具体的なコードが一切載っていなかったり、
日本人が書いたものなので、初心者にも読みやすいのかなと思います。

改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで

そもそものJavaScriptへの入門として、こちらの教材は少し難解な部分もあるのですが、
一通りしっかりやると、かなりの実力がつくのではないかなあと思います。
JavaScriptの古い記法から、新しいES6の記法までをさらっているため、
よりES6のメリットが分かったり、現場によっては割と古い記法で書かれているところもあるので、
そういった方にもお勧めできますね!

やっぱりプログラミング(今回はvue)の学習といえばアプリ作成!ということでカレンダー!

学習のために、最初の方は色々と教材を買ってやってみたのですが、
やっぱり自分でアプリを作るのが一番学習になります。
ということで私はこちらの記事を参考に、カレンダー的なものを作りました!
追加機能として例えばモーダルを自作してみたりしてもいいかもしれませんね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ConoHa VPS,Vue-cli,Docker】Vue CLIで作成したSPAをConoHaで公開するまで①(ConoHa登録~VPSにログイン)

初投稿です。よろしくお願い致します。
新卒入社して半年、「エンジニアなら自分のプロダクトを公開したい!」という思いから、学生自体にお世話になったバイト先のWEBサイトを公開することにしました。

その際の作業ログを共有いたします。

前提条件

Vue Cliで作成したアプリケーションがあること
もし作成していない場合は、下記URLなどを参考に作成してみましょう。
https://qiita.com/yuucu/items/189945f984e5e53117ea

環境

PC: MacBookPro
VPS: ConoHa
CentOS: 8.2.2004
docker-compose: 1.27.4
Docker: 19.03.13
nginx: 1.19.4
vue-cli: 4.5.8
npm: latest(作業時:15.2.0)

ConoHa登録

https://www.conoha.jp/vps/
_2020-10-25_15.23.07.png

新規登録

signup.png

登録後

私は下から二番目のグレードの「memory:1GB」を選択
OSはCentOS8にしました。
右側の値段は、デフォルトでは、三ヶ月分まとめ払いが選択されていますが、
一ヶ月ごとの支払いに変更することもできます。
しかし、まとめ払いがお得みたいです。
ここで、rootパスワードとネームタグを入力しましょう。
※rootパスワードはVPSに接続するために必要です。必ずメモしましょう!
※また、この時SSH Keyを作成できるはずだったのですが、私は作成し損ねてしまったので、自力で作成していきます。
screencapture-manage-conoha-jp-Wizard-Create-2020-10-25-05_49_24.png

コントロールパネル

コントロールパネルはこんな感じです。
自分が作成したVPSが一覧で確認できます。
スクリーンショット 2020-11-19 22.31.35.png

ネームタグをクリックすると、サーバの詳細が確認できます。
ここで、VPSのIPアドレスを確認しておきましょう。
スクリーンショット 2020-11-19 22.34.31.png

VPSにリモートアクセス

rootでSSH接続していきます。
今回はMacのTerminalでの接続を想定しております。
Windowsの方は、Teratermなどで接続されることをお勧めします。

ssh root@ipアドレス

The authenticity of host '(IPAddress) ((IPAddress))' can't be established.
ECDSA key fingerprint is (...).
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '(IPAddress)' (ECDSA) to the list of known hosts.
root@(IPAddress)'s password: #初めに設定したrootパスワードを入力

※基本的に、パスワード入力をしても、画面上に表示されません。
しかし、文字は見えないだけで入力されています。
注意して入力しましょう。

このように表示されれば、リモートアクセス成功です。

[root@(IPアドレス) ~]# 

次回:セキュリティ対策

サーバに接続して、早速アプリケーションの環境を立ち上げたいところですが、
まずはセキュリティ対策をします。

とはいえ、新卒研修でも行うぐらい作業自体は難しいものではありません。
(法人レベルになると、もっと高度な対策が求められると思われますが…)

地道に進めていきます。

参考・引用

https://qiita.com/mkoku/items/ea107a29fd925081ebcf

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue 3.x (Vue CLI 5.x) 環境構築完全版 Docker + TypeScript + Yarn + Router

Vue 3 環境セットアップ手順

数ある記事から見ていただき有難うございます。
Vue 3 がリリースされてから2ヶ月ほど経過しましたが、
今回は会社でとりあえず採用している Vue 3 の環境構築をご紹介します。

歓迎条件

  • Docker の環境構築ができている。
  • これまでに Vue (2.x) の環境を構築したことがある。
  • とりあえず TypeScript で Vue3 触ってみたい!
  • 完全版と書いていますがあくまで一つの環境に過ぎませんので構築したい環境の条件が合う方は使ってみてください。

プロジェクトの作成

自身のプロジェクトを作成します。
名前はお任せします。(どうしても決められない方は new-vue3-app とかで大丈夫です)

プロジェクト直下に docker-compose.yml ファイルを作成

version: "2.4"

services:
  node:
    image: node:latest
    ports: 
      - 8080:8080
    volumes:
      - .:/srv:cached
    working_dir: /srv
    command: yarn serve

プロジェクトで以下のコマンドを実行

Vue CLI インストール

docker-compose run --rm node yarn add @vue/cli

Vue CLI バージョン確認

vue -V
=> @vue/cli 5.9 (2020/11/24)

アプリケーションの初期化

docker-compose run --rm node ./node_modules/.bin/vue create . 
// 最後の . はカレントディレクトリを指すのでお忘れなく!
?  Your connection to the default yarn registry seems to be slow.
   Use https://registry.npm.taobao.org for faster installation? (Y/n) Y
? Generate project in current directory? (Y/n) Y

プロジェクトの作成方法を選択

? Please pick a preset: (Use arrow keys)
  Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
❯ Manually select features

プロジェクトへ導入するライブラリを選択

? Please pick a preset: Manually select features
? Check the features needed for your project:
 ◉ Choose Vue version
 ◉ Babel
 ◉ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◯ Vuex
❯◉ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

重要 : バージョンの選択 3.x を選択

? Choose a version of Vue.js that you want to start the project with
  2.x
❯ 3.x (Preview)

Class Style or Object Style の選択(基本的に No の Object Style で良い)

? Use class-style component syntax? (y/N) N

自動検出されたポリフィルに Babel と TypeScript を使うか選択

? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) Y

Vue RouterのHistoryモードを利用するかを設定

? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) Y

CSS プリプロセッサの選択

? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): (Use arrow keys)
❯ Sass/SCSS (with dart-sass)
  Sass/SCSS (with node-sass)
  Less
  Stylus

ESLintのプリセットを選択

? Pick a linter / formatter config:
  ESLint with error prevention only
  ESLint + Airbnb config
❯ ESLint + Standard config
  ESLint + Prettier
  TSLint (deprecated)

ESLintの実行タイミングを選択

? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ Lint on save
 ◯ Lint and fix on commit

ライブラリ(BabelやESLint)の設定ファイルの配置個所を選択

? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files
  In package.json

次回以降のcreate projectで利用できるプリセットに登録するか設定

? Save this as a preset for future projects? (y/N) N

Yarn or NPM を選択

? Pick the package manager to use when installing dependencies: (Use arrow keys)
❯ Use Yarn
  Use NPM

完了時のログ

success Saved lockfile.
Done in 80.73s.
⚓  Running completion hooks...

?  Generating README.md...

?  Successfully created project srv.
?  Get started with the following commands:

 $ yarn serve

 WARN  Skipped git commit due to missing username and email in git config, or failed to sign commit.
You will need to perform the initial commit yourself.

サーバを立てる

yarn serve
yarn run v1.22.4
warning ../../../package.json: No license field
$ vue-cli-service serve
 INFO  Starting development server...
98% after emitting CopyPlugin

 DONE  Compiled successfully in 2932ms                                                                        21:55:12


  App running at:
  - Local:   http://localhost:8081/
  - Network: http://10.13.52.2:8081/

  Note that the development build is not optimized.
  To create a production build, run yarn build.

No issues found.

構築完了です。
新生 Vue3 をお楽しみください。
お疲れ様でした!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue歴5日でポートフォリオサイトを作成する 1

Vueを触り始めて5日ほどで簡単なポートフォリオサイトなら作成することが出来ました。その過程について書いていきます。

私のポートフォリオサイト

事前準備

手始めにVueで開発する為に必要なものをインストールしていきます。

コマンドラインインタフェースをインストールします。

$ npm install -g @vue/cli

次にプロジェクトを作成します。今回はリファレンスの多さからVue2で書いています。

$ vue create my_portfolio

プロジェクトの作成が完了した後、一旦立ち上げてみます。

$ cd my-portfolio
$ npm run serve

以下のメッセージが表示されましたら、localhost:8080にアクセスします。

Your application is running here: http://localhost:8080

EmsM2m2UcAAgVq3.jpeg

導入成功です。

次にVue.js Devtoolsを導入していきます。これはChromeブラウザの拡張機能で、Vueで書かれたコード内のデータを手っ取り早く確認することが出来る便利な機能です。導入した後Devtoolを開くとVueと書かれたタブが表示されているはずです。Vueファイルが読まれている時はアイコンがアクティブになります。
スクリーンショット 2020-11-24 20.35.10.png

これにて事前準備は完了です。

Vueと仲良くなる

publicディレクトリのindex.htmlを見てみます。

public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

注目するべきはbodyタグ内の<div id="app">です。このappは一体何者でしょうか?Devtoolで確認してみると...スクリーンショット 2020-11-24 20.55.37.png

何やらdivタグの中でさらに展開されています。
この中身についてはApp.vueを見てみましょう。

App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

index.htmlのappはこれを呼び出しているみたいです。
さらにApp.vueの中でHelloWorldというcomponentを読むことで部品としてHelloWorldをタグに使用していますね。
Vueではindex.htmlをそのまま表示するのではなくてbuildを挟むことで./dist/index.htmlへと変換します。これをブラウザで表示している訳です。

次にmain.jsを確認します。

main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

まずVueを動かすためのインスタンスをvueから作り出し、先ほどの<div id="app">にマウントしていることがわかります。だからindex.html内でApp要素が表示されたんですね。

Vueの基本

データバインディング

まずはデータバインディングをやってみます。
データバインディングをするには要素をマスタッシュ(二重の中括弧)で囲みます。
scriptタグにはdata()で要素をreturnしてあげます。

App.vue
<template>
  <div id="app">
    {{ msg }}
  </div>
</template>

<script>

export default {
  name: 'App',
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

Vueはホットリローディングしているので、scriptタグ内のmsgを変更するとそれにしたがってテキストが自動変更されます。上のコードの場合ページ上にData-binding!の文字だけ表示されるはずです。

コンポーネント

コンポーネントはVueファイルをそれぞれ部品のように扱うことで、分離して開発することが可能な機能です。使うには呼び出したいVueをscriptタグ内でimportしてからcomponentsに登録します。templateタグ内でタグとして使用できます。

App.vue
<template>
  <div id="app">
    {{ msg }}
    <HelloWorld></HelloWorld>
  </div>
</template>

<script>
import HelloWorld from "./components/HelloWorld.vue"

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

HelloWorld.vueに書かれていた内容が表示されたかと思います。

フォームとの同期

v-modelを使うことで入力や選択をDOMに反映させることができます。

App.vue
<template>
  <div id="app">
    {{ msg }}
    <input type="text" v-model="msg">
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

フォームに入力したテキストが即座に反映されていることが確認できるかと思います。
Vueにはまだまだ機能がありますが、列挙するとキリがないので、次回からポートフォリオ作成編に入っていきます。次回もよろしくお願いします。

次回

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptをまともに触ったことない初心者がVueを利用したポートフォリオサイトを作成する 1

JavaScriptはRailsでアプリを作成する際に数行しか書いたことがありませんでしたが、Vueを触り始めて5日ほどで簡単なポートフォリオサイトなら作成することが出来ました。その過程について書いていきます。

私のポートフォリオサイト

事前準備

手始めにVueで開発する為に必要なものをインストールしていきます。

コマンドラインインタフェースをインストールします。

$ npm install -g @vue/cli

次にプロジェクトを作成します。今回はリファレンスの多さからVue2で書いています。

$ vue create my_portfolio

プロジェクトの作成が完了した後、一旦立ち上げてみます。

$ cd my-portfolio
$ npm run serve

以下のメッセージが表示されましたら、localhost:8080にアクセスします。

Your application is running here: http://localhost:8080

EmsM2m2UcAAgVq3.jpeg

導入成功です。

次にVue.js Devtoolsを導入していきます。これはChromeブラウザの拡張機能で、Vueで書かれたコード内のデータを手っ取り早く確認することが出来る便利な機能です。導入した後Devtoolを開くとVueと書かれたタブが表示されているはずです。Vueファイルが読まれている時はアイコンがアクティブになります。
スクリーンショット 2020-11-24 20.35.10.png

これにて事前準備は完了です。

Vueと仲良くなる

publicディレクトリのindex.htmlを見てみます。

public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

注目するべきはbodyタグ内の<div id="app">です。このappは一体何者でしょうか?Devtoolで確認してみると...スクリーンショット 2020-11-24 20.55.37.png

何やらdivタグの中でさらに展開されています。
この中身についてはApp.vueを見てみましょう。

App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

index.htmlのappはこれを呼び出しているみたいです。
さらにApp.vueの中でHelloWorldというcomponentを読むことで部品としてHelloWorldをタグに使用していますね。
Vueではindex.htmlをそのまま表示するのではなくてbuildを挟むことで./dist/index.htmlへと変換します。これをブラウザで表示している訳です。

次にmain.jsを確認します。

main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

まずVueを動かすためのインスタンスをvueから作り出し、先ほどの<div id="app">にマウントしていることがわかります。だからindex.html内でApp要素が表示されたんですね。

Vueの基本

データバインディング

まずはデータバインディングをやってみます。
データバインディングをするには要素をマスタッシュ(二重の中括弧)で囲みます。
scriptタグにはdata()で要素をreturnしてあげます。

App.vue
<template>
  <div id="app">
    {{ msg }}
  </div>
</template>

<script>

export default {
  name: 'App',
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

Vueはホットリローディングしているので、scriptタグ内のmsgを変更するとそれにしたがってテキストが自動変更されます。上のコードの場合ページ上にData-binding!の文字だけ表示されるはずです。

コンポーネント

コンポーネントはVueファイルをそれぞれ部品のように扱うことで、分離して開発することが可能な機能です。使うには呼び出したいVueをscriptタグ内でimportしてからcomponentsに登録します。templateタグ内でタグとして使用できます。

App.vue
<template>
  <div id="app">
    {{ msg }}
    <HelloWorld></HelloWorld>
  </div>
</template>

<script>
import HelloWorld from "./components/HelloWorld.vue"

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

HelloWorld.vueに書かれていた内容が表示されたかと思います。

フォームとの同期

v-modelを使うことで入力や選択をDOMに反映させることができます。

App.vue
<template>
  <div id="app">
    {{ msg }}
    <input type="text" v-model="msg">
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

フォームに入力したテキストが即座に反映されていることが確認できるかと思います。
Vueにはまだまだ機能がありますが、列挙するとキリがないので、次回からポートフォリオ作成編に入っていきます。次回もよろしくお願いします。

次回

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

getBoundingClientRectとツールチップ

この記事は

JavaScriptのgetBoundingClientRectを用いたツールチップについての解説です。

環境

Vue2
TypeScript(decorator使用)

背景

?アイコンにカーソルを当てた時だけに表示させる汎用的なツールチップを作りたい。

課題

?アイコンの直下に表示させたい。
画面をスクロールしてもそれに追従させたい。

解決

ツールチップコンポーネントtemplate

ソース

<template>
  <span
    @mouseover="activate"
    @mouseleave="deactivate"
  >
    <slot name="activator"></slot>
    <div
      :class="{'tool-tip__content--active': isActive}"
      :style="contentStyleObj"
      ref="content"
    >
      <slot></slot>
    </div>
  </span>
</template>

解説

mouseoverとmouseleaveでツールチップの表示有無処理を発火します。

<span
  @mouseover="activate"
  @mouseleave="deactivate"
>

?アイコンは親コンポーネントからslotしてきます。

<slot name="activator"></slot>

表示フラグが立っている時にツールチップ本体を表示します。

:class="{'tool-tip__content--active': isActive}"

script部で算出した座標をスタイルに反映します。

:style="contentStyleObj"

ツールチップコンポーネントscript

TypeScriptを用いています。

<script lang="ts">
import { Component, Prop } from 'vue-property-decorator';
import BaseView from '@/views/Base';

@Component
export default class ToolTipComponent extends BaseView {
  // ツールチップ表示フラグ
  isActive = false;

  // 表示位置
  contentPosition = {
    top: 0,
    left: 0
  }

  // ツールチップを表示する基準位置
  defaultPosition = {
    top: 40,
    left: 100
  }

  // ?アイコンにマウスオーバーした時の表示処理
  activate() {
    const target = event!.target as any;
    const rect = target!.getBoundingClientRect();
    this.contentPosition.top = rect.top + window.pageYOffset + this.defaultPosition.top;
    this.contentPosition.left = rect.left - this.defaultPosition.left;

    this.isActive = true;
  }

  // マウスオーバーの解除で非表示
  deactivate() {
    this.isActive = false;
  }

  // 座標をスタイルに指定
  get contentStyleObj() {
    return {
      top: `${this.contentPosition.top}px`,
      left: `${this.contentPosition.left}px`,
    }
  }

  // 一番上の親要素の直下にツールチップを配置する
  mounted() {
    const app = document.getElementById('app');
    app!.appendChild(this.$refs.content as any);
  }

  // コンポーネント破壊時にツールチップを削除する
  beforeDestroy() {
    const app = document.getElementById('app');
    app!.removeChild(this.$refs.content as any);
  }
}
</script>

解説

表示処理では、getBoundingClientRect()を用いてビューポートからの座標を取得します。
ツールチップの表示位置をデフォルトポジションとして調節し、表示座標を算出します。
その後、表示スタイルを有効にするためのフラグを立てます。

activate() {
  const target = event!.target as any;
  const rect = target!.getBoundingClientRect();
  this.contentPosition.top = rect.top + window.pageYOffset + this.defaultPosition.top;
  this.contentPosition.left = rect.left - this.defaultPosition.left;

  this.isActive = true;
}

そして算出された座標をスタイルに渡すゲッターを定義します。

get contentStyleObj() {
  return {
    top: `${this.contentPosition.top}px`,
    left: `${this.contentPosition.left}px`,
  }
}

ツールチップは画面描画時には非表示ですが、DOM内には配置しておきます。
一番外側の要素の子要素として配置します。
画面遷移時等のコンポーネント破壊時には配置した要素も忘れず削除するようにしましょう。

// 一番上の親要素の直下にツールチップを配置する
mounted() {
  const app = document.getElementById('app');
  app!.appendChild(this.$refs.content as any);
}

// コンポーネント破壊時にツールチップを削除する
beforeDestroy() {
  const app = document.getElementById('app');
  app!.removeChild(this.$refs.content as any);
}

利用コンポーネント

親コンポーネントでは?アイコンをactivatorスロットに配置。
またツールチップ内の文章はデフォルトスロットに配置します。

<ToolTip class="tool-tip" v-show="toolTipMessage">
  <template slot="activator">
    <img src="@/assets/img/icon/icon-help.png" alt="help">
  </template>
  <div class="tool-tip__help-text">
    {{ toolTipMessage }}
  </div>
</ToolTip>

まとめ

少々トリッキーなコンポーネントとなってしまいましたが、getBoundingClientRectはこういう使い方もできる点は参考になりますね。
応用もできると思うので、座標を取って何かするような動作のコンポーネントに使うと良いかもしれません。
間違っている点、改善点あればご教授頂けますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vueでタブに表示されているタイトルをデフォルトから変更する

やりたいこと

chromeの上部に表示されているタイトルを変更したい
スクリーンショット 2020-11-23 10.34.19.png

環境情報

  • vue
    • 2.6.12

index.htmlを確認

index.htmlに定義があるので確認してみる
titleタブで表示されている内容が htmlWebpackPlugin.options.title となっている

htmlWebpackPlugin.options.title の値を定義するには、 vue.config.js を編集したら良い
※ vue cliでプロジェクトを作成

vue.config.jsを編集

以下の様に編集

module.exports = {
  pages: {
    index: {
      entry: "src/main.js",
      title: "ああああ"
    }
  }
}

無事変わりました
スクリーンショット 2020-11-24 20.40.24.png

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js と Laravel バリデーションの連携

はじめに

jQuery を極めすぎて、とんと食わず嫌いになっていた Vue.js を本格導入するにあたって、フォームのバリデーションをサーバーサイドの Laravel で実装することだけは譲れなかった。
まして、サーバーサイドとフロントエンドの両方で同じ処理(バリデーション)を、しかも異なる言語(php と javascript)で記述するなど論外。

  • Laravel v 6.0
  • Vue.js v 2.6.12
  • form-backend-validation v 2.4.0

パッケージの選択

Vuetify

  • リアルタイムバリデーションのパッケージ。
  • フォームエレメントを <v-form や <v-input のように記述し、Vue.js とは別の学習コストが必要。
  • そもそも、リアルタイムバリデーションを javascript で定義する気はないので却下。

FormVuelar

  • サーバーサイドバリデーションのパッケージ。
  • Vuetify の <-v-form が <ivl-from に変わるもので学習コストがさらに増えるので却下。

form-backend-validation

  • 同じくサーバーサイドバリデーションのパッケージ。
  • Vue.js の data に与える from オブジェクトを拡張するだけなので扱いやすい。
  • フォーム Blade が、通常の Laravel の記述に近い。
  • ララジャパンの記事 がわかりやすい。

fom-backend-validation の導入

共通処理の切り分け

ララジャパンの記事 の最後にあるように、フォームを持つすべての Blade に Vue.js 初期化のインラインスクリプトを繰り返し記述することは避けたいので、共通処理を切り分けてみた。

resources/js/app.js

  • Vue.js の初期化は各ページに任せるので app.js は最小限。
require('./bootstrap');

window.Vue = require('vue');

import Form from 'form-backend-validation';

window.Form = Form;

各 Blade の記述

  • フォームにはブラウザのバリデーションが発生しないよう novalidate を付与。
  • インラインスクリプトで定義するのは、フォーム内のカラム要素 data.fields だけ。
  • ページ固有の処理のため data に定義を追加したり、watch 等を追加してもよい。
@extends('layouts.app')

@section('content)
  (中略)
  <div class="alert" :class="messageClass" v-if="message">
    <a class="float-right close" v-on:click="clearMessage()">x</a>
      @{{ message }}
  </div>

  <form @submit.prevent="onSubmit" 
    @keydown="form.errors.clear($event.target.name);" novalidate>
    <div class="form-group row">
      <label for="email" class="col-md-4 col-form-label text-md-right">メールアドレス</label>
      <div class="col-md-6">
        <input id="email" type="email" v-model="form.email"
          class="form-control" :class="{ 'is-invalid': form.errors.has('email') }">
        <div class="invalid-feedback"
          v-if="form.errors.has('email')"
          v-text="form.errors.first('email')"></div>
      </div>
    </div>    
  (中略)
@endsection

@section('script')
  import {formBackendValidation} from "{{ mix('/js/libs.js') }}";
  formBackendValidation({
    data: {
      fields: {
        email:     "{{ $user->email }}",
        password:  "",
        name:      "{{ $user->name }}",
        is_active: {{ $user->is_active }},
      }
    }
  }, "{{ route('admin.user.update', $user) }}");
@endsection

resources/js/libs.js

  • 各ページから呼び出される共通処理。
  • ページ側で Vue.js に渡す定義が追加できるように、ディープマージしている。
  • ページ側で form を直接定義せずに fields で渡してるのは、将来の拡張の可能性のため(後段参照)。
/**
 * Vue.js with form-backend-validation の初期定義
 * 
 * @param {object} option data.fields を含む Vue へ渡すオプション
 * @param {string} url    フォームのポスト先URL
 */
export function formBackendValidation(option, url) {
  const app = new Vue(merge({
    el: '#app',
    data: {
      form: new Form(option.data.fields),
      message: '',
      messageClass: ''
    },
    methods: {
      onSubmit() {
        this.form['post'](url)
          .then(res => {
            if (res.redirect) {
              location.href = res.redirect
            } else if (res.message) {
              this.displayMessage(message, true);
            } else {
              this.displayMessage('更新しました。', true);
            }
          })
          .catch(res => this.displayMessage('エラーを確認してください。', false));
      },
      displayMessage(message, success) {
        this.messageClass = 'alert-' + (success ? 'success' : 'danger')
        this.message = message;
      },
      clearMessage() {
        this.message = '';
      },
    },
  }, option));
  return app;
}

/**
 * deep merge
 */
function merge() {
  return [].reduce.call(arguments, function merge(a, b) {
    Object.keys(b).forEach(function (key) {
      a[key] = (typeof a[key] === "object" && typeof b[key] === "object")
        ? a[key] = merge(a[key], b[key]) : a[key] = b[key];
    });
    return a;
  });
}

webpack.mix.js

  • libs.js を mix.js() で処理すると webpack 変換されて import できなかったので、app.js とは処理を分けた。
mix.js('resources/js/app.js', 'public/js')
   .sass('resources/sass/app.scss', 'public/css');

// モジュールで呼び出される js は webpack変換されないよう .scripts を使う
mix.scripts(['resources/js/libs.js'], 'public/js/libs.js');

mix.version();

resources/views/layouts/app.blade.php

  • CSRFトークン、app.js の組み込みは Laravel 初期通りに必須。
  • Balde で定義したインラインスクリプトの @yield ディレクティブを type="module" で追加。
  • コンテンツコンテナに id="app"
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- CSRF Token -->
  <meta name="csrf-token" content="{{ csrf_token() }}">

  <title>{{ config('app.name', 'Laravel') }}</title>

  <!-- Scripts -->
  <script src="{{ mix('js/app.js') }}" defer></script>
  <script type="module">
    @yield('script')
  </script>
  (中略)
</head>
<body>
  <div id="app">
    (中略)
    <main class="py-4">
      @yield('content')
    </main>
  </div>
</body>
<body>

コントローラ

バリデーションが通りDB更新後は、libs.js の then メソッドで返り値を受け取って分岐できるよう、リダイレクト先の URL を json で返す。

    /**
     * @param \App\Http\Requests\UserPost $request
     * @param \App\Models\User            $user
     * @return \Illuminate\Http\JsonResponse
     */
    public function update(UserPost $request, User $user)
    {
        $validated = $request->validated();
        $user->update($validated);
        return response()->json(['redirect' => route('admin.user.show', $user)]);
    }

以上で、フォームのサブミットボタンをクリックしたときに、画面遷移を伴わないバリデーション処理が実現できた。万歳。

Laravel によるリアルタイムバリデーションの模索

this.form の変更を watch して1項目ずつサーバーに問い合わせることができれば、一元管理されたバリデーション定義で、リアルタイムバリデーションも実現できるのでは?・・・と、試してみた。

  • libs.js に computedapp.$watch のテストコードを追加した。
/**
 * Vue.js with form-backend-validation の初期定義
 * 
 * @param {object} option data.fields を含む Vue へ渡すオプション
 * @param {string} url    フォームのポスト先URL
 */
export function formBackendValidation(option, url) {
  const app = new Vue(merge({
    el: '#app',
    data: {
      form: new Form(option.data.fields),
      message: '',
      messageClass: ''
    },
    methods: {
      onSubmit() {
        this.form['post'](url)
          .then(res => {
            if (res.redirect) {
              location.href = res.redirect
            } else if (res.message) {
              this.displayMessage(message, true);
            } else {
              this.displayMessage('更新しました。', true);
            }
          })
          .catch(res => this.displayMessage('エラーを確認してください。', false));
      },
      displayMessage(message, success) {
        this.messageClass = 'alert-' + (success ? 'success' : 'danger')
        this.message = message;
      },
      clearMessage() {
        this.message = '';
      },
    },
    computed: {
      computedForm() {
        return JSON.parse(JSON.stringify(this.form))
      }
    }
  }, option));

  app.$watch('computedForm', function (newVal, oldVal) {
    for (let key in app.fields) {
      if (newVal[key] != oldVal[key]) {
        console.log(key, newVal[key], oldVal[key]);
      }
    }
  }, {deep: true});

  return app;
}
後略

deep: true で定義した this.form 一括 watch では、2つの引数 newVal と oldVal の値が同じになってしまうため、一旦 computed で計算した値を watch する必要があったが、これで変更があったカラムを抽出することが可能となった。

次の手順としては・・・

  • フロント側
    1. 変更がなかったカラムを disabled にし、変更があったカラムだけでフォームポストする。
    2. または、変更があったカラム名を、hidden 要素としてフォームに追加したポストする。
  • Laravel 側
    • 2の場合は、変更があったカラムのポストだけに Laravel 側で絞り込む。
    • バリデーション定義にはすべて somtimes を加え、ポストされたカラムに対してだけ判定を返す。

2の方法では、フォームに file 要素があった場合に負荷が大きすぎるので、1の方法がベターか。
なんとか実現の方向性が見えてきたように気がした・・・この時点では。

リアルタムバリデーション計画の行方は・・・

1文字入力するたびにサーバーへ通信するわけにはいかないので、v-model に修飾子 lazy を追加する。これにより入力を終えてフォーカスが移ったときの判定になる・・・はずだった。

<input id="email" type="email" v-model.lazy="form.email"
  class="form-control" :class="{ 'is-invalid': form.errors.has('email') }">

ところがなんと、この追加で入力要素において1文字以上入力できない問題が発生した!

ネイティブの Vue.js では生じない症状なので、form-backend-validation 固有の問題であるよう。この問題が解決するまでは、リアルタムバリデーションはお預けとなった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

続・「シューティングゲームの当たり判定をQRコード読み取りでやってみた」やつをスマホで使えるようにした。

スマホでシューティングゲーム

まずは前回の記事を参照してください。

シューティングゲームの当たり判定をQRコード読み取りでやってみた

構成(前回との変更点)

image.png

射撃:Webアプリ内のボタン押下 → obnizのボタン押下
カメラ:フロントカメラ → リアカメラ(背面カメラ)

動かしてみたいかたは↓のURLをクリック

https://nervous-ardinghelli-c10e69.netlify.app
※リアカメラがない端末では動きません
※スマホの場合は横向きにしてください
※ご自身のobnizをご用意ください

■obnizについては↓を参照
https://obniz.com/ja/products/obnizboard

コード

See the Pen QRコードで当たり判定。トリガーはobniz by sawa (@sawakoshi_yy) on CodePen.

課題

・スマホでフルスクリーンにできない

<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

上のタグを入れてもブラウザのヘッダー・フッターが消えない
 →Webアプリに接続したら、フルスクリーンでカメラを表示させたい

・射撃時に音がでない。

var audio = new Audio('https://soundeffect-lab.info/sound/battle/mp3/beamgun1.mp3');
audio.play();

上を追加しても音がでない。

・射撃時にヒットしたら、LINEnotifyで通知をしたいがCORSのエラーを解消できない
↓記事参照
CORSのエラーが出たけど解消できなかったときのメモ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsとReact.js

私について

・文系
・プログラミング経験1年
・サマーハッカソン優勝
・大手IT企業を目指して就活中

なぜ記事を書くのか?

今まで楽しくてコードはたくさん書いてきたが、ほとんどが動いたら良いやと仕組みやシステムについてあまり考えてこなかった。一流のエンジニアを目指す上で、なんとなくではなく、根拠をもって技術選定ができたりエラーを解決をできるように記事を書いていく。

技術選定の仕方

・自分のやりたい事が実現しやすい
・情報量が多い

プログラミングはあくまでも手段でしかなく、プロダクトの価値を最大化させることこそが目的である。またプログラミング言語は、基礎文法は少し変わるぐらいで大した差はない。言語に縛られるよりも、自分が言語に合わせて勉強していくほうが、プロダクトの価値も開発効率も上がる。

Vue.jsとReact.jsどっちが簡単?

結論から言うと、vue.jsのほうが圧倒的に簡単です。

Vue.jsのメリット

①SFC(Single File Component)
HTML/JavaScript/CSSが区別して管理でき、Web制作上がりの僕のような人にはとっつきやすい。

②Vue CLIが便利
TypeSclipt、ESLint などの plugin の使用などをコマンドラインの指示に従って簡単に導入できる。

③HMR(Hot Module Replacement)
ブラウザ画面の再描画(リロード)すること無しにコードの変更をブラウザに適用してくれるので、手間が減って開発が楽になる。

上記3つの点が、僕のような初学者でも簡単に感覚的にコードを書くことができ、かつ多くのことが容易に実現できる。

では、React.jsのメリットって?

①主にクラスベースで実装するため、大規模開発に適している。

②typescriptとの組み合わせが良い。

最後に

手軽さという面でVue.jsを選定してきましたが、これからtypescriptがもっと流行すること、会社での大規模開発に携わるであろうことから、Reactの開発もしていこうと考える。
最後まで読んでいただき、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npmを特定のポート番号で動かしたい

ご無沙汰してます、おおのんです。
一時的に指定したポート番号でnuxtアプリを立ち上げる方法。

ターミナルでnuxtアプリを5000番で動かす。
npm run dev -- --port 5000

はい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【個人開発】DjangoRESTFrameworkとVue-CliでSPAでアーティスト向けのアプリを作成した話【DRF+Vue.js】

はじめに

こんにちは。
2年目ペーペー新人エンジニアです。
今回は、個人開発したアプリをついにリリースしたので、
それを記事にしようとおもいます。

作成物

アプリ:https://paintmonitor.com
今回作成したのは、『PaintMonitor』というアプリです。
top-cap.jpg

名前も当初かなり迷走して、結局わかりやすく、
Paint(描いて)Monitor(見える化)にしました。
アプリの特徴としては、
描いて(Paint)、解析して(Monitor)、共有する(SNS)。の3点です。

システム構成

Drainer.jpg

システム構成は、一番使い慣れているUbuntuServerで、
開発言語はDjangoのDjangoRESTFrameworkを利用しました。
DjangoRESTFrameworkは、とても使いやすく、直感的に理解しやすかったです。
認証回りの構築は、『現場で使えるDjangoRESTFramework』の本を参考にしました。
このDjangoの現場で使えるシリーズは、本当に現場で使えそうな内容が豊富なのでおすすめです。

フロント周りは、最近仕事でもよく使うVue.jsを利用しました。
Vue.jsは、開発コストは低く使いやすいので、
SPAの作成にはとてもよかったです。
Webpackで、利用すればhotタイミングでコードが反映されるので開発スピードを上げてくれました。
開発当初は、jQueryで開発していましたが、ネイティブ展開もしたかったので、
モダンな構築に作り変えました。

DBはいつものMySQL。ストレージはAWSを利用しています。
APサーバーはGunicornを利用しています。
シングルパフォーマンスでは、uWSGIのほうが優れていますが、
システムの安定度を重視したい場合は、Gunicornがいいみたいです。
WEBサーバーはいつものNginxでリリース、それをLetsEncriptで暗号化しました。

個人開発で作ろうと思ったきっかけ

ある程度WEBができるようになって、何か作りたいなぁと常に何かしらは作っていました。
アートとテクノロジーが好きなので、
それらを合わせてアーティスト向けのアプリを作ろうと思ったのがきっかけでした。
WEBで描いて、練習して、SNSにするサービスっていいなと思い、作り始めました。
最初は、tensorflowを利用して、絵をスコア化して、レート戦の
バトルをメインとするアプリを作ろうと考えたのですが、
普通に絵の練習するアプリのほうが実用的だなと思って、練習をメインとするアプリにしました。
バトルやそのほかの機能は後からのアップデートで追加していければいいかなと。

個人開発で苦労したところ

全部ひとりで考えなくてはいけなくて、大変な作業量になる。

単純に作業量が暴力的なので、仕事の合間に開発するには少ししんどかったです。
会社へ行く電車の乗り降りの時間、座れる時間帯に電車にのり、Surfaceを広げてカタカタと作業してました。
でもコードを書くのは好きなので、なんだかんだ充実した日々を過ごせました。

デザインや仕様を考えるのが大変。

私は考えるより手を動かす派なので、デザインに関しては、FIGMAやXD等は使わずに、簡単に画面をスケッチしてデザインしました。
私の場合は、好きなWEBサービスのPinterestを参考したりしました。
仕様は、適当に考えて、修正があればその都度修正する、みたいなチーム開発なら怒られるであろう進め方をしていました。

こだわりすぎて時間がかかる

私自身よく絵を描くので、ペイント部分はかなりこだわって作りました。
WEBでありながら実用的なペイントツールを作るのに4,5回新規でペイントアプリを作り、
開発の7割はこのペイントツールに費やしたせいで、開発に半年以上かかりました。

最後に

個人開発は大変でしたが、
個人のポートフォリオにもなるし、なによりめちゃくちゃ勉強になります。
開発力をつける一番の方法は、アプリを作ってリリースするのがいいのでは?
と感じました。
長々と話してしまいましたが、
作成したPaintMonitor
昨日の夜にリリースしたばかりなので、まだ利用者が全然いません。
ぜひ触ってあそんでもらえたらうれしいです。
今後もアップデート頑張っていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

S3の画像データを無理やりBlob型に変換する方法

なにこれ

S3の画像データをBlob型にしたい!っていう珍しい状況になった時の解決策です。
Vue(Nuxt)で下記のように'Content-Type': 'multipart/form-data'で送りたい!って時に遭遇する可能性が高そうです。
test.js
const response = await this.$axios.put(`/hoges/${this.hoge.id}`, req, {
headers: {
'Content-Type': 'multipart/form-data'
}
})

どうやるか

axiosを利用します

axiosでs3のURLをgetして、その時にresponseをblobで受け取ります。
受け取ったresponseを使って、new BlobでBlob型に変換します。

test.js
import axios from 'axios'

// 〜省略〜
          const req = new FormData()
          try{
            const response = await axios.get(S3のURL, { responseType: 'blob' })
            const blobData = new Blob([ response.data ], { type: "image/png" })
            req.append(`image`, blobData)
          } catch (error) {
            console.error(error.response)
          }

その後は肉なり焼くなり好きに処理してください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueでJestで画面サイズの単体テストをやってみる

概要

画面サイズの変更を単体テストでテストしたかった。

環境

  • Vue3
  • Typescript
  • Jest

コード

まずはテスト駆動ぽくやるために、テストコードの方を記述する

describe("LogListContent", () => {
    test("画面のサイズによって、フラグが切り替わるか", (): void => {

        const wrapper = mount(LogListContent);

        //とりあえずPCサイズ
        resizeWindow(1440);

        //PCサイズの画面判定なので、mobileモードは偽値になっているはず
        expect(wrapper.vm.isMobileMode).toEqual(false)

        //mobileサイズをiPhoneSE2にする
        resizeWindow(740);

        //PCサイズの画面判定なので、mobileモードは真になっているはず
        expect(wrapper.vm.isMobileMode).toEqual(true)
    });
});

const resizeWindow=(x:number) =>{
    window = Object.assign(window, { innerWidth: x });
    window.dispatchEvent(new Event("resize"));
}

テスト対象のコンポーネントにて、isMobileModeでモバイルで見ているかPCなのかの真偽値を持たせて管理したいと思います。
resizeWindow関数で、画面サイズの値を更新しています。
Typescriptだと、window.innerWidthへ値を代入して更新しようとすると、読み取り専用だぞと怒られるので、
スマートではありませんが、Object.assingを使って更新しています。

  setup() {
    const isMobileMode = ref(false);

    /**
     * 画面サイズ変更時に変更イベントを検知しモバイルかどうか判断する
     */
    const changeWindowSize = () => {
      750 >= window.innerWidth
        ? (isMobileMode.value = true)
        : (isMobileMode.value = false);
    };

    //画面サイズが変更された時に発火するイベントを登録する
    //Vue3からcreatedは明示的に書かずにsetupに直接コードするためここにかく
    window.addEventListener("resize", changeWindowSize);

    //インスタンスが破棄された時に画面サイズ変更でのイベントを破棄する
    //破棄イベントは明示的に書かなくてはいけないので、Vue3でのdestroyedの役目であるonUnmountedを利用する
    onUnmounted(() => {
      window.removeEventListener("resize", changeWindowSize);
    });

    return {
      isMobileMode
    };
  },

Vueのコンポーネント側のコードはこんな感じになります。
三項演算子を使っている条件文のところで、750 >= window.innerWidthと画面幅をハードコーディングしていますが、
実際に使う場合には、定数を使ったりするなどでハードコーディングしないようにしましょう。
今回は説明用なのでハードコーディングしちゃってます。

Vue2でのライフサイクルにおける、createdにおける役割は今回は明示的に書かなくてもよくなりました。
setup関数の中がそれを担っているようなので、Vue2のような

  created(){
    window.addEventListener("resize", changeWindowSize);
   }

とは書かずに、ダイレクトにsetup関数に書いています。
このままだとこのコンポーネントのインスタンスが破棄されても、イベントは残り続けるので、イベントを削除する必要があります。
以前なら、destoryedで行っていましたが、Vue3ではそれがonUnmountedへと変更になりました。

これで画面サイズの変更を単体テストでテストすることができました。
Vue3になっても、単体テストのコードはVue2からあまり変更がないため、再度の覚え直しが少なくて楽かなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む