現在プライベートで作っているWEBサービスのフロントエンドを作るにあたって、Vue.js+TypeScriptを試してみることにした。
基本的には公式サイトを参照させていただいた。
作るもの
メモを編集する画面を作成する。
- 初期表示
- メモ表示欄とEDITボタンが表示されている
- メモ表示欄にはメモ取得APIで取得したメモの内容を表示する
- APIで取得する内容はこんな感じ。
{ id: 123, body: "これはサンプルです"}
- EDITボタンクリック
- 表示欄+EDITボタンを非表示にし、編集欄(textarea)+SAVEボタンを表示する
- SAVEボタンクリック
- 編集欄+SAVEボタンを非表示にし、表示欄+EDITボタンを表示する
- メモ保存APIでメモを保存する
プロジェクト作成
npmのインストール
以前記事を書いたのでこちらを参照のこと。
node.jsおよびnpmのTips
vue-cliのインストール
npm install -g @vue/cli
プロジェクトを作成
以下のコマンドで対話型で作成するプロジェクトの内容を決めていく。
vue create frontend
以下の設定で作成した。
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Linter
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript for auto-detected polyfills? Yes
? Pick a linter / formatter config: Standard
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i
> to invert selection)Lint on save
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedica
ted config files
? Save this as a preset for future projects? (y/N) N
- TypeScriptをonに。
- 今回はRoutingは必要ないためRouterは入れてない。
-
TypeScriptチームがEslintを自分たちのリポジトリに正式採用した)とのことなので、
Eslint Standard
をlinterに指定している。
アプリ起動
サーバを起動する。
cd frontend
npm run serve
サイトにアクセスすると、とりあえずサンプル画面が表示される。
http://localhost:8081/
自動生成された設定ファイル群の説明
.browserslistrc
異なるフロントエンドツールにおいて、ターゲットブラウザやNode.jsのバージョンをシェアするための設定を記載するもの。
下記設定は、ブラウザの利用シェアが1%より大きく、かつ、直近2バージョンをターゲットとして指定している。
詳細はこちらを参照のこと。
> 1%
last 2 versions
.editorconfig
EditorConfigは異なるエディタやIDEで一貫したコーディングスタイルを維持するための設定を記載するもの。
ファイル保存時にフォーマットされるようにするには以下の作業をする必要がある。
- Editor Config for VS Code拡張をインストール
- Code -> Preference -> Settings -> Text Editor -> Formatting -> Format on Saveをチェック
- これをやると、保存時にindentもやってくれるが、
"
->'
変換や行末に;
追加などいらんことをするようになってしまう - こちらの記事のようにすれば回避できる模様
- ただ、lintの定義が増えてしまうので今回自分は Format on Save は行わないことにした
- これをやると、保存時にindentもやってくれるが、
下記設定は、特定の拡張子において、インデントのスタイルや行末のスペース、最終行の改行などの扱いを設定している。
詳細はこちらを参照のこと。
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
.eslintrc.js
EsLintは、JavaScriptを実行せずに静的にJavaScriptコードの問題を発見するためツール。
EsLintを実行する方法はいくつかある。
-
npm run serve
を実行する。サーバが立ち上がりファイルが保存されるたびにeslintが走る。 -
npm run lint
を実行する。 - VS Codeの
ESlint
拡張をインストールする(デフォルトでSettingsのEslint: Enable
になっている。)
下記設定の意味はコード上にコメントで記載。詳細はこちらを参照のこと。
module.exports = {
root: true, // このフォルダがrootだという指定。つまり親フォルダの設定ファイルを探しに行かない
env: {
node: true // 事前定義されているグローバル変数をNode.jsグローバル変数にする。`browser`なども指定可能。
},
'extends': [ // vue用の拡張。基本これに従っておけばいいのだと思う。
'plugin:vue/essential',
'@vue/standard',
'@vue/typescript'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', // productionの場合はerrorレベルで出力。それ以外は普通に出力
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: '@typescript-eslint/parser' // typescript用のparserを指定
}
}
.gitignore
git管理対象外のファイルを指定してくれている。
babel.config.js
BabelはJavaScriptのコンパイラ。古いブラウザでES2015以降の新しいバージョンのJavaScriptを動かせるよう、後方互換性のあるバージョンのJavaScriptへコンパイルしてくれる。
ここではvue用のプリセットが設定されている。詳細はこちらを参照のこと。
module.exports = {
presets: [
'@vue/app'
]
}
package.json
npmでインストールする対象のJavaScriptパッケージとタスクの設定。
詳細はこちらを参照のこと。
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve", // アプリを起動するタスク
"build": "vue-cli-service build", // production用にjavascriptをコンパイルするタスク
"lint": "vue-cli-service lint" // lintを実行するタスク
},
"dependencies": {
"core-js": "^2.6.5",
"vue": "^2.6.10",
(省略)
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.7.0",
(省略)
}
}
postcss.config.js
PostCSSはJSプラグインでスタイルを変換するためのツール。変数はmixinをサポートしており、将来のCSSシンタックスでcssを記述できるらしい。
今回はVuetifyを利用するので、独自にスタイルを定義しない方針。
詳細はこちらを参照のこと。
module.exports = {
plugins: {
autoprefixer: {}
}
}
tsconfig.json
TypeScriptに関する設定。詳細はこちらを参照のこと。
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true, // strictモード
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true, // mapファイルを出力する
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*" // ランタイムにおいて、"@/hoge" -> "./src/hoge"としてパスを解決する
]
},
"lib": [ // コンパイルに含めるライブラリのリスト
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [ // コンパイル対象のファイル
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [ // 対象外にするフォルダ
"node_modules"
]
}
入れておいた方がいいVS Codeの拡張
- Vue
Vetur
拡張をインストールする -
Editor Config for VS Code
拡張をインストールする
実装Tips
やりたいこと別にTipsを追記していく方式にしようと思う。
Vueインスタンスに外部からパラメータを渡す
こちらを参考に、main.ts
にてComponentOptions
の定義をすれば良い。
下記サンプルは、url
というパラメータをオプションとして追加している。?:
で定義しないとエラーになるので注意。
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
// ここでComponentOptionsにurlというオプションを追加している
declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> {
url?: string
}
}
// urlというオプションに値を渡している。
new Vue({
url: 'http://rinoguchi.com/hoge/fuga',
render: h => h(App)
}).$mount('#app')
これにより、Vueインスタンスからいつでもoptionの値を取得できるようになる。
export default class App extends Vue {
private url?: string = this.$root.$options.url
}
子コンポーネントから親コンポーネントの処理を呼び出す
今回は、Loginコンポーネントでログインが完了したら、親のAppコンポーネントのloggedIn
プロパティをtrue
にするというケースを説明する。
まずは、子コンポーネントのLogin
コンポーネントにて、@Emit
を使ってsucceeded
イベントを定義する。
下記サンプルでは、login
ボタンをクリックしたら、ログイン処理が実行され、処理が終わったらthis.succeeded()
を実行している。
<template>
<div>
<button @click=login>login</button>
</div>
</template>
<script lang="ts">
@Component
export default class Login extends Vue {
@Emit() private succeeded () {}
login () {
// login処理が実装してある
(省略)
// login処理が終わったらsucceededを実行する
this.succeeded()
}
}
</script>
次に、親コンポーネントのApp
コンポーネントにて、Login
コンポーネントの@succeeded
イベントに対してloginSucceeded()
を紐づける。
これによりLogin
コンポーネントにてsucceeded
イベントが発火されたら、親コンポーネント側のloginSucceeded()
が実行されることになる。
<template>
<div id="app">
<template v-if=loggedIn>
<Memo>
</template>
<template v-else>
<Login @succeeded=loginSucceeded />
</template>
</div>
</template>
<script lang="ts">
export default class App extends Vue {
private loggedIn: boolean = false
loginSucceeded (): void {
this.loggedIn = true
private url?: string = this.$root.$options.url
}
}
</script>
axiosを使ってAJAX通信を行う
axiosを使ってAPIを呼び出す。
今回は、mounted
にてメモを取得するAPIを呼び出し、その結果を画面に描画するようにしている。
まずは、依存関係を追加する。
npm install axios --save
- templateでは、
v-if
を使ってAPIでメモを取得するまではloading...
を表示し、取得したらmemo
を描画している。 - scriptでは、
@Prop
で親コンポーネントから渡ってきたmemoId
をプロパティとして定義している。 - ライフサイクルフックの
mounted
にてAPIを呼び出している。- 200以外が返ってきたらalertを表示してアーリーリターンする。
- 200の場合は、responseデータの
memo
をプロパティのmemo
に設定している -
finally
でthis.loading = false
を設定し、メモが表示される。
<template>
<div>
<template v-if=loading>
<pre>loading...</pre>
</template>
<template v-else>
<pre>{{ memo }}</pre>
</template>
</div>
</template>
<script lang="ts">
export default class Memo extends Vue {
@Prop() private memoId!: string
private loading: boolean = false
private memo?: string
mounted (): void = {
axios
.get(`http://rinoguchi.com/memos`, { params: { id: this.memoId } })
.then(response => {
if (response.status !== 200) {
alert('api call error')
return
}
this.body = response.data.body
})
.finally(() => {
this.loading = false
})
}
}
typescriptを使っているのでModelクラスを定義してみる
今回は、id
とbody
の二つのプロパティを持つMemoModel
を定義してみようと思う。
いくつかの定義パターンが考えられるので、解説しようと思う。
1. プロパティがundefined
でもOKな場合
class MemoModel {
id?: number
body?: string
}
const memo = new MemoModel()
console.log(memo.body) // -> undefined
memo.body = 'xyz'
console.log(memo.body) // -> 'xyz'
2. プロパティがundefinded
だと困るけど、初期値を設定できる場合
class MemoModel {
id: number = -1
body: string = 'default'
}
const memo = new MemoModel()
console.log(memo.body) // -> `default`
memo.body = 'xyz'
console.log(memo.body) // -> 'xyz'
3. プロパティがundefinded
だと困るけど、初期値を設定できない場合
class MemoModel {
id: number = -1
body: string = 'default'
constructor(theId: number, theBody: string) {
this.id = theId
this.body = theBody
}
}
const memo = new MemoModel(123, 'xyz')
console.log(memo.body) // -> `xyz`
今回は、プロパティがundefinded
だと困るけど、初期値は設定しても問題なかったので、2.のパターンで実装した。
もちろん、Modelクラスは別ファイルで定義して、importして使っても良い。
<template>
<div>{{ memo.body }}</div>
</template>
<script lang="ts">
class MemoModel {
id: number = -1
body: string = 'default'
}
@Component
export default class Memo extends Vue {
private memo: MemoModel = new MemoModel()
mounted (): void {
axios
.get(`http://rinoguchi.com/memos/latest`)
.then(response => {
if (response.status !== 200) {
alert('api call error')
return
}
this.memo.id = response.data.id
this.memo.body = response.data.body
})
.finally(() => {
this.loading = false
})
}
}
環境変数を読み込む
今回はvue-cliを使っているので、こちらを参考にした。
以下の記述がほぼ全てを表していると思う。
.env # loaded in all cases
.env.local # loaded in all cases, ignored by git
.env.[mode] # only loaded in specified mode
.env.[mode].local # only loaded in specified mode, ignored by git
今回は、developmemt
モードで動かしている際の環境変数を定義した。
変数名はVUE_APP
で始まっている必要があるので要注意!!
APIのベースURLは、.env.development
にて管理することにした。
VUE_APP_BASE_URL=https://rinoguchi.com/
Google OAuth認証のクライアントIDは、センシティブな情報なのでGITにcommitしたくない。そのため、.env.development.local
にて管理することにした。
VUE_APP_GOOGLE_CLIENT_ID=***********.apps.googleusercontent.com
あとは、process.env
を使ってどこからでも利用できる。
console.log(process.env.VUE_APP_BASE_URL)
console.log(process.env.VUE_APP_GOOGLE_CLIENT_ID)