20201215のvue.jsに関する記事は12件です。

【Vue.js】Local Storageでデータを永続化させる

Local Storageとは

JavaScriptを使ってWebブラウザにデータを保存できる仕組みです

5~10MB程度のデータを永続化できます

保存された内容はChrome DevToolsで確認できます
スクリーンショット 2020-12-15 16.33.06.png

今回はVue.jsで見ていきます

保存する

保存するにはlocalStorage.setItem()を使用します

第1引数にキー
第2引数にバリュー
を渡します

<template>
  <div class="container">
    <button @click="set()">保存</button>
  </div>
</template>

<script>
export default {
  methods: {
    set() {
      localStorage.setItem('name', '田中')
    }
  }
}
</script>

簡単に「保存ボタン」が押されたらデータを保存するようにします

demo


第2引数に複数の値を渡す場合はJSON.stringify()を使用しJSON文字列へ変換する必要があります

<template>
  <div class="container">
    <button @click="set()">保存</button>
  </div>
</template>

<script>
export default {
  methods: {
    set() {
      localStorage.setItem('obj', JSON.stringify({
        id: 1,
        name: 'tanaka',
        age: 20
      }))
    }
  }
}
</script>

demo


取得する

データを取り出すにはlocalStorage.getItem()を使用します
現状JSON形式でデータが保存されているのでJSON.parse()でJavaScriptのオブジェクトに変換する必要があります

mountedにデータを取得する記述を用意しレンダリングします

<template>
  <div class="container">
    <button @click="set()">保存</button>
    <p>{{this.info.id}}</p>
    <p>{{this.info.name}}</p>
    <p>{{this.info.age}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      info: {}
    }
  },
  mounted() {
    this.info = JSON.parse(localStorage.getItem('obj'))
  },
  methods: {
    set() {
      localStorage.setItem('obj', JSON.stringify({
        id: 1,
        name: 'tanaka',
        age: 20
      }))
    }
  }
}
</script>

スクリーンショット 2020-12-15 23.33.07.png

永続化されているのでリロードしても消えません

削除する

最後です

localStorage.removeItem();で引数に渡したキーに紐づく値を削除できます。

また、localStorage.clear();で全ての値を削除できます

localStorage.removeItem('obj');

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

【写真とコード付き】Vue.jsでフェードイン・フェードアウトする簡単は方法


はじめに

この記事はVue.jsの基本は抑えられている程で、話を進めていきます。もしVue.jsを初めて触ると言う方は、こちらの記事を参照していただければと思います。

⬇️【写真とコード付き】Vue.jsの構築から基本的な書き方まで1から解説【超初心者向け】
https://qiita.com/yuki4839/items/62f40564e3f4c8dbfc51

さて今回は、Vue.jsでフェードイン・フェードアウトの仕方について、ご紹介させていただこうと思います。

早速いきましょう。



実行環境

使用ツール、デバイスはこちら

  • Google chrome
  • Mac OS Catalina
  • Visual Studio Code


また今回使用するディレクトリ階層はこちら。

ディレクトリ階層
─ root(任意のディレクトリ)
│
├─ index.html
│
├─ css
│   └ style.css
│
└─ js
    └ main.js


各ファイルの初期値はこちら。

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <link rel="stylesheet" href="./css/style.css">
</head>

<body>

  <div id="web">
    <p>
      {{ context }}
    </p>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
  <script src="./js/main.js"></script>
</body>

</html>
style.css
/* 出力結果を見やすくするためのスタイルです */
body {
  background-color: #add8e6;
}

#web {
  background-color: #fff;
  margin: 20px;
  padding: 20px;
  width: 300px;
}
main.js
const web = new Vue({
  el: '#web',
  data: {
    context: `Hello Vue.js!`
  }
})


現時点での出力結果はこちら。

スクリーンショット 2020-12-12 21.13.46.png



transition

単刀直入に言うと、フェードイン・フェードアウトを行うには、マウントしたHTMLタグ内で transitionタグ を利用することによって作成することができます。

まずは実際のコードをご覧ください。

index.html
<!-- headタグ省略 -->
<body>

  <div id="web">
    <button v-on:click="show=!show">
      Click
    </button>
    <transition>
      <p v-show="show">
        This sentence is a Transition.
      </p>
    </transition>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
  <script src="./js/main.js"></script>

</body>
style.css
body {
  background-color: #add8e6;
}

#web {
  background-color: #fff;
  margin: 20px;
  padding: 20px;
  width: 300px;
}

.v-enter-active,
.v-leave-active {
  transition: 3s;
}

.v-enter,
.v-leave-to {
  opacity: 0;
}
main.js
var web = new Vue({
  el: '#web',
  data: {
    show: false
  }
})

手順

①まず v-on:click="show=!show" (falseとtrueの切り替え) でクリックイベントを作成。
②次に v-show="show" でクリックイベントの対象要素を指定。
③クリックイベントの対象要素を transitionタグ で囲む。

動作手順

①Vue.js の data が false に設定されているので、pタグ には display: none;当たっている。
②buttonタグ のボタンをクリックすると、pタグ の display: none;外れる。
③display: none; が外れると同時に、transitionタグ の配下の要素(上記なら pタグ)に、 class="v-enter-active v-enter"当てられる。
④イベントが終了したら、class="v-enter-active v-enter"外れる。
⑤再度 buttonタグ のボタンをクリックすると、transitionタグ の配下の要素(上記なら pタグ)に、 class="v-leave-action v-leave-to"当てられる。
⑥イベントが終了したら、class="v-enter-active v-enter"外れdisplay: none;当てられる。

動作確認

まずデフォルトはこちら。

スクリーンショット 2020-12-15 23.18.23.png

ボタンをクリックすると、ゆっくりフェードイン。

スクリーンショット 2020-12-15 23.19.45.png

しばらくすると描画完了です。

スクリーンショット 2020-12-15 23.18.39.png

再びボタンを押すと、ゆっくりフェードアウト。

スクリーンショット 2020-12-15 23.19.45.png

しばらくすると描画完了です。

スクリーンショット 2020-12-15 23.18.23.png




まとめ

今回は Vue.js でのフェードイン・フェードアウトの方法を解説いたしました。ぜひ実際にコードを書いて見てください!

最後まで読んでいただき、ありがとうございました!





筆者:yuki|IT業界のリアルな転職事情など発信|元トップ営業マン(訪販)→未経験からエンジニア転職へ
Qiita:https://qiita.com/yuki4839
Twitter:https://twitter.com/yukifullstack

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

Jestでdocument.activeElementを使えるようにする方法

はじめに

document.activeElementのテストをJestで書いていた時に沼にハマったので備忘録として残す

モック方法

mountでtemplateを定義した後、attachToでdocument.bodyを定義することでdocument.activeElementを使う事が出来るようになる

component.spec.js
const wrapper = ('component', () =>
    mount({
      template: `<div>
        <input type='text' />
        <input type='text' />
        <input type='text' />
      </div>`,
    },
    {
      attachTo: document.body,
    })

参考

https://qiita.com/ykhirao/items/8e8a9547a693c677813c
https://vue-test-utils.vuejs.org/ja/api/options.html#attachtodocument

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

【自分用】VueがIE11で動かなかった

WebpackとBabelを通しているがIE11で何も表示されなくなった

SCRIPT1002: Syntax error

Eval関数のところでエラーが出ていたので、webpackに以下の設定を追加してみたが、直らなかった

webpack.config.js
module.exports = {
    //...
    devtool: 'none'
}

原因はvueの読み込みところだった

script.js
import Vue from 'vue/dist/vue.esm.browser.min.js'

以下に変更

script.js
import Vue from 'vue'

resolve.alias の設定がされていないので、追加

webpack.config.js
module.exports = {
    //...
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
        }
    }
}

resolve.alias の指定は以下の記事を参考にした
https://blog.websandbag.com/entry/2020/08/07/190655

他のPJでは動いていたので、根本的な原因は別のところにあると思うが、とりあえず解決したのでここまで

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

Vue.js 3 入門 「ルーティング」(Vue Router)

はじめに

Vue.js 3 の ルーティング について、自分が学んだことを備忘録として記載します。
Vue.js に殆ど触れたことが無い方に少しでも参考になれば幸いです。
誤り等あれば、ご指摘頂けますと大変喜びます

プロジェクトの作成

まずは Vue CLI を用いてプロジェクトを作成します。
Vue CLI についてはこちらの記事を参照してください。

プロジェクトを作成するには、作成したいフォルダで以下のコマンドを実行します。
hello-routerはプロジェクト名です。任意のプロジェクト名を設定してください。

cd 任意のフォルダ
vue create hello-router

プリセットの選択

すると、以下のように利用するプリセット(プロジェクト設定)の選択を求められます。
まずは最低限の構成とするので「Manually select features」(手動で選択)を選択します。
versionはご自身のバージョンに読み替えてください。

Vue CLI v4.5.9
? Please pick a preset:
  Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
> Manually select features    

プロジェクトに組み込むモジュールを選択

プロジェクトに組み込むモジュールを選択します。
ここでBabelLinterに加えて、Routerを選択します。
[Space]キーで選択することができ、[Enter]キーで確定となります。

Routerを選択することによって、Vue Routerというルーティング機能を提供するライブラリが組み込まれます。

Vue CLI v4.5.9
? 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                                                                                                                                                                                                                                                                          

Vue.js のバージョンを選択

Vue.js のバージョンを選択します。
本記事では 3.x を選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with
  2.x
> 3.x (Preview)                                                                                                                                                                                              

Historyモードの有効/無効を選択

Historyモードの有効/無効を選択します。
基本的にY(有効)で問題ありません。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) Y   

Linter の設定を選択

Linter の設定を選択します。
今回は最低限のESLint with error prevention only(エラー防止のみ)を選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: (Use arrow keys)
> ESLint with error prevention only
  ESLint + Airbnb config
  ESLint + Standard config
  ESLint + Prettier                                                                                                                                                                                                                                                                                                          

続けて、Lintの実行タイミングの選択を求められます。
Lint on save(保存時)を選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Lint on save
 ( ) Lint and fix on commit      

設定情報の格納先を選択

BabelESLintの設定情報を個別の設定ファイルとするか、package.jsonにまとめるかを選択します。
個別の設定ファイルとしたほうが綺麗なのでIn dedicated config filesを選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
> In dedicated config files
  In package.json 

今回の設定を保存しておくかを選択

今回の設定を保存しておくかを選択します。
今回はあくまでお試しなのでN(保存しない)とします。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) N                                                                                                                         

プロジェクトの生成開始

ここまでの設定内容を元に、プロジェクトの生成が開始されるので、完了するまで待機します。
正常に完了すると、以下のような文言が表示されます。

Vue CLI v4.5.9
Creating project in 任意のフォルダ\hello-router.
Installing CLI plugins. This might take a while...

途中省略...

Running completion hooks...

Generating README.md...

Successfully created project hello-router.
Get started with the following commands:

 $ cd hello-router
 $ npm run serve

生成されたフォルダを確認

カレントフォルダに、指定したプロジェクト名のフォルダが生成されています。

image.png

アプリの実行

早速実行してみましょう。
上記のプロジェクト生成完了時の文言(Get started with the following commands:)にある通り、以下のコマンドを実行します。
プロジェクトルートに移動して、開発用のサーバーを実行するコマンドです。

cd hello-router
npm run serve

以下のような文言が表示されれば、開発用のサーバーが起動できています。
ブラウザを起動しhttp://localhost:8080にアクセスしてください。

  App running at:

途中省略...

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

ページの表示

以下のような画面が表示されれば、プロジェクトの作成は成功です。
開発用サーバーは[Ctrl] + [C]で終了することができます。

image.png

挙動を確認する

ここからルーティングの挙動を確認していきます。
http://localhost:8080にアクセスすると、以下のようにHome画面が表示されます。
つまり、Homeコンポーネント(/src/views/Home.vue)が呼び出されていることがわかります。

image.png

ここでアドレスバーから手入力でURLを変更します。
変更前は以下のようになっているかと思います。

http://localhost:8080

アドレスバーから手入力でURLを以下のように変更してください。
末尾に/aboutを追加します。

http://localhost:8080/about

変更後、Enterを押下すると、以下のようにAbout画面が表示されると思います。
つまり、Aboutコンポーネント(/src/views/About.vue)が呼び出されました。

image.png

ここで生じた2つの疑問について、仕組みを説明していきます。

  • http://localhost:8080にアクセスすると、なぜHomeコンポーネント(/src/views/Home.vue)が呼び出されるのか
  • /aboutを追加すると、なぜAboutコンポーネント(/src/views/About.vue)が呼び出されるのか

ルーティング

クライアントから要求されたURLに応じて、処理を受け渡すコンポーネントを決定する仕組みのことです。
ルーティング機能を提供するライブラリのことをルーターと呼び、Vue.js では標準的なルーターとしてVue Routerが用意されています。

ルーティングの流れ

リクエストのURLをアプリケーションが定義しているルート(Route)と照合します。
一致するルートが見つかった場合は、該当するコンポーネントが呼び出されます。

http://localhost:8080/about

従ってAboutコンポーネントが呼び出されるのは、上記のURLをルートと照合した結果、一致するルートが見つかったからということになります。

ルート

では、ルートはどこでどのように定義されているのでしょうか。
ルーティング情報として/src/route/index.jsに定義されています。

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

まず抑えておきたいのはcreateRouterメソッドによって、ルーティング情報を扱うルーターが生成されている点です。

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

createRouterメソッド

  • history
    • historyモードの基本情報
    • ほぼ定型
      • createWebHistory(process.env.BASE_URL)
  • routes
    • ルート
    • ここではroutes変数の値が割り当てられている

実際のルートが定義されているのは、routes変数です。

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]
  • path
    • リクエストパス
  • name
    • ルートの名前
  • component
    • ルーティングによって呼び出されるコンポーネント

従って、ここでは以下の2つのルートが定義されていることになります。

  • Homeルート
    /Homeコンポーネントが呼び出される

  • Aboutルート
    /aboutAboutコンポーネントが呼び出される

結果として、http://localhost:8080にアクセスするとHomeコンポーネントが呼び出され、/aboutを追加すると、Aboutコンポーネントが呼び出されることになります。

ルーターの有効化

なお、ルーターは/src/main.jsで有効化されています。

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

Vueインスタンスにライブラリを組み込むuseメソッドに、定義したルーター(router)を渡すことで有効化しています。

ルートを追加してみる

コンポーネントを追加

src/views/Article.vueを追加

src/viewsフォルダには、ルーティングに関わるコンポーネントを配置します。
src/componentsフォルダも同様の役割ですが、こちらにはより細かい部品を配置します。

<template>
    <div class="article">
        <h1>This is an article page</h1>
    </div>
</template>

ルートを追加

src/router/index.jsを以下のように修正

Articleルートを追加します。

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  // ↓ 追加
  {
    path: '/article',
    name: 'Article',
    component: () => import('../views/Article.vue')
  }
]

ついでにTOPのリンクも追加

src/App.vueを以下のように修正

ルーター経由のリンクは、aタグではなく、router-linkを利用します(to属性でリンク先を指定)。
また、ルーター経由で呼び出されたコンポーネントは、router-viewの領域に反映されます。
※ルーターを利用する場合、router-viewによる表示領域の確保は必須

<template>
  <div id="nav">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link> |
    <router-link to="/article">Article</router-link> <!-- 追加 -->
  </div>
  <router-view/>
</template>

結果を確認

以下のURLにアクセス

http://localhost:8080

Articleページへのリンクが追加されています。

image.png

以下のURLにアクセス または Articleのリンクを押下

http://localhost:8080/article

Articleコンポーネントが呼び出されていることが確認できました。

image.png

以上となります。
ありがとうございました。

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

vue.js google adsense

vue.js で adsense を使いたい。

参考
https://www.ketancho.net/entry/2018/05/22/080000

インスコ

npm install vue-adsense --save

コード改変

これをやらないとエラーが出る

vim node_modules/vue-adsense/main.js をして、
以下の内容に変更

node_modules/vue-adsense/main.js
import VueAdsense from './VueAdsense.vue'
export default VueAdsense

app.js に追記

app.js
import VueAdsense from 'vue-adsense'
// Google Adsense 用
Vue.component('adsense', VueAdsense)

コード挿入

使いたいコードに以下を追記して完了。
それぞれ、adsenseから拾ったデータを入れること。

hoge.vue
<adsense
        ad-client="ca-pub-xxx"
        ad-slot="2061149xxxx"
        ad-format="auto"
        full-width-responsive="true">
</adsense>


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

JSX etcの問題点

※この文章はベータ版です。

JSXの問題はロジックの中にHTMLなどのレイアウト用言語のコード片(orそのコード片を生成するコード)が埋め込まれてしまうことにあります。

(JSXはトランスパイラにより、JS的に問題のない専用の関数でHTMLなどのレイアウト用コードを組み立てるコードに変換されます。)

昔からロジック中でHTMLを生成できるライブラリ・フレームワークが多数、作成されてきました。

現在もRails etcのWEBアプリケーションフレームワーク(WAF)のコントローラにおいて、用意された関数(orメソッド)群を用いてHTMLを組み立てるコードを書くことが可能になっています。

しかし、メジャーWAFでは、ロジックの中にHTMLなどのレイアウト用言語のコード片(orそれを組み立てるコード)を埋め込むことはメンテナンスetcの観点から、特定のケースを除いて回避することが推奨されてきました。

ロジックの中にHTMLなどのレイアウト用言語のコード片(orそれを組み立てるコード)を埋め込むことを、何の制限もなく、許可してしまうと、コード中にレイアウト用言語のコード片が散逸し、コードの見通しが悪く、レイアウトの変更が発生した場合etcに、ロジックを追いかけなければならない事態になります。

ほとんどの言語やフレームワークで、ロジックの中にHTMLなどのレイアウト用言語のコード片を埋め込むことは推奨されず、HTMLなどのレイアウト用言語で記述するテンプレートにロジック・コードを埋め込むスタイルが採用されてきました。

ロジックの中に埋め込むよりは、テンプレートとして切り出し、ロジックの埋め込みを可能にするほうが問題が少ないと判断されてきたからだと思います。

ロジック中に埋め込む場合に比べ、テンプレートにロジックを埋め込む場合は多少のオーバーヘッドがあることが多いですが、パフォーマンスに対する要求がハイレベルな場合を除き、ロジック中に埋め込む場合におこり得る問題の前では有意なオーバーヘッドではありません。

(PHPはレイアウト言語にロジック・コードを埋め込むスタイルの仕様でありながら、ロジックの中にレイアウト用コード片を埋め込むこむこともできてしまいます、が、PHPのモダンなフレームワークでは、view用テンプレートでレイアウト用言語にロジック・コードを埋め込み、ロジック中にレイアウト用コード片を埋め込まないスタイルを採用しています。)

(HamlやSlimなど、HTMLタグを対応した関数に置き換え、テンプレート自体をレイアウト組み立てロジックとして記述し、実行時にHTMLに変換するテンプレート・エンジンもありますが、エンジニア視点のみで作成されているもので、汎用性も乏しいので、個人的には邪道だと思っています。)

HTML仕様においては、scriptタグにロジックのJSコードを集約し、scriptタグ外では、タグのイベント用属性でJSコード片を埋め込める以外はJSコードを埋め込むことはできません。

HTML仕様自体が、デザイン(レイアウト)とロジックを分離する形で設計されています。

(HTMLは元々、デザイン(レイアウト)用の言語であり、プログラマブルではありませんでした。デザイン(レイアウト)を動的に変更・制御したいというニーズが発生し、それに応える形でJavascriptが誕生し、scriptタグが実装され、イベント用の属性が追加され、HTMLは後付けでプログラマブルになったのです。)

JSXの信奉者?による文章には、VueやAngularのコード例として、入門記事では見かけるものの実際はVueやAngularにおいてバッドなコード例とされているようなコードを引用して、JSXの優位性を主張しているモノがありますが、

VueやAngularにおいてHTMLorHTMLテンプレート用にビルトインで用意されているディレクティブは、それぞれのグッドなコード例のように書けば、見通しのよいスマートなHTMLテンプレートを記述できます。 (VueやAngularに用意されているディレクティブはHTMLのタグに属性を追加する形でHTMLを拡張して、デフォルトのHTMLにはない機能を擬似的にHTMLに追加しています。)

HTMLテンプレートによるデザインとロジックの分離は、デザイナーとエンジニアの協業だけでなく、コード全体の見通しも良く、してくれます。

HTMLなどのレイアウト用言語のコードはHTMLファイルorテンプレートに集約し、ロジック内に埋め込まないスタイルが望ましいと個人的には思います。

JSX etcでレイアウト用言語のコードをロジックに埋め込む際には、規約で埋め込み可能な箇所を限定し、レイアウト用言語のコード片がロジック中に散らばらないようにすべきです。

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

Vue.js Vuex編(シンプルなカウンターアプリケーション)

実行環境

macOS Catalina バージョン 10.15.7
MacBook Pro 13インチ
Vue 2.6.12

カウンターアプリケーション

vuexを用いてカウンターアプリケーションを作成する。まず、プロジェクト内でvuexをインストールする。

npm install vuex

その後、srcディレクトリの直下にstore.jsを作成する。シンプルなVuexStoreを記述する。(main.js内でstoreをインポートしてvueインスタンス内に記述しないとvuexを使うことができないので注意。)

store.js
// vue,vuexをインポート
import Vue from 'vue'
import Vuex from 'vuex'

// vuexプラグインの使用の宣言
Vue.use(Vuex)

// シンプルなVuexStore
const store = new Vuex.Store({
    state: {
        count: 0,
    },
    mutations: {
        increment(state) {
            state.count++
        }
    }
})

// storeをエクスポート
export default store

stateに格納されているデータは直接変更することはできない。ミューテーションをコミットすることによって値の更新をすることができる。ミューテーションの中で定義する関数の引数にはstateが必要となる。
次にApp.vueの子コンポーネントとしてCount.vueを作成する。

Count.vue
<template>
    <div>
        <h3>カウンターアプリケーション</h3>
        <button @click="increment">+1</button>
        <p>{{ count }}</p>
    </div>
</template>

<script>
export default {
    computed: {
        count() {
            return this.$store.state.count
        },
    },
    methods: {
        increment() {
            this.$store.commit('increment')
        },
    }
}
</script>

コンポーネントから値を更新する際、this.$store.commit('increment')のようにしている部分がミューテーションをコミットしている部分である。

スクリーンショット 2020-12-15 10.23.09.png

最後に

今回は非常にシンプルなカウンターアプリケーションを作成した。次の記事でgetters,mutations,actionsについてまとめようと思う。

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

Vue.jsでパンくずリスト(手動でページ設定)

はじめに

Vue.jsでパンくずリストを実装すべく調べながら進めたので、メモ書きとして残しておきます。
自動でリストを作成する内容ではありませんので、よろしくお願いします。

仕様について

TOPページから一覧ページを挟んでとあるページに遷移します。

実装

呼び出し側の中身
Example.vue
<div>
    <BreadCrumb :breadcrumbs="breadcrumbs"/>
</div>

<script>

import BreadCrumb from "/components/BreadCrumb";

export default {
  components: {
    BreadCrumb
  },
  data () {
    return {
      breadcrumbs: [
       {
          name: 'TOP',
          path: '/'
        },
        {
          name: 'Category', // 中間ページ
          path: '/category',
        },
        {
          name: 'ExamplePage' // とあるページ
        }
      ]
    }
};
<script>
パンくずリストのコンポーネント中身
BreadCrumb.vue
<div class="breadcrumb-area">
    <div class="breadcrumb-item">
      <div v-for="v in breadcrumbs">
        <div v-if="v.path">
          <router-link :to="v.path">
            <span class="link">{{v.name}}</span>
          </router-link>
          <span class="space">></span>
        </div>
        <div v-else>
          {{v.name}}
        </div>
      </div>
    </div>
  </div>
解説

例として最後のとあるページで記載しています。
子コンポーネントに渡すデータにそのページまでのnameとpathをdataに配列型に持たせることで、パンくずが増えていくイメージです。
とあるページでは、nameは保持させますが、pathを保持させないことで、v-ifを使ってリンクかただの文字を表示させるかを切り分けています。

↓こんな感じ
スクリーンショット 2020-12-15 10.16.22.png

最後に

手動でページ設定をする方法でパンくずリストを作成してみました。
ちなみに、自動でページ毎に設定をさせる方法もあるみたいですので、大量のページに使う方などはそちらを参考にして頂いた方がいいかもしれません。

https://hirakublog.com/code/218/
参考にさせていただきました。ありがとうございます。

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

脆弱性のある依存パッケージをnpm auditで監査する

フューチャーAdventCalendar2020 15日目です。

昨日は、iwamoto-san(@qiiwa)さんによる、PingでLinuxサーバの死活監視をするべきではないという話でした。

はじめに

こんにちは。2019年4月入社の市川です。
現在私はVue.jsでのSPA(Single Page Application)の開発業務に携わらせていただくことが多いのですが、プロジェクトで使用しているパッケージの脆弱性対応を業務で行いました。

今回は私が調べながら脆弱性対応を行った内容を、こちらで紹介をしていきたいと思います。

環境

  • npm 6.14.8
  • node 14.9.0

現状把握

npm v.6.0.0より、そのプロジェクトで使用しているパッケージの依存関係を監査して脆弱性が存在するかどうかを確認できる、npm audit コマンドが用意されています。(https://docs.npmjs.com/cli/v6/commands/npm-audit)

まず今回はこちらを使用して自分のプロジェクトで使用しているパッケージの依存関係にどの程度の脆弱性があるのかを確認してみましょう。

実行すると、下記のような出力結果を得ることができます。

$ npm audit

                       === npm audit security report ===                        
┌───────────────┬──────────────────────────────────────────────────────────────┐
│ Low           │ Prototype Pollution                                          │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package       │ foo                                                          │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ bar                                                          │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path          │ hoge                                                         │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info     │ https://npmjs.com/advisories/fuga                            │
└───────────────┴──────────────────────────────────────────────────────────────┘
...
...
...(割愛)
...
found 100 vulnerabilities (95 low, 5 high) in 200 scanned packages ・・・①
  run `npm audit fix` to fix 98 of them. ・・・②
  2 vulnerabilities require manual review. See the full report for details. ・・・③

(↑脆弱性件数の数値を少しいじってます)

npm auditによって下記のようなことがわかりました。

① プロジェクトで使用している200個のpackageのうち100件の脆弱性があり、
  そのうち95件はlow level、5件はhigh levelであること
② 100件のうち98件の脆弱性に関してはnpm audit fix コマンドで脆弱性対応ができること
③ 2つの脆弱性については手動での確認が必要なこと

npm audit fix での脆弱性対応

現状も把握できたので、脆弱性対応を実施していきます。
上記の出力結果にある npm audit fix コマンドはnpmのv6.1.0から使用でき、node_modulesに存在する大抵の脆弱性を自動修正してくれます。

では、npm audit fix を実行しましょう。

$ npm audit fix

+ hoge@1.17.30
added 1 package from 1 contributor, removed 10 packages and updated 15 packages in 10.000s

63 packages are looking for funding
  run `npm fund` for details

fixed 98 of 100 vulnerabilities in 200 scanned packages
  2 vulnerabilities required manual review and could not be updated

npm audit fix により、100個の脆弱性のうち98個に対応することができました。便利ですね。
ログに出力されている通り、パッケージの依存関係によって npm audit fix で自動で修正しきれない脆弱性が残ってしまうことがあり、こちらは手動で解消しなくてはいけません。

今回、手動で対応しなければならない脆弱性が2つ残っているのでその内容を確認します。

$ npm audit
                       === npm audit security report ===                        

# Run  npm update webpack --depth 2  to resolve 2 vulnerabilities
┌───────────────┬──────────────────────────────────────────────────────────────┐
│ High          │ Remote Code Execution                                        │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package       │ serialize-javascript                                         │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ @vue/cli-plugin-babel [dev]                                  │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path          │ @vue/cli-plugin-babel > webpack > terser-webpack-plugin >    │
│               │ serialize-javascript                                         │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info     │ https://npmjs.com/advisories/1548                            │
└───────────────┴──────────────────────────────────────────────────────────────┘

┌───────────────┬──────────────────────────────────────────────────────────────┐
│ High          │ Remote Code Execution                                        │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package       │ serialize-javascript                                         │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ @vue/cli-plugin-eslint [dev]                                 │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path          │ @vue/cli-plugin-eslint > webpack > terser-webpack-plugin >   │
│               │ serialize-javascript                                         │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info     │ https://npmjs.com/advisories/1548                            │
└───────────────┴──────────────────────────────────────────────────────────────┘

npm auditを実行すると、上記のような出力結果を得ました。
ログに出力されている、More info のリンクにアクセスすると詳細な内容が記載されています。

image.png
(https://www.npmjs.com/advisories/1548)

この2つの脆弱性に対応するためには、npm update webpack --depth 2 を実行してwebpackをupdateするよう記載があるので、それに従って npm update webpack --depth 2 を実行し、npm auditで出力結果を確認します。

$ npm update webpack --depth 2
...
...
$ npm audit                                                                     
                       === npm audit security report ===                        

found 0 vulnerabilities in 200 scanned packages

こちらで依存しているパッケージの脆弱性が0件となることが確認できました。

脆弱性対応を自動化する

ソフトウェアの世界では、日々脆弱性が発見されては修正されてを繰り返しています。
人間が常に脆弱性の情報をキャッチアップし、アップデート作業を行うのは少し億劫ですよね

プライベートで使用しているGithubでは脆弱性が発見された時、自動で脆弱性対応を行うPull Requestを作成する設定を行っているのですが、業務の方ではあまり自動化できていないので、 gitlab-ciをなどを活用した脆弱性対応が行えないか後日検証してみたいと思います。

参考

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

大急ぎでAPI+SPA構成のアプリを立ち上げる(Nuxt.js&簡易認証編)

前回の記事

大急ぎでAPI+SPA構成のアプリを立ち上げる(Spring Boot&Heroku編)

あらすじ

こんなの(下図)を明日までに準備することになり、バックエンドとDBの構築までを終わらせました。
今回はフロントエンドを何とかします。

Picture2.png

Nuxt.jsについて

Nuxt.jsはフロントエンドアプリを構築するためのフレームワークです。
Vue.jsの知識は必要になりますが、最初からガッツリ出来上がったものが手に入るので、急いでいる時にはもってこいです。

環境

OS

  • macOS Catalina

ツール・ソフトウェア

  • Node.js v10.15.3
  • npm v6.14.8
  • WebStorm 2020.2.4

(※バックエンド・Heroku関連は前回記事に記載)

雛形を作る

Get Startedに従い、雛形を作ります。
このコマンドを実行するとカレントディレクトリにソースが生成されるため、あらかじめ開発用のディレクトリに移動しておきます。

npx create-nuxt-app my-rapid-app-front

生成中にいろいろ聞かれますが、落ち着いて答えていきましょう。

■Project name, Project description, Author name

何でもOKです。適当に入れます。

■package manager

yarn か npm の2択です。
長い目で見ればyarnがオススメですが、未インストールならnpmでもOKかと。

■UI framework, custom server framework, Nuxt.js modules, linting tools, test framework

たくさんあって迷っちゃいますね。全部Noneです。
Lintツールくらいは入れておいても損はないのですが、今回の記事では取り上げません。

■rendering mode

サーバサイドレンダリング(SSR)かSPAの2択です。今回はSPA。

Screen Shot 2020-12-11 at 20.45.45.png

できました。
早速プロジェクトを開いてみましょう。

Screen Shot 2020-12-11 at 20.50.29.png

何から何までご用意されています。最高ですね。

動作確認

「nuxt dev」コマンドを実行して、ローカルで開発用サーバを起動します。
このときホットリロードが有効になるため、コードを修正すると即時反映されます。

npm run dev

起動したら、ブラウザでhttp://localhost:3000を開きます。

Screen Shot 2020-12-12 at 16.38.36.png

適当に入れたメッセージが表示されてて恥ずかしいので、さっさと直していきましょう。

APIサーバと接続する(バックエンド側)

早速APIを叩いていきたいところですが、先にバックエンド側にCORSの設定を入れます。
どこから飛んできたリクエストを許容するか、あらかじめ決めておく必要があるわけです。

▼設定しないとこんなエラーが出ます
Screen Shot 2020-12-11 at 22.47.10.png

DemoApplication.javaに設定を追記したら、Herokuにpush。

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    // 以下を追記
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                // localhostとフロントエンドのドメイン(予定)に対し、"/api/"で始まるURIへのリクエストを許可
                // 末尾に/を入れると機能しないので注意
                registry.addMapping("/api/**")
                        .allowedOrigins(
                                "http://localhost:3000",
                                "https://my-rapid-demo-front.herokuapp.com"
                        );
            }
        };
    }
}
git add .
git commit -am "cors setting"
git push heroku master

これで準備完了です。

APIサーバと接続する(フロントエンド側)

ローカルからの接続先はhttp://localhost:8080、Herokuからはhttps://***.herokuapp.comにしたいので、環境変数で制御できるように設定を入れます。
nuxt.config.jsを開き、末尾に以下を追加してください。

env: {
    baseUrl: process.env.BASE_URL || 'http://localhost:8080'
}

これにより、変数process.env.baseUrlに対し、環境変数BASE_URLが設定されていればその値が、されていなければhttp://localhost:8080が格納されるようになります。

続いてpages/index.vueを開き、<template><script>をごそっと編集。
<style>はとりあえずそのまま。

<template>
  <div class="container">
    <ul>
      <li v-for="employee in employees">
        <span>{{ employee.name }} [{{ employee.department.name }}]</span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      employees: []
    }
  },
  async created() {
    const response = await fetch(`${process.env.baseUrl}/api/employees`)
    if (response.ok) {
      this.employees = await response.json()
    } else {
      console.error(response.statusText)
    }
  }
}
</script>

これがトップページになります。中身をざっくり解説すると、

  • ページのインスタンスができたタイミング(created)で、Employee一覧取得APIを実行
  • 変数(date)のemployeesにレスポンスボディを格納
  • v-forを使い、employeesの要素数だけ<li>タグを生成

という流れです。

画面を再度開き、以下のようになっていれば接続はOKです。

Screen Shot 2020-12-12 at 14.52.05.png

殺風景ですが、とりあえずヨシ。

デプロイする

静的ビルドしてNetlify等に置くだけでもOKなのですが、今回はフロントエンドもHeroku上で動かします。
公式のガイドを参考に進めていきましょう。

まず、Heroku用の設定ファイルを作ります。
ファイル名は「Procfile」。ここに起動時のコマンドを設定します。

echo 'web: nuxt start' > Procfile

プロジェクト直下にProcfileができたことを確認したら、Herokuアプリケーションを作成。

heroku create my-rapid-demo-front

Herokuに設定を入れます。上2つがサーバの動作に必要な設定、
下が「process.env.baseUrl」に与えるための環境変数です。

heroku config:set HOST=0.0.0.0 -a 【アプリ名】
heroku config:set NODE_ENV=production -a 【アプリ名】
heroku config:set BASE_URL=https://【バックエンドのアプリ名】.herokuapp.com -a 【アプリ名】

ここで注意なのですが、npm/yarn使用時に生成されるpackage-lock.jsonyarn.lockをコミットすると、ビルドに失敗してしまいます。
.gitignoreに追記して、コミット対象外になるようにしておきます。

echo "package-lock.json" >> .gitignore
echo "yarn.lock" >> .gitignore

コードをpush。

git add .
git commit -am "initial commit"
git push heroku master

pushが完了したら、https://【アプリ名】.herokuapp.com/を開いてみましょう。
起動まで少し時間がかかるかもしれません。

Screen Shot 2020-12-12 at 14.56.14.png

無事に成功しました。
しばらく待っても何も出てこない場合、git push時の出力、Herokuのログ、デベロッパーツールのコンソールを確認し、エラーが起きていないか確認してください。

Done!

これで最低限の構築は完了です。
あとは必要に応じてAPIを増やしたり、テンプレートやUIフレームワークを見繕ってフロントエンドの見栄えを調整したりしていきましょう。
下のスクリーンショットはVuetifyで体裁を整えた画面です。

Screen Shot 2020-12-12 at 16.34.06.png

簡易認証を入れる(おまけ)

さて、開発用とはいえAPIと画面が公開されているのは気になる…
ということで、最低限の認証機構を入れてみます。

  1. フロントエンドとバックエンドで共通のAPIキーを持たせてチェック
  2. 画面を開くときにユーザ名・パスワードの入力を求める

なお、これを導入したとてAPIキーが流出すればアクセスし放題なので、言うまでもなく気休めです。くれぐれも本番運用にはお使いになりませんよう…

1. APIキーのチェック

まずはバックエンド側から。
application.propertiesAPI_KEY=dummyという行を追加します。
この値は環境変数で上書きされるため、環境変数があればその値、なければ「dummy」が適用されます。

続いて、パッケージinterceptorを新しく切り、中に以下のクラスを作成します。

package com.example.demo.interceptor;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class ApiInterceptor implements HandlerInterceptor {

    @Value("${API_KEY}")
    private String API_KEY;

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) throws IOException {

        // preflightの場合はスキップ
        if (request.getMethod().equals(HttpMethod.OPTIONS.name())) return true;

        String authorizationHeader = request.getHeader("Authorization");

        if (authorizationHeader == null || !authorizationHeader.equals("Bearer " + API_KEY)) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
            return false;
        }

        return true;
    }
}

このpreHandleというメソッドは、リクエストがControllerに渡される前に実行されます。
Headerに設定された文字列を読み取り、キーが一致しなかったら401エラーを返して後続処理が行われないようにしています。

続いて、DemoApplication.javaを開き、先ほど作ったcorsConfigurerをまるっと書き換えます。

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                        .allowedOrigins(
                                "http://localhost:3000",
                                "https://my-rapid-demo-front.herokuapp.com"
                        );
            }

            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(apiInterceptor)
                        .addPathPatterns("/api/**");
            }
        };
    }

addInterceptorsメソッドを実装することで、先ほどのInterceptorが/api/以下のリクエストに適用されるようになります。

続いてフロントエンド側。
先ほどのbaseUrlと同様に、nuxt.config.jsに変数を追加します。

env: {
    baseUrl: process.env.BASE_URL || 'http://localhost:8080',
    apiKey: process.env.API_KEY || 'dummy'
}

そして、リクエスト送信時にヘッダを追加します。

    const response = await fetch(`${process.env.baseUrl}/api/employees`, {
      headers: {
        Authorization: `Bearer ${process.env.apiKey}`
      }
    })

これで実装は完了です。
試しに"dummy"を別の文字列に変えてみると、401エラーが返ってくるかと思います。

Heroku上にはもう少し長めの文字列を付与してあげます。
なお、本格的に認証を行うのであればただの文字列ではなく、JWTトークンなどを検討するのが良いかと思います。

heroku config:set API_KEY=【キー文字列】 -a 【フロントエンドアプリ名】
heroku config:set API_KEY=【キー文字列】 -a 【バックエンドアプリ名】

2. 画面に認証ダイアログ

こちらはNuxt.js用の神モジュールを使わせて頂きます。

nuxt-basic-auth-module

npm install nuxt-basic-auth-module
  modules: [
    'nuxt-basic-auth-module'
  ],
  basic: {
    name: process.env.BASIC_AUTH_USER || 'user',
    pass: process.env.BASIC_AUTH_PASSWORD || 'password',
    enabled: true
  },

Screen Shot 2020-12-12 at 21.03.38.png

デプロイ後に画面を開き直すと、ダイアログが出てきました。
これですべて完了です。

おわりに

ここまで読んでいただきありがとうございました。
リポジトリのリンクを置いておきますので、お急ぎの際はご自由にお使いください。

GitHub

my-rapid-demo
my-rapid-demo-front

参考ページ

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

Vue.js で Atomic Design のサイトテンプレートを作成

Atomic Designの勉強のために
Vue.js で簡単なサイトテンプレートを作成してみました。

作ったもの

Videotogif (1).gif

vue create ~ npm run serve

npm, vueバージョン

$ npm -v
6.14.8

$ vue --version
@vue/cli 4.5.9

プロジェクト作成

vue create vue_atomic

プリセットはマニュアルを選択しました

? Please pick a preset:
  Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
> 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

Vue3 を選択

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

以下の2つはデフォルトにしました

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

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

ヒストリーモードを使用するようにしました。

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

以下の項目もデフォルトにしました。

? 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

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

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

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

プリセットを保存はしませんでした。

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

動作確認

cd vue_atomic
npm run serve

vue_atomic.png
TypeScript の表示も出ています。

CSS Frameworkのインストール

2020/12/12現在ではBootstrap-vueが未対応の様でした。

フリーランス 技術調査ブログ
https://px-wing.hatenablog.com/entry/2020/11/25/082139

を参考にさせていただいたところ
TailwindCSSが対応しているとのことだったので、
初めてですがこの機会に使用してみることにしました。

TailWindCSS公式を参考に、

npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9

ルートディレクトリに postcss.config.js を作成し、以下を記述します。

postcss.config.js
/* eslint-disable @typescript-eslint/no-var-requires */
const autoprefixer = require('autoprefixer');
const tailwindcss = require('tailwindcss');

module.exports = {
  plugins: [
    tailwindcss,
    autoprefixer,
  ],
};

index.cssを作成し以下を記述します。
(srcフォルダ直下にcssフォルダを作成して、配置しました)

index.css
/* ./src/index.css */

/*! @import */
@tailwind base;
@tailwind components;
@tailwind utilities;

main.ts に以下の一文を記述します。

main.ts
import './css/index.css'

これでTailWindCSSで記述できる準備ができました。

フォルダ構成

今回は以下のようにいたしました

src
 - assets
 - components
    - atoms
    - molecules
    - organisms
    - templates
 - css
 - pages
 - router
 - store
 App.vue
 main.ts

初期状態だとviewsフォルダがあったのですが削除しました。
templatesは作らず、他の4つで構成しているという例をよく見たのですが、
今回は半ば無理やりtemplatesも作ってみました。

main.ts

最終的なmain.ts.

main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './css/index.css'

const app = createApp(App)

/**
 * componentsフォルダの中のVueファイルを読み込み、
 * グローバルコンポーネントとして登録。
 */
const files = require.context('$comp', true, /\.vue$/)
const components: { [key: string]: __WebpackModuleApi.RequireContext} = {}
files.keys().forEach(key => {
  const str: string = key.replace(/(\.\/|\.vue)/g, '')
  components[str] = files(key).default
})

// 読み込んだvueファイルをグローバルコンポーネントとして登録
Object.keys(components).forEach(key => {
  const nameArr = key.split('/')
  const name = nameArr[nameArr.length - 1]
  app.component(name, components[key])
});

app.use(store).use(router).mount('#app')

最初、こう書いていたら型エラーが出ましたので、

const components: { [key: string]: string} = {}

こう修正したところ、エラーが無くなりました。

const components: { [key: string]: __WebpackModuleApi.RequireContext} = {}

各ファイルの作成

最初に作成されたHome.vueを参考にしながら各ファイルを作成していきました。

Home.vue
<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
  </div>
</template>

<script lang="ts">
import { Options, Vue } from 'vue-class-component';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src

@Options({
  components: {
    HelloWorld,
  },
})
export default class Home extends Vue {}
</script>

App.vue

App.vue
<template>
  <Layout-Default>
    <template v-slot:header>
      <The-Header></The-Header>
    </template>

    <template v-slot:main>
      <transition name="fade">
        <router-view />
      </transition>
    </template>

    <template v-slot:footer>
      <The-Footer></The-Footer>
    </template>
  </Layout-Default>
</template>

Atoms

CustomLink.vue
<template>
  <router-link
    :to="{ name: link }"
    class="px-3 py-2 rounded-md text-sm font-medium"
  >
    {{ label }}
  </router-link>
</template>
CustomTitle.vue
<template>
  <div class="mx-auto text-9xl">
    <p>
      {{ label }}
    </p>
  </div>
</template>

Molecules

CustomCard.vue
<template>
  <!-- This example requires Tailwind CSS v2.0+ -->
  <div
    class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-lg m-4 sm:align-middle sm:max-w-lg sm:w-full"
  >
    <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
      <div class="mt-3 text-center">
        <h3
          class="text-lg leading-6 font-medium text-gray-900"
          id="modal-headline"
        >
          {{ title }}
        </h3>
        <div class="mt-2">
          <p class="text-sm text-gray-500">
            {{ text }}
          </p>
        </div>
      </div>
    </div>
  </div>
</template>

TailWindCSS公式にある、Modalの中身を少し変更したものになります。

Organisms

CustomTitleContainer.vue
<template>
  <div key="title" class="container mx-auto self-center">
    <slot name="contents"></slot>
  </div>
</template>
CustomContentsContainer.vue
<template>
  <!-- This example requires Tailwind CSS v2.0+ -->
    <div key="content" class="md:space-y-0 md:grid md:grid-cols-2 md:gap-x-8 md:gap-y-4 mx-auto">
      <div class="flex">
        <slot name="contents1"></slot>
      </div>

      <div class="flex">
        <slot name="contents2"></slot>
      </div>

      <div class="flex">
        <slot name="contents3"></slot>
      </div>

      <div class="flex">
        <slot name="contents4"></slot>
      </div>
    </div>
</template>

テンプレートのメインコンテンツを入れ替えるためのコンテナになります。
コンテナはOrganismsを内包することもあるので、自身もOrganismsになるかと思うのですが
そもそもコンテナのようなものを要素として作るべきなのか、作ったとして別の要素になるのか、
いまいち理解できていません…

TheHeader.vue
<template>
  <nav class="bg-blue-800 shadow">
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
      <div class="flex items-center justify-between h-16">
        <div class="flex items-center">
          <div class="flex-shrink-0">
            <Custom-Link class="text-gray-200 hover:text-white text-lg" label="HOME" link="Home"></Custom-Link>
          </div>
          <div class="">
            <div class="ml-10 flex items-baseline space-x-4">
              <Custom-Link class="text-gray-200 hover:text-white" label="PageA" link="PageA"></Custom-Link>

              <Custom-Link class="text-gray-200 hover:text-white" label="PageB" link="PageB"></Custom-Link>
            </div>
          </div>
        </div>
      </div>
    </div>
  </nav>
</template>
TheFooter.vue
<template>
  <nav>
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 ">
      <div class="flex items-center justify-between h-16">
        <div class="flex items-center">
          <div class="flex-shrink-0">
            <Custom-Link
              class="text-sm text-gray-500 hover:text-gray-900"
              label="HOME"
              link="Home"
            ></Custom-Link>
          </div>
          <div>
            <p class="text-xs">&copy; xxxxxxxx {{ year }}</p>
          </div>
        </div>
      </div>
    </div>
  </nav>
</template>

ヘッダーとフッター。
Vueの命名規則に則ってこちらの2つのみThe~ という名前にしています。

Templates

LayoutDefault.vue
<template>
  <div>
    <header>
      <slot name="header"></slot>
    </header>
    <div>
      <main class="container main-container mx-auto flex">
        <slot name="main"></slot>
      </main>
    </div>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

やっぱり無理やり作った感じが否めません。
VueでAtomicDesignにする時にはTemplatesは作らなくても良さそうだと感じました。

Pages

Home.vue
<template>
  <Custom-Title-Container>
    <template v-slot:contents>
      <Custom-Title :label="label"></Custom-Title>
    </template>
  </Custom-Title-Container>
</template>
PageA.vue
<template>
<template>
  <Custom-Contents-Container>
    <template v-slot:contents1>
      <Custom-Card :title="title1" :text="text1"></Custom-Card>
    </template>

    <template v-slot:contents2>
      <Custom-Card :title="title2" :text="text2"></Custom-Card>
    </template>

    <template v-slot:contents3>
      <Custom-Card :title="title3" :text="text3"></Custom-Card>
    </template>

    <template v-slot:contents4>
      <Custom-Card :title="title4" :text="text4"></Custom-Card>
    </template>
  </Custom-Contents-Container>
</template></template>

各PagesはOrganismsに作ったコンテンツ表示用のコンテナを中身を入れ替えて表示するようにしました。

まとめ

AtomicDesignを採用したからといって問題が何でも解決するわけでもなく、
データの受け渡し方法など考慮しなければならないポイントが多くあることに気づけました。
Vue3やTypeScriptを使ってみたくて選んだのですが、特徴や良さを活かすことがまったくできなかったので反省です。

TailWindCSSはClassの記述量が多くなるのが難点だと感じましたが、
AtomicDesignでCSSをメインに適用するコンポーネントとそうでないものを分離することで、
少しデメリットを吸収することができるのでは、と思いました。

今回の制作物は以下のGitのリポジトリに置いております。

https://github.com/akymyuuki/vue_atomic

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