20191222のvue.jsに関する記事は19件です。

Vuexの状態定義を抽出してやる!

なぜ?

SPAで開発してると状態管理が大変になり、参照したい状態がどこにあるか探すのが大変。
そこで、状態のみを抽出して構造化したら全体像を把握できて探すのが楽になるのでは?
と思いやってみます。

どうやって?

AST(Abstract Structure Tree)を使って、静的構文解析をして状態を抽出します。
とりあえず、モジュールは考えないでやってみる。

環境

  • Node.js(v10.15.3)
  • babylon(6.18.0)
  • @babel/traverse(7.4.3)

想定するファイルと構成

store/index.js
import Vue from 'vue';
import Vuex form 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    account: {}
  },
  getters: {},
  mutations: {},
  actions: {}
});

期待する出力

state.json
{
  'root': {
    'account': {}
  }
}

状態を抽出するコード

extract-state.js
const fs = require('fs')
const babylon = require('babylon')
const traverse = require('@babel/traverse').default

const IsParentState = Symbol('IsParentState')
const ast = babylon.parse(fs.readFileSync('./store/index.js', 'utf8'), {sourceType: 'module'})
const state = {
  'root': {}
}

const visitor = {
  ObjectExpression: {
    enter(nodePath) {
      // 親ノードがオブジェクトのプロパティかつ、キーがstate
      if (nodePath.parent.type === 'ObjectProperty' && nodePath.parent.key.name === 'state') {
        nodePath[IsParentState] = true
      }
    },

    exit(nodePath) {
      // state(Vuex)キーのvalueを評価して、出力用のstate.rootのvalueとする
      if (nodePath[IsParentState]) {
        const { value } = nodePath.evaluate()
        Object.assign(state.root, value)
      }
    }
  }
}

traverse(ast, visitor)
console.log(state)

実行

node extract-state.js

結果

{ root: { account: {} } }
store/index.js
import Vue from 'vue';
import Vuex form 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    account: {},
    isSignIn: false
  },
  getters: {},
  mutations: {},
  actions: {}
});

で実行すると、

{ root: { account: {}, isSignIn: false } }

となる。

今後の課題

  • stateがショートハンドで初期化されてる場合、抽出できない
  • stateキーがあるオブジェクトすべてを対象としてしまってるので正しく抽出できない場合がある
  • モジュールの状態が抽出できてない

モジュールの状態が抽出できないと目的達成できないですが、時間切れなので
また別の機会に解決したいと思います。

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

Vue.jsで帳票印刷したい

Vue.jsを利用したWebシステムで帳票印刷する必要がありました。
方法がいくつかあり、検討した知見を紹介します。

背景

  • フロントはVue.jsを利用したSPA
  • 指定条件を入力しビジネスの統計情報を画面表示する
    • 集計、グラフを伴う
  • 統計画面上の「印刷」ボタンを押下すると帳票印刷 or PDFダウンロードする
    • ビジネスレポートは複数ページに渡る
    • 数値の集計表、グラフなどを伴う
  • ヘッダ/フッタなどの体裁はこだわらない

また、画面ページ上の統計情報と印刷ページ上の統計情報は同じ情報であり、
開発コスト低減のために可能であれば、集計表、グラフなどの画面表示用コンポーネントを流用して帳票をレイアウトしたい状況です。

方式検討

実現方式は以下のように複数考えられますが、
最終的には「ブラウザの印刷機能でPDF保存」させる方法を採用しました。

  • サーバサイドでPDFファイルを生成
    • テンプレートPDFを元に差し込み
    • PDF生成エンジンを利用
      • 独自レイアウト方式型
      • HTML+CSSでレイアウト型
    • headlessブラウザのレンダリング/印刷機能を利用
  • クライアントサイドでPDFファイルを生成
    • PDFファイルを生成するJSライブラリ利用
      • 独自レイアウト型
      • HTML+CSSでレイアウト型
      • Domを元にCanvas化し、Canvas画像をPDFに貼り付ける
  • クライアントサイドでブラウザの印刷機能でPDF保存 ←採用
    • レイアウトはHTML+CSS
    • Mediaクエリで印刷用CSS定義

以下、検討の際に考慮した観点について補足します。

定形雛形に差し込み vs ページ記述

例えば、「領収書印刷」などの用途のような、以下の条件のようなものは

  • 1枚ペラもの
  • 定型フォーマット
  • 所定の位置に 宛名、金額 が記載できればOK

定形のテンプレートPDFを作成しておき、宛名、金額など一部のテキストに必要な値を差し込む方式が適していると言えます。

本要件は比較的ボリュームの多いビジネスレポートであり、以下のような特性があるため、テンプレート形式を取りにくいと考えました。

  • ページ数は不定
    • 長文は改ページして複数ページをまたぐ
  • グラフ、表が含まれる

レイアウト方式

独自方式

このとき、どのようにレイアウトをPDF化するか、という問題がありますが、
PDF生成エンジン・ライブラリによっては、独自のレイアウト指定が必要なものがあります。

以下はPDF生成ライブラリpdfkit の例です。
テキストだけなら良いですが、表やグラフをこれで描画するのは骨が折れそうです。

pdfkitの例
const PDFDocument = require('pdfkit');

// Create a document
const doc = new PDFDocument;

// Pipe its output somewhere, like to a file or HTTP response
// See below for browser usage
doc.pipe(fs.createWriteStream('output.pdf'));

// Embed a font, set the font size, and render some text
doc.font('fonts/PalatinoBold.ttf')
   .fontSize(25)
   .text('Some text with an embedded font!', 100, 100);

// Add an image, constrain it to a given size, and center it vertically and horizontally
doc.image('path/to/image.png', {
   fit: [250, 300],
   align: 'center',
   valign: 'center'
});

HTMLでレイアウト

独自レイアウト方法が大変なので、
慣れたHTML+CSSでレイアウト指定が可能なものがありますが、
カスタムCSSが利用しづらいなどの注意点ががあるようです。

bpampuch/pdfmake

ブラウザの印刷機能でPDFレンダリング

ChromeやFirefox, Safariなど人気のあるモダンブラウザでは
印刷機能でPDFとして保存する機能を備えており、
そのレンダリング性能も信頼できると言って良いでしょう。

Mediaクエリを利用して印刷用CSSを定義し、
印刷する際のコンテンツサイズ指定、不要な画面サイドのナビやボタン類を非表示にするなどの制御が可能です。

フォント

PDF生成エンジン/ライブラリによってはデフォルトで日本語フォントを扱えないものが多いようです。
pdfkit, pdfmake(内部でpdfkitを利用)などはフォント埋め込みのための事前処理や設定が必要なようです。

特に、クライアントサイドでPDFを生成する場合は埋め込み用フォントを事前生成するという方法以外に、
Domとして日本語表示したイメージをあえて画像化してPDFに埋め込む、という方法もあります。

日本語コンテンツのボリュームによっては、多くの画像が埋め込まれるため、(フォント埋め込みの場合よりも)PDFのサイズは大きくなるかもしれません。

改ページ

複数ページにまたがるPDFの場合は改ページ位置を制御したいかもしれません。
PDF生成エンジン、ライブラリを利用する場合は、そのエンジンの改ページ制御方法に倣う必要があります。

ブラウザの印刷機能でPDFを生成する場合もMediaクエリで改ページの制御が可能です。

一方、フォントの問題のためにコンテンツを画像化してPDFに貼り付けている場合は改ページ制御が煩わしいかもしれません。

その他のデザイン・体裁のコントロール

特に、ブラウザの印刷機能を利用してPDFファイルを作成する場合、
クライアントユーザの設定によって生成されるPDFが異なるため、
デザイン・体裁的にコントロール、画一化することは難しいのが現状です。

  • 用紙サイズ、向き、マージン
  • 背景画像の有無
  • ヘッダ・フッタの有無
    • URL, ページタイトル, 作成日

主要なモダンブラウザのデフォルトで利用した際に
文章の中身が読めれば良く、ヘッダ・フッタなどにはこだわらなくて良い、
という今回の要件では許容できると判断しました。

処理の負荷 (サーバサイド/クライアントサイド)

不特定多数のユーザが利用するシステムであり、
サーバ負荷を抑えたいため、可能であればクライアント側でPDFを生成したいと考えました。

デバッグのしやすさ

ブラウザの印刷機能を利用する場合は、デバッグの際に、印刷用CSSと画面表示用CSSを合わせて置けばレイアウト確認は楽々ですね。
ブラウザのデベロッパーツールも活用できるので、ルーラを表示したり、画面を見ながら微調整を試すことも容易です。

PDF生成エンジン/ライブラリの場合はデバッグは大変かも。

Vue.js での実装例

<template>
  <div class="sheets">
    <div>
      <el-button type="primary" @click="handlePrint">印刷</el-button>
      ※PDFで保存したい場合は印刷ダイアログで「PDF保存」を指定してください
    </div>
    <div class="sheet">
      <h2>帳票サンプル</h2>
      <h3>テーブルを印刷する</h3>
      <el-table :data="list" border fit>
        <el-table-column label="ID" prop="id" align="center" width="80px">
          <template slot-scope="scope">
            <span>{{ scope.row.id }}</span>
          </template>
        </el-table-column>
        <el-table-column label="Title" min-width="150px">
          <template slot-scope="{row}">
            <span>{{ row.title }}</span>
          </template>
        </el-table-column>
        <el-table-column label="Author" width="110px" align="center">
          <template slot-scope="scope">
            <span>{{ scope.row.author }}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <div class="sheet">
      <h3>改ページのテスト</h3>
      2ページ目
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: null
    }
  },
  created() {
    this.getList()
  },
  mounted() {
    this.fetchData()
  },
  methods: {
    getList() {
      // APIからデータ取得する想定
      this.list = [
        { id: 1, author: 'John Due', title: 'Hello, world' },
        { id: 2, author: '太郎', title: 'あいうえお かきくけこ' }
      ]
    },
    fetchData() {
      document.title = 'タイトルをいい感じに設定する'
      setTimeout(() => {
        this.$nextTick(() => {
          this.handlePrint()
        })
      })
    },
    handlePrint() {
      window.print()
    }
  }
}
</script>

<style lang="scss" scoped>
.sheet {
  page-break-after: always;
}

/* hide in print */
@media print {
  .sheets > :not(.sheet) {
    display: none;
  }
}

/* for preview */
@media screen {
  /* mm単位で指定しているけど、vueコンポ側はpx単位なので、無理にmmにしなくてもいいかも。解像度の違いでハマるかも */
  .sheet {
    width: 200mm;
    min-height: 296mm; /* 設定しなくてもいいかも。あまり印刷画面に似せすぎると、些細な違いがバグに見えてしまう */
    margin: 5mm;
    padding: 5mm;
    background: white;
    box-shadow: 0 .5mm 2mm rgba(0,0,0,.3);
  }
}
</style>
<style lang="scss">
/* for preview */
@media screen {
  BODY {
    background: #eee;
  }
}
</style>

Vueで実装した帳票プレビュー画面

Webシステムで帳票印刷機能を実行すると、プレビュー画面を表示するようにしています。
画面上部にある「印刷」ボタンは、メディアクエリにて画面表示の場合のみボタン表示して、印刷時には非表示にしています。

あえて、印刷プレビューっぽく見えるようにグレー背景、縦ページ、ドロップシャドウなどを設定していますが、ブラウザの印刷ダイアログでもプレビュー表示されるので、無くて良いかも。

Vueで作成したプレビュー画面.jpg

ブラウザの印刷ダイアログ

Chromeの印刷ダイアログの例。
デフォルトではWebページタイトルがPDFファイル名になるので、印刷直前にWebページタイトルを切り替えるように実装しておきます。

ブラウザの印刷ダイアログ.jpg

参考

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

Javascriptで帳票印刷したい

Vue.jsを利用したWebシステムで帳票印刷する必要がありました。
方法がいくつかあり、検討した知見を紹介します。

背景

  • フロントはVue.jsを利用したSPA
  • 指定条件を入力しビジネスの統計情報を画面表示する
    • 集計、グラフを伴う
  • 統計画面上の「印刷」ボタンを押下すると帳票印刷 or PDFダウンロードする
    • ビジネスレポートは複数ページに渡る
    • 数値の集計表、グラフなどを伴う
  • ヘッダ/フッタなどの体裁はこだわらない

また、画面ページ上の統計情報と印刷ページ上の統計情報は同じ情報であり、
開発コスト低減のために可能であれば、集計表、グラフなどの画面表示用コンポーネントを流用して帳票をレイアウトしたい状況です。

方式検討

実現方式は以下のように複数考えられますが、
最終的には「ブラウザの印刷機能でPDF保存」させる方法を採用しました。

  • サーバサイドでPDFファイルを生成
    • テンプレートPDFを元に差し込み
    • PDF生成エンジンを利用
      • 独自レイアウト方式型
      • HTML+CSSでレイアウト型
    • headlessブラウザのレンダリング/印刷機能を利用
  • クライアントサイドでPDFファイルを生成
    • PDFファイルを生成するJSライブラリ利用
      • 独自レイアウト型
      • HTML+CSSでレイアウト型
      • Domを元にCanvas化し、Canvas画像をPDFに貼り付ける
  • クライアントサイドでブラウザの印刷機能でPDF保存 ←採用
    • レイアウトはHTML+CSS
    • Mediaクエリで印刷用CSS定義

以下、検討の際に考慮した観点について補足します。

定形雛形に差し込み vs ページ記述

例えば、「領収書印刷」などの用途のような、以下の条件のようなものは

  • 1枚ペラもの
  • 定型フォーマット
  • 所定の位置に 宛名、金額 が記載できればOK

定形のテンプレートPDFを作成しておき、宛名、金額など一部のテキストに必要な値を差し込む方式が適していると言えます。

本要件は比較的ボリュームの多いビジネスレポートであり、以下のような特性があるため、テンプレート形式を取りにくいと考えました。

  • ページ数は不定
    • 長文は改ページして複数ページをまたぐ
  • グラフ、表が含まれる

レイアウト方式

独自方式

このとき、どのようにレイアウトをPDF化するか、という問題がありますが、
PDF生成エンジン・ライブラリによっては、独自のレイアウト指定が必要なものがあります。

以下はPDF生成ライブラリpdfkit の例です。
テキストだけなら良いですが、表やグラフをこれで描画するのは骨が折れそうです。

pdfkitの例
const PDFDocument = require('pdfkit');

// Create a document
const doc = new PDFDocument;

// Pipe its output somewhere, like to a file or HTTP response
// See below for browser usage
doc.pipe(fs.createWriteStream('output.pdf'));

// Embed a font, set the font size, and render some text
doc.font('fonts/PalatinoBold.ttf')
   .fontSize(25)
   .text('Some text with an embedded font!', 100, 100);

// Add an image, constrain it to a given size, and center it vertically and horizontally
doc.image('path/to/image.png', {
   fit: [250, 300],
   align: 'center',
   valign: 'center'
});

HTMLでレイアウト

独自レイアウト方法が大変なので、
慣れたHTML+CSSでレイアウト指定が可能なものがありますが、
カスタムCSSが利用しづらいなどの注意点ががあるようです。

bpampuch/pdfmake

ブラウザの印刷機能でPDFレンダリング

ChromeやFirefox, Safariなど人気のあるモダンブラウザでは
印刷機能でPDFとして保存する機能を備えており、
そのレンダリング性能も信頼できると言って良いでしょう。

Mediaクエリを利用して印刷用CSSを定義し、
印刷する際のコンテンツサイズ指定、不要な画面サイドのナビやボタン類を非表示にするなどの制御が可能です。

フォント

PDF生成エンジン/ライブラリによってはデフォルトで日本語フォントを扱えないものが多いようです。
pdfkit, pdfmake(内部でpdfkitを利用)などはフォント埋め込みのための事前処理や設定が必要なようです。

特に、クライアントサイドでPDFを生成する場合は埋め込み用フォントを事前生成するという方法以外に、
Domとして日本語表示したイメージをあえて画像化してPDFに埋め込む、という方法もあります。

日本語コンテンツのボリュームによっては、多くの画像が埋め込まれるため、(フォント埋め込みの場合よりも)PDFのサイズは大きくなるかもしれません。

改ページ

複数ページにまたがるPDFの場合は改ページ位置を制御したいかもしれません。
PDF生成エンジン、ライブラリを利用する場合は、そのエンジンの改ページ制御方法に倣う必要があります。

ブラウザの印刷機能でPDFを生成する場合もMediaクエリで改ページの制御が可能です。

一方、フォントの問題のためにコンテンツを画像化してPDFに貼り付けている場合は改ページ制御が煩わしいかもしれません。

その他のデザイン・体裁のコントロール

特に、ブラウザの印刷機能を利用してPDFファイルを作成する場合、
クライアントユーザの設定によって生成されるPDFが異なるため、
デザイン・体裁的にコントロール、画一化することは難しいのが現状です。

  • 用紙サイズ、向き、マージン
  • 背景画像の有無
  • ヘッダ・フッタの有無
    • URL, ページタイトル, 作成日

主要なモダンブラウザのデフォルトで利用した際に
文章の中身が読めれば良く、ヘッダ・フッタなどにはこだわらなくて良い、
という今回の要件では許容できると判断しました。

処理の負荷 (サーバサイド/クライアントサイド)

不特定多数のユーザが利用するシステムであり、
サーバ負荷を抑えたいため、可能であればクライアント側でPDFを生成したいと考えました。

デバッグのしやすさ

ブラウザの印刷機能を利用する場合は、デバッグの際に、印刷用CSSと画面表示用CSSを合わせて置けばレイアウト確認は楽々ですね。
ブラウザのデベロッパーツールも活用できるので、ルーラを表示したり、画面を見ながら微調整を試すことも容易です。

PDF生成エンジン/ライブラリの場合はデバッグは大変かも。

Vue.js での実装例

<template>
  <div class="sheets">
    <div>
      <el-button type="primary" @click="handlePrint">印刷</el-button>
      ※PDFで保存したい場合は印刷ダイアログで「PDF保存」を指定してください
    </div>
    <div class="sheet">
      <h2>帳票サンプル</h2>
      <h3>テーブルを印刷する</h3>
      <el-table :data="list" border fit>
        <el-table-column label="ID" prop="id" align="center" width="80px">
          <template slot-scope="scope">
            <span>{{ scope.row.id }}</span>
          </template>
        </el-table-column>
        <el-table-column label="Title" min-width="150px">
          <template slot-scope="{row}">
            <span>{{ row.title }}</span>
          </template>
        </el-table-column>
        <el-table-column label="Author" width="110px" align="center">
          <template slot-scope="scope">
            <span>{{ scope.row.author }}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <div class="sheet">
      <h3>改ページのテスト</h3>
      2ページ目
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: null
    }
  },
  created() {
    this.getList()
  },
  mounted() {
    this.fetchData()
  },
  methods: {
    getList() {
      // APIからデータ取得する想定
      this.list = [
        { id: 1, author: 'John Due', title: 'Hello, world' },
        { id: 2, author: '太郎', title: 'あいうえお かきくけこ' }
      ]
    },
    fetchData() {
      document.title = 'タイトルをいい感じに設定する'
      setTimeout(() => {
        this.$nextTick(() => {
          this.handlePrint()
        })
      })
    },
    handlePrint() {
      window.print()
    }
  }
}
</script>

<style lang="scss" scoped>
.sheet {
  page-break-after: always;
}

/* hide in print */
@media print {
  .sheets > :not(.sheet) {
    display: none;
  }
}

/* for preview */
@media screen {
  /* mm単位で指定しているけど、vueコンポ側はpx単位なので、無理にmmにしなくてもいいかも。解像度の違いでハマるかも */
  .sheet {
    width: 200mm;
    min-height: 296mm; /* 設定しなくてもいいかも。あまり印刷画面に似せすぎると、些細な違いがバグに見えてしまう */
    margin: 5mm;
    padding: 5mm;
    background: white;
    box-shadow: 0 .5mm 2mm rgba(0,0,0,.3);
  }
}
</style>
<style lang="scss">
/* for preview */
@media screen {
  BODY {
    background: #eee;
  }
}
</style>

Vueで実装した帳票プレビュー画面

Webシステムで帳票印刷機能を実行すると、プレビュー画面を表示するようにしています。
画面上部にある「印刷」ボタンは、メディアクエリにて画面表示の場合のみボタン表示して、印刷時には非表示にしています。

あえて、印刷プレビューっぽく見えるようにグレー背景、縦ページ、ドロップシャドウなどを設定していますが、ブラウザの印刷ダイアログでもプレビュー表示されるので、無くて良いかも。

Vueで作成したプレビュー画面.jpg

ブラウザの印刷ダイアログ

Chromeの印刷ダイアログの例。
デフォルトではWebページタイトルがPDFファイル名になるので、印刷直前にWebページタイトルを切り替えるように実装しておきます。

ブラウザの印刷ダイアログ.jpg

参考

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

Vue x Bootstrap Vue x Netlifyでさくっと作るTrello風アプリ

はじめに

こんにちは!
Mikatus株式会社でインフラエンジニアっぽいようなことをしている福田です。
最近フロントエンド領域に興味があり、デザインやJavaScriptの勉強をしています。
インフラネタにしようか迷ったのですが、今回はjavaScriptだけで動く簡単なアプリを作ってみました。

開発経緯

今や知らない人は少ないかもしれませんが、Trelloというのはタスク管理ウェブアプリケーションです。
https://trello.com/home

私は業務タスクだけでなく、家事タスクもTrelloを使って管理しています。

私はTrelloにおいて、タスクがドラックアンドドロップで移動できる部分が好きなので、この部分を自分で実装してみたいと思ったのが今回Trello風アプリを作ろうと思ったきっかけです。

こんなの作った

Trello風アプリ
image.png
はい、名前の通りTrelloっぽいアプリです。
機能としては以下。

  • リストを作成できる
  • リストの中にタスクを作成できる
  • タスクをドラッグアンドドロップで移動できる

コード
https://github.com/pistachiyoda/practice-trello-like-app

使用技術

  • Vue.js
  • Vue.Draggable
  • Bootstrap Vue
  • Netlify

致命的なバグ(実装間に合わんかった)

  • リストの移動ができない
  • リストの削除ができない
  • タスクの削除もできない
  • ブラウザをリロードしたら作ったリスト・タスクは消える
  • 背景をサンタクロースから変更できない
  • 空欄でもリスト・タスクが作成できちゃう(フォームバリデーション実装してない)

などなど…
目標だったドラッグアンドドロップでのタスク移動の部分は実装できた(といってもライブラリを使ってしまったので、実装できたと言っていいのかという葛藤…)のですが、細かいと部分はもっと詰めていかないといけません。
ちょっとずつ修正したり機能追加してみようと思います。

振り返り

実際に動くものを作るのは楽しいし勉強になりますね。
flexboxやgrid systemについて復習するいい機会になりました。

アプリのデプロイが終わった後の満足感にやられて記事の執筆に全く身が入らなかったため、アウトプット前提のアプリを開発する場合は開発と執筆を並行して実施したほうがいいなと思いました。

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

v-modelの使い方と双方向データバインディング

フォーム入力バインディング

フォーム(input要素, select要素, textarea要素など)の入力値 / 選択値をvueインスタンスdataを同期させる 「双方向データバインディング」を行うにはv-modelディレクティブを使用する。

index.html
<div id="app">
  <input v-model="message">
  <p>{{ message }}</p>
</div>
vue.js
new Vue ({
  el: '#app',
  data: {
    message: ''
  }
})

v-modelディレクティブは次の2つの処理を一つにまとめています。
(1) データバインディングで、要素のvalue属性(値)を更新する

v-bind:value="message"

(2) イベントハンドリング(イベントが発生したときに呼び出される処理)で、受け取った値をデータに代入

入力フォームにユーザが入力

↓  inputイベントが発生

this.message = event.target.value

で値を得られる。
messageプロパティの値が変更されます。

DOM要素へのデータバインディングと、要素から取得したデータをリアクティブデータに反映させるこの2つの処理を自動化した構文がv-model

ラジオボタン

ラジオボタンのデフォルトの値の方は文字列

index.html
<div>
    <span>性別:</span>
    <label>
      男性
      <input type="radio" value="male" v-model="form.sex">
    </label>
    <label>
      女性
      <input type="radio" value="female" v-model="form.sex">
    </label>
    <p>性別: {{ getRadioValue }}</p>
  </div>
vue.js
data: {
  return {
    //省略
    form: {
      sex: '',

    },
  }
},

value属性の値がバインディングされるので female / male のどちらかが
dataの中のsexプロパティへ代入されます。

セレクトボックス

index.html
<div>
  <select v-model="form.selected">
    <option disabled value="">--選択してください--</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
    <option>D</option>
  </select>
</div>

単数選択

vue.js
data: {
  return {
    //省略
    form: {
      selected: ''
    },
  }
},

複数選択

vue.js
data: {
  return {
    //省略
    form: {
      selected: []
    },
  }
},

チェックボックス

チェックボックスのデフォルト値の値の型はBoolean

index.html
<div>
  <label>
    <input type="checkbox" v-model="form.checked">
    20際以上です
  </label>
</div>
vue.js
data: {
  return {
    //省略
    form: {
      checked: true
    },
  }
},
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsで入力フォームにバリデーションをつける

はじめに

この記事はVue#2アドベントカレンダーの23日目です?

アドベントカレンダー初参加の@yakumomutsukiです。
当初は「vue-cliを使わなくてもできるvueを使った開発」を考えていたのですが、書いているうちにただのWebpackの説明になってしまったので、実務であるあるなお悩みを書いてみようと思いました。

バリデーション + 入力フォーム

フロントエンドの開発において、フォーム周りのバリデーションの設計は比較的悩みどころかなあと思います。zipというプロパティにたいして、watchを使ってバリデーションを張るか、computedを使うか、このあたりは実装者によってさまざまあると思います。

さまざまあるなかでの「私はこう実装した」という一例をご覧ください。

App.vue
<template>
  <div id="app">
    <div class="container">
      <form>
        <div class="form-row">
          <div class="form-group col-md-6">
            <label for="email">メールアドレス(必須)</label>
            <input
              id="email"
              required
              type="email"
              class="form-control"
              :class="formError.email.className"
              name="email"
              placeholder="メールアドレスを入力してください"
              @change="e => update(e)"
              :value="formState.email"
            />
            <span>{{ formError.email.errorMessage }}</span>
          </div>
          <div class="form-group col-md-6">
            <label for="password">パスワード(必須)</label>
            <input
              id="password"
              required
              type="password"
              class="form-control"
              :class="formError.password.className"
              name="password"
              placeholder="パスワード"
              @change="e => update(e)"
              :value="formState.password"
            />
            <span>{{ formError.password.errorMessage }}</span>
          </div>
        </div>
        <div class="form-row">
          <div class="form-group col-md-4">
            <label for="zip">郵便番号(必須)</label>
            <input
              id="zip"
              required
              type="text"
              class="form-control"
              :class="formError.zip.className"
              name="zip"
              placeholder="1000000"
              maxlen="7"
              @change="e => update(e)"
              :value="formState.zip"
            />
            <p>{{ formError.zip.errorMessage }}</p>
          </div>
          <div class="form-group col-md-4">
            <label for="state">都道府県(必須)</label>
            <select
              id="state"
              class="form-control"
              :class="formError.state.className"
              :value="formState.state"
              @change="e => update(e)"
              required
            >
              <option value="" selected>選択してください</option>
              <option value="tokyo">東京</option>
              <option value="osaka">大阪</option>
            </select>
            <p>{{ formError.state.errorMessage }}</p>
          </div>
          <div class="form-group col-md-4">
            <label for="city">市区町村(必須)</label>
            <input
              id="city"
              required
              type="text"
              class="form-control"
              :class="formError.city.className"
              name="city"
              placeholder="千代田区"
              @change="e => update(e)"
              :value="formState.city"
            />
            <p>{{ formError.city.errorMessage }}</p>
          </div>
        </div>
        <div class="form-group">
          <label for="address1">住所1(必須)</label>
          <input
            id="address1"
            required
            type="text"
            class="form-control"
            :class="formError.address1.className"
            name="address1"
            placeholder="秋葉原1-1-1"
            @change="e => update(e)"
            :value="formState.address1"
          />
          <p>{{ formError.address1.errorMessage }}</p>
        </div>
        <div class="form-group">
          <label for="address2">住所2</label>
          <input
            id="address2"
            type="text"
            class="form-control"
            name="address2"
            placeholder="秋葉原ビル1F"
            @change="e => update(e)"
            :value="formState.address2"
          />
        </div>
        <button class="btn btn-primary" @click="registration">登録する</button>
      </form>
    </div>
  </div>
</template>

<script>
const errorBase = {
  errorMessage: "",
  className: ""
};
const patterns = {
  password: /^(?=.*?[a-zA-Z])(?=.*?\d)[a-zA-Z\d]{8,16}$/,
  zip: /^[0-9]{7}$/,
  phoneNumber: /^[0-9]{10,11}$/
};

import isEmail from "validator/lib/isEmail";
import isMatch from "validator/lib/matches";

export default {
  name: "App",
  data() {
    return {
      formState: {
        email: "",
        password: "",
        zip: "",
        state: "",
        city: "",
        address1: "",
        address2: ""
      },
      formError: {
        email: { ...errorBase },
        password: { ...errorBase },
        zip: { ...errorBase },
        state: { ...errorBase },
        city: { ...errorBase },
        address1: { ...errorBase },
        address2: { ...errorBase }
      }
    };
  },
  methods: {
    /**
     * 値が更新されたらupdateメソッドが呼ばれる
     * formStateの値を更新したら、バリデーションを実行
     * @param e HTMLElement
     */
    update(e) {
      const { name, value } = e.target;
      this.formState[name] = value;
      this.formError[name] = { ...errorBase };

      this.validate(name);
    },

    /**
     * バリデーションを行います
     * 未入力チェックが必要な場合は、第2引数にfalseを指定する
     * @param name
     * @param allowBlank
     */
    validate(name, allowBlank = true) {
      const formVal = this.formState[name];

      // 未入力チェックをする場合
      if (!allowBlank && !formVal) {
        this.formError[name] = {
          errorMessage: "未入力です",
          className: "error"
        };
        return;
      }

      // メールアドレス
      if (name === "email" && formVal && !isEmail(formVal)) {
        this.formError[name] = {
          errorMessage: "メールアドレスに誤りがあります",
          className: "error"
        };
        return;
      }

      // パスワード
      if (
        name === "password" &&
        formVal &&
        !isMatch(formVal, patterns.password)
      ) {
        this.formError[name] = {
          errorMessage: "パスワードに誤りがあります",
          className: "error"
        };
        return;
      }

      // 郵便番号
      if (name === "zip" && formVal && !isMatch(formVal, patterns.zip)) {
        this.formError[name] = {
          errorMessage: "郵便番号に誤りがあります",
          className: "error"
        };
      }
    },

    /**
     * 登録処理を行います
     * バリデーションで引っかかった場合は、登録処理を行えません
     */
    registration() {
      for (let [key] of Object.entries(this.formState)) {
        // バリデーションを行わないのはcontinueで抜けておく
        if (key === "address2") {
          continue;
        }
        // バリデーションを実行
        this.validate(key, false);
      }

      let isValid = true;
      for (let [key] of Object.entries(this.formError)) {
        // エラーメッセージがない場合にはtrueになる
        isValid = !this.formError[key].errorMessage ? isValid : false;
      }

      if (isValid) {
        window.alert("登録に成功しました");
      } else {
        window.alert("入力不備があります");
      }
    }
  }
};
</script>

<style>
.error {
  background-color: #ffecec !important;
}
</style>

お気づきになりましたでしょうか。
このコードはwatchもcomputedも使っておりません。しかし、formStateとfromErrorの各プロパティのキー名を同じにしておくことで、updateメソッドからvalidateメソッドに同じキー名を渡して、バリデーションをかけることができています。

    update(e) {
      const { name, value } = e.target;
      this.formState[name] = value;
      this.formError[name] = { ...errorBase };

      this.validate(name);
    },

引数として受け取ったHTMLElementから、const { name, value } = e.target;のように分割代入して、nameとvalueを受け取り、formStateに値を設定、fromErrorの初期化して、もう一度バリデーションという流れになっています。

おわりに

書いていて思ったのですが、別にVueじゃなくてJavaScriptのテクニック感集ですね...。v-modelやwatch、computedは使わなくてもバリデーションはできますというお話でした。
明日のアドベントカレンダーは@kacky917さんです!!?

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

Vue.jsのtemplateの中でURLとかの文字列をフィルタで置き換えする方法

タイトルからすごく直感的に分かりづらいタイトル!

【本記事の目的】

vue.jsの中で、文字列の中の一部をフィルターで変更したい!
(phpでいうところのstr_replace)

【前置き】(長いので飛ばしてもokです)

laravelでは画像データを投稿すると、デフォルトでpublicに保管されます。
それを実際に表示させるには画像をシンボリックリンクでstorage側で表示させる事を推奨されています。
参考:https://qiita.com/koru1893/items/1d2f522e20744b03e3ad

でもそれをlaravel経由のvue.jsで表示させようとするとうまくいかず。
vue.jsの中でimgタグの参照urlをpublic→storage に置き換える
という作業にハマったのでメモです。

**publicだと読み込めず、storageだと読み込める状態です。
Sky_Light_Lover.png

なお、php(view)でそのデータを読み込むだけなら
str_replaceで画像のurlを変数にして置き換えて、imgタグの読み込み先として指定すれば行けます。

//$image_urlがdbから持ってきたurl情報。urlの一部を置き換えるということ。
$image_url = str_replace('public/', 'storage/', $image_url);

phpのview内でやる方法で完了しているじゃん!て思うんですが、
vue.jsとか使って非同期でdb上のURL文字列を取ってくる場合、img参照用のURLをjsファイル内で(今回の場合vue.jsん中で)差し替える必要があります。


やっと本題

カスタムフィルタを作りました!
(※vue.jsのフィルタ機能は今は自作しないとダメぽい。vue.1系はフィルタのメソッドあったぽいです。なんで削除されたんだろう?)

filtersの中に、
「public」という文字列を「..storage」に置き換える
「replace」という処理を作りました。
(置き換え前、置き換え先のコードはもちろん任意です。)

vue.js
<script>
    export default {
        props:['hoge'],
        data:function(){
            return{
             (略)
            }
        },
        mounted(){
          (略)
        },
        filters:{ //カスタムフィルタ。こんな感じ!
            replace:function(val){
                return val.replace("public", "../storage");   
            }
        },
       }
    }
</script>


replaceのフィルタは準備できました。
あとはtemplateの中で、フィルタします!
(※vue.jsの中では {{ フィルタしたいデータ|フィルタの処理名 }} という書き方でフィルタリングできます。マスタッシュ!)

vue.js
<template>
(略)
<img :src="image_url | replace('public','../storage')" alt="">
(略)
</template>

これで、urlのなかの一部を置き換えられます。
公開サイト側でソースをみると、下記のようになります。

フィルタなしの結果

<img src="public/post_images/hoge.jpg">

フィルタありの結果

<img src="../storage/post_images/hoge.jpg">

これできちんと画像が公開サイトで読み込めました。やった!

参考:https://www.itsolutionstuff.com/post/how-to-replace-string-in-vue-jsexample.html

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

【Laravel】 Vue.jsのtemplateの中で文字列置き換えする方法【フィルタ】

タイトルからすごく直感的に分かりづらいタイトル!

【前置き】

まず、larabelでは投稿した画像はpublicに保管されてしまうんですが
それを実際に表示させるには画像をシンボリックリンクでstorage側で表示させないといけません。
参考:https://qiita.com/koru1893/items/1d2f522e20744b03e3ad
で、本記事でやりたいことは
vue.jsの中でimgタグの参照urlをpublic→storage に置き換える
という作業。

Sky_Light_Lover.png

php(view)でそのデータを読み込むだけなら
str_replaceで画像のurlを変数にして置き換えて、imgタグの読み込み先として指定すれば行けます。

//$image_urlがdbから持ってきたurl情報。urlの一部を置き換えるということ。
$image_url = str_replace('public/', 'storage/', $image_url);

本題

長くなりましたが本題。
vue.jsで文字列置き換え、前述のreplace的なことをやろうとして「フィルタが良さそう!」
となったんですが、いい感じのreplace処理がない!
参考情報調べても少しややこいreplaceばかり出てきたけど、簡単そうな方法がありました。
参考:https://www.itsolutionstuff.com/post/how-to-replace-string-in-vue-jsexample.html

調べるのに少し時間かかったので、備忘録を兼ねて。

やろうとしたこと

  1. コントローラーでDBから取得したデータを
  2. view に持ってきて、そこからvue.jsに受け渡す。
  3. vue.js内で受け取ったデータのimage出力するときに、url情報を一部書き換えたい(書き換えたい理由は[前提]参照)

1.2は割愛。この辺りが参考になるかもです!

3でやった方法。
カスタムフィルタで特定のコードをreplaceしたい!
※vue.jsのフィルタ機能は今は自作しないとダメぽい(vue.1系はフィルタのメソッドあったぽいです。なんで削除されたんだろう)

filtersの中に、
「public」という文字列を「..storage」に置き換える
「replace」というフィルタを作りました。

vue.js
<script>
    export default {
        props:['hoge'],
        data:function(){
            return{
             (略)
            }
        },
        mounted(){
          (略)
        },
        filters:{ //カスタムフィルタ。こんな感じ!
            replace:function(val){
                return val.replace("public", "../storage");   
            }
        },
       }
    }
</script>


replaceのフィルタは準備できました。
あとはtemplateの中で、フィルタします!
vue.jsの中では
{{ フィルタしたいデータ|フィルタの処理名 }}
という書き方でフィルタリングできます

vue.js
<template>
(略)
<img :src="image_url | replace('public','../storage')" alt="">
(略)
</template>

これで、urlのなかの一部を置き換えられます。
公開サイト側でソースをみると、下記のようになります。

フィルタなしの結果

<img src="public/post_images/hoge.jpg">

フィルタありの結果

<img src="../storage/post_images/hoge.jpg">

これできちんと読み込めました。やった!

参考:https://www.itsolutionstuff.com/post/how-to-replace-string-in-vue-jsexample.html

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

【Laravel】 Vue.jsのtemplateの中でURLとかの文字列置き換えする方法【フィルタ/非同期】

タイトルからすごく直感的に分かりづらいタイトル!

【前置き】

まず、larabelでは投稿した画像はpublicに保管されてしまうんですが
それを実際に表示させるには画像をシンボリックリンクでstorage側で表示させないといけません。
参考:https://qiita.com/koru1893/items/1d2f522e20744b03e3ad
で、本記事でやりたいことは
vue.jsの中でimgタグの参照urlをpublic→storage に置き換える
という作業。

Sky_Light_Lover.png

php(view)でそのデータを読み込むだけなら
str_replaceで画像のurlを変数にして置き換えて、imgタグの読み込み先として指定すれば行けます。

//$image_urlがdbから持ってきたurl情報。urlの一部を置き換えるということ。
$image_url = str_replace('public/', 'storage/', $image_url);

phpのview内でやる方法で完了しているじゃん!て思うんですが、
vue.jsで非同期でも取ってくるのをやるのも試そうとして、img参照用のURLを差し替える方法を探すのに時間がかかったのです。


本題

長くなりましたが本題。
vue.jsで文字列置き換え、前述のreplace的なことをやろうとして「フィルタが良さそう!」
となったんですが、いい感じのreplace処理をやるのに簡単そうな方法がありました。
参考:https://www.itsolutionstuff.com/post/how-to-replace-string-in-vue-jsexample.html

もう少し細かく全体の流れをかくと

  1. コントローラーでDBから取得したデータを
  2. view に持ってきて、そこからvue.jsに受け渡す。
  3. vue.js内で受け取ったデータのimage出力するときに、url情報を一部書き換えたい(書き換えたい理由は[前提]参照)

1.2は割愛。この辺りが参考になるかもです!

で、3でやった方法。

カスタムフィルタで特定のコードをreplaceしました!
(※vue.jsのフィルタ機能は今は自作しないとダメぽい。vue.1系はフィルタのメソッドあったぽいです。なんで削除されたんだろう?)

filtersの中に、
「public」という文字列を「..storage」に置き換える
「replace」という処理を作りました。
(置き換え前、置き換え先のコードはもちろん任意です。)

vue.js
<script>
    export default {
        props:['hoge'],
        data:function(){
            return{
             (略)
            }
        },
        mounted(){
          (略)
        },
        filters:{ //カスタムフィルタ。こんな感じ!
            replace:function(val){
                return val.replace("public", "../storage");   
            }
        },
       }
    }
</script>


replaceのフィルタは準備できました。
あとはtemplateの中で、フィルタします!
(※vue.jsの中では {{ フィルタしたいデータ|フィルタの処理名 }} という書き方でフィルタリングできます。マスタッシュ)

vue.js
<template>
(略)
<img :src="image_url | replace('public','../storage')" alt="">
(略)
</template>

これで、urlのなかの一部を置き換えられます。
公開サイト側でソースをみると、下記のようになります。

フィルタなしの結果

<img src="public/post_images/hoge.jpg">

フィルタありの結果

<img src="../storage/post_images/hoge.jpg">

これできちんと画像が公開サイトで読み込めました。やった!

参考:https://www.itsolutionstuff.com/post/how-to-replace-string-in-vue-jsexample.html

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

v-forによる繰り返しの描画

v-forによる繰り返し

配列やオブジェクトをループ処理して、
要素を繰り返しを描画するには、v-forディレクティブを使用する。

index.html
// v-for 構文
<li v-for="各要素を代入する変数名 in 繰り返したい配列やオブジェクト">
index.html
<div id="app">
  <ul>
    <li v-for="item in items" v-bind:key="item.id">
  </ul>
</div>
app.js
new Vue ({
  el: '#app',
  data: {
    items: [
      { id: 1,
        title: '1番目のリスト',
      },
      { id: 2,
        title: '2番目のリスト',
      },
      { id: 3,
        title: '3番目のリスト',
      },
    ]
  }
})

dataオプションに登録されているitemsの配列から、v-forディレクティブを使って1つずつ要素を取り出し描画します。

Key の役割

v-bind:key="item.id"

v-forディレクティブでループしている要素に対しては、v-bind:keyディレクティブにその要素として識別情報となる値(一意の値)を指定することが推奨されています。Vueが各要素を効率よく追跡できるようにするため、ということのようです。

:keyv-bind:keyの省略構文。

不変でユニークなキーを設定しよう

要素の削除や並び替えも考慮して「不変でユニークなキー」を設定する必要があります。
ユニークな乱数を生成するuuidや、vue-uuidを使用して:keyの値を乱数で指定することができます。

uuid
vue-uuid

(例)「vue-uuid」を使用して:keyを指定する際は乱数を値に指定できるようにする場合

index.js
import Vue from 'vue';
import uuid from 'vue-uuid'; // 追加

Vue.use(uuid); // 追加

new Vue({
  el: '#app'
});

uuidモジュールをimportして、Vue.use()の引数に指定することによって
アプリケーションでuuidを使用できる様にします。

app.vue.js
data() {
  return {
    //省略
    items: [
      {
        id: this.$uuid.v4(), // バージョン4のUUIDは、乱数により生成される。
        title: '1番目のリスト',
      },
      {
        id: this.$uuid.v4(),
        title: '2番目のリスト'
      },
      {
        id: this.$uuid.v4(),
        title: '3番目のリスト'
      }
    ],
  }
}

これでidには乱数が指定されるようになりました。

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

【入門】Laravel×Vue.js②〜実装編〜

はじめに

PHPのフレームワークであるLaravelで作成したアプリケーションに
JavaScriptのフレームワークであるVue.jsを連携させる方法について説明します。
簡単な機能を実装します。自分のコンポーネントを登録する方法について説明します
実装する機能
  メッセージを表示して、ボタンを押すと、メッセージを反転する

コンポーネントの作成

まず、resources/js/components/内にHelloComponent.vueを作成してください
そして、内容を以下のようにしてください

resources/js/components/HelloComponent.vue
<template>
    <div id="app">
      <p>{{ message }}</p>
      <button v-on:click="reverseMessage">Reverse Message</button>
    </div>
</template>
<script>
export default {
      data:function(){
          return {
            message: 'Hello Vue!',
          };
      },
      methods: {
        reverseMessage: function () {
          this.message = this.message.split('').reverse().join('')
        }
      }
}
</script>

コンポーネントの登録

上で作成したコンポーネントを登録しましょう

resource/js/app.js
require('./bootstrap');

window.Vue = require('vue');

Vue.component('example-component', require('./components/ExampleComponent.vue').default);
Vue.component('hello-component', require('./components/HelloComponent.vue').default);


ビューファイルへの組み込み

ビューファイルの追加したい時に下記を記述してください

hello/index.blade.php
<div id="app">
  <hello-component></hello-component>
</div>

以上で実装完了です。

疑問、気になるところがございましたら、質問、コメントよろしくお願いします!!!

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

メソッドに引数を与える ❏Vue.js❏

メソッドの引数に与えた数の倍数分カウントアップしていきます!

(例)3を与えるとこうなります。

Screenshot from Gyazo



開発環境はJSFiddle
https://qiita.com/ITmanbow/items/9ae48d37aa5b847f1b3b

html
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
  <p>{{ number }}</p>
  <button @click="countUp(3)">カウントアップ</button>
</div>
javascript
new Vue({
  el: "#app",
  data: {
    number: 0
  },
  methods: {
    countUp: function(times) {
      this.number += times;
    }
  }
})

①html側には引数3を与えます。
@click="countUp(3)"

②js側にはtimesとして引数を受け取ります。
countUp: function(times)

③引数分足します
this.number += times;

できあがり!



ではまた!

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

初心者によるプログラミング学習ログ 191日目

100日チャレンジの191日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

191日目は

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

【CGI】Vue.js + axiosでサーバのPythonプログラムを動かし出力データをもらう

はじめに

フロントエンドで全部できるっちゃできるけど、やっぱり計算部分にPythonの楽さは欲しいなあ、という思いからサーバでプログラムを動的に動かすCGIを触ってみたのでそのまとめ。バックエンドは未だ疎い。

フリーレンタルサーバーのXREAのアカウントを持っているので実際にそこで試してみる。
無料から使える高機能・高品質レンタルサーバー | XREA(エクスリア)

やりたいことは、

  1. ボタンをクリック
  2. Vue.jsとaxiosを使ってデータをサーバに渡す
  3. Pythonで書かれたCGIスクリプトを実行して演算、結果をJSONで返す
  4. 同じ画面に返されたデータを表示

というかんじ。

今回は例として入力された2数の最大公約数と最小公倍数を計算して返すプログラムを作る。

作ってみる

ファイルの配置

├─ cgi-bin
│    └─ index.cgi
├─ index.html
└─ main.js

Pythonで書かれたCGIスクリプトは cgi-bin というディレクトリを作ってその中に投入。Pythonで書かれてるけど拡張子は .cgi

サーバにアップロードした後、CGIまわりのパーミッションを指定する必要があるけどそれは後述。

HTML (index.html)

<html>
<head>
    <meta charset="UTF-8">
    <title>最大公約数と最小公倍数を返す</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
    <script type="module" src="main.js"></script>
</head>
<body>
    <div id="app">
        <!-- 入力部分 -->
        <p>
            <input v-model="a" type="number" value="12">
            <input v-model="b" type="number" value="15">
            <button v-on:click="getResult">計算</button>
        </p>
        <!-- 表示部分 -->
        <p>
            最大公約数: {{ result['gcd'] }}<br>
            最小公倍数: {{ result['lcm'] }}
        </p>
    </div>
</body>
</html>

CDNでVue.jsとaxiosを読み込んでおく。
Vueで書いたJavaScriptのファイルは type="module" で読み込むと、DOMが読み込まれた後に実行されるので安心。

JavaScript (main.js)

var vm = new Vue({
    el: '#app',
    data: {
        a: 12,
        b: 15,
        result: {},  // 返ってきたオブジェクトを格納する変数
    },
    methods: {
        getResult: function() {
            const url = './cgi-bin/';
            // axiosでデータの受け渡し
            axios.get(url, {
                params: {  // paramsでパラメータを指定
                    a: this.a,
                    b: this.b
                }
            })
            .then(res => this.result = res.data)  // データをresultに格納
        }
    }
});

Vueインスタンスを作ってaxiosでデータの受け渡しをする。
URLは ./cgi-bin/index.cgi という名前ならHTML同様ファイル名を指定しなくてもOKなもよう。

また、返ってくるデータ res.data はオブジェクトとして取り出せるので JSON.parse() とかで直す必要がなかった。(JSONにして返したんだからJSON文字列で返ってくるだろうという思い込みにやられた。)
ちなみにやるとこんなエラーが出る。

SyntaxError: Unexpected token o in JSON at position 1

o ってなんぞやと思ったら objecto らしい...

Python CGI (index.cgi)

#!/usr/local/bin/python3
# -*- coding: UTF-8 -*-
import cgi
import cgitb
import math
import json

cgitb.enable()  # CGIのデバッグをオン

form = cgi.FieldStorage()  # GETで得たデータを格納
a = int(form['a'].value)   # データは文字列でやってくるので数値に変換
b = int(form['b'].value)

data = {
    'gcd': math.gcd(a, b),  # math.gcd() は Python 3.5 で追加されたらしい
    'lcm': a * b // math.gcd(a, b)
}

print('Content-Type: application/json')
print()
print(json.dumps(data))  # json形式にして返す

行頭でPythonのインタプリタの場所を指定。レンタルサーバの仕様ページに各言語のCGIの実行パスが書いてあると思うので確認する。XREAだとここ。
xrea_py_path.png

Context-Type 指定の行の後の print() は改行のためで、これを入れないとダメな仕様みたい。 普通に Context-Type 文字列の最後に \n してもOK。

アップロード後はCGIスクリプトを入れたディレクトリとそのファイルのパーミッションを指定する必要があり、XREAにも推奨パーミッションが書いてある。
xrea_permission.png
特に、CGIの実行ファイルは700以上(実行可能)にする必要があるのは注意(よく忘れた)。

以上のXREAの仕様が書いてあるリンクも貼っておく。
仕様 | 無料から使える高機能・高品質レンタルサーバー | XREA(エクスリア)

実行

ファイルを上げて実際に試してみた画面がこちら
xrea_cgi_disp.png
いいかんじ。空欄や小数で送信したら普通にエラーになるけど、まあお試しなので。

以上。

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

【CGI】Vue.jsとaxiosを使ってサーバに置いたPythonプログラムを動かしデータを得る

はじめに

フロントエンドで全部できるっちゃできるけど、やっぱりPythonの計算の楽さは欲しいなあと思ってCGIについていろいろ調べたのでそのまとめ。
フリーレンタルサーバーのXREAのアカウントを持っているので実際にそこで試してみます。
無料から使える高機能・高品質レンタルサーバー | XREA(エクスリア)

やりたいことは、

  1. ボタンをクリック
  2. Vue.jsとaxiosを使ってデータをサーバに渡す
  3. Pythonで書かれたCGIスクリプトを実行して演算、結果をJSONで返す
  4. 同じ画面に返されたデータを表示

というかんじ。
今回は例として入力された2数の最大公約数と最小公倍数を計算して返すプログラムを作ります。

作ってみる

ファイルの配置

├─ cgi-bin
│    └─ index.cgi
├─ index.html
└─ main.js

Pythonで書かれたCGIスクリプトは cgi-bin というディレクトリを作ってその中に投入。Pythonで書かれてるけど拡張子は .cgi
サーバにアップロードした後、CGIまわりのパーミッションを指定する必要があるけどそれは後述。

HTML

<html>
<head>
    <meta charset="UTF-8">
    <title>最大公約数と最小公倍数を返す</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
    <script type="module" src="main.js"></script>
</head>
<body>
    <div id="app">
        <!-- 入力部分 -->
        <p>
            <input v-model="a" type="number" value="12">
            <input v-model="b" type="number" value="15">
            <button v-on:click="getResult">計算</button>
        </p>
        <!-- 表示部分 -->
        <p>
            最大公約数: {{ result['gcd'] }}<br>
            最小公倍数: {{ result['lcm'] }}
        </p>
    </div>
</body>
</html>

CDNでVue.jsとaxiosを読み込んでおく。
Vueで書いたJavaScriptのファイルは type="module" で読み込むと、DOMが読み込まれた後に実行されるので安心。

JavaScript

var vm = new Vue({
    el: '#app',
    data: {
        a: 12,
        b: 15,
        result: {},  // 返ってきたオブジェクトを格納する変数
    },
    methods: {
        getResult: function() {
            const url = './cgi-bin/';
            // axiosでデータの受け渡し
            axios.get(url, {
                params: {  // paramsでパラメータを指定
                    a: this.a,
                    b: this.b
                }
            })
            .then(res => this.result = res.data)  // データをresultに格納
        }
    }
});

Vueインスタンスを作ってaxiosでデータの受け渡しをする。
URLは ./cgi-bin/index.cgi という名前ならHTML同様ファイル名を指定しなくてもOKなよう。
また、返ってくるデータ res.data はオブジェクトとして取り出せるので JSON.parse() とかで直す必要がなかった。(JSONにして返したんだからJSON文字列で返ってくるだろうという思い込みにやられた。)
ちなみにやるとこんなエラーが出る。

SyntaxError: Unexpected token o in JSON at position 1

o ってなんぞやと思ったら objecto らしい...

Python CGI

#!/usr/local/bin/python3
# -*- coding: UTF-8 -*-
import cgi
import cgitb
import math
import json

cgitb.enable()  # CGIのデバッグをオン

form = cgi.FieldStorage()  # GETで得たデータを格納
a = int(form['a'].value)   # 辞書型で参照が可能
b = int(form['b'].value)   # データは文字列でやってくるので数値に変換

data = {
    'gcd': math.gcd(a, b),  # math.gcd() は Python 3.5 で追加されたらしい
    'lcm': a * b // math.gcd(a, b)
}

print('Content-Type: application/json')
print()
print(json.dumps(data))  # json形式にして返す

行頭でPythonのインタプリタの場所を指定。レンタルサーバの仕様ページに各言語のCGIの実行パスが書いてあると思うので確認する。XREAだとここ。
xrea_py_path.png
Context-Type 指定の行の後の print() は改行のためで、これを入れないとダメな仕様みたい。 普通に Context-Type 文字列の最後に \n してもOK。

アップロード後はCGIスクリプトを入れたディレクトリとそのファイルのパーミッションを指定する必要があり、XREAにも推奨パーミッションが書いてある。
xrea_permission.png
特に、CGIの実行ファイルは700以上(実行可能)にする必要があるのは注意(よく忘れた)
以上のXREAの仕様が書いてあるリンクも貼っておきます。
仕様 | 無料から使える高機能・高品質レンタルサーバー | XREA(エクスリア)

実行

ファイルを上げて実際に試してみた画面がこちら
xrea_cgi_disp.png

いいかんじ。空欄や小数で送信したら普通にエラーになるけど、まあ試しなので。

以上。

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

【CGI】Vue.jsとaxiosを使ってサーバのPythonプログラムを動かしデータを得る

はじめに

フロントエンドで全部できるっちゃできるけど、やっぱり計算部分にPythonの楽さは欲しいなあ、という思いからサーバでプログラムを動的に動かすCGIを触ってみたのでそのまとめ。バックエンドは未だ疎い。

フリーレンタルサーバーのXREAのアカウントを持っているので実際にそこで試してみる。
無料から使える高機能・高品質レンタルサーバー | XREA(エクスリア)

やりたいことは、

  1. ボタンをクリック
  2. Vue.jsとaxiosを使ってデータをサーバに渡す
  3. Pythonで書かれたCGIスクリプトを実行して演算、結果をJSONで返す
  4. 同じ画面に返されたデータを表示

というかんじ。
今回は例として入力された2数の最大公約数と最小公倍数を計算して返すプログラムを作る。

作ってみる

ファイルの配置

├─ cgi-bin
│    └─ index.cgi
├─ index.html
└─ main.js

Pythonで書かれたCGIスクリプトは cgi-bin というディレクトリを作ってその中に投入。Pythonで書かれてるけど拡張子は .cgi
サーバにアップロードした後、CGIまわりのパーミッションを指定する必要があるけどそれは後述。

HTML (index.html)

<html>
<head>
    <meta charset="UTF-8">
    <title>最大公約数と最小公倍数を返す</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
    <script type="module" src="main.js"></script>
</head>
<body>
    <div id="app">
        <!-- 入力部分 -->
        <p>
            <input v-model="a" type="number" value="12">
            <input v-model="b" type="number" value="15">
            <button v-on:click="getResult">計算</button>
        </p>
        <!-- 表示部分 -->
        <p>
            最大公約数: {{ result['gcd'] }}<br>
            最小公倍数: {{ result['lcm'] }}
        </p>
    </div>
</body>
</html>

CDNでVue.jsとaxiosを読み込んでおく。
Vueで書いたJavaScriptのファイルは type="module" で読み込むと、DOMが読み込まれた後に実行されるので安心。

JavaScript (main.js)

var vm = new Vue({
    el: '#app',
    data: {
        a: 12,
        b: 15,
        result: {},  // 返ってきたオブジェクトを格納する変数
    },
    methods: {
        getResult: function() {
            const url = './cgi-bin/';
            // axiosでデータの受け渡し
            axios.get(url, {
                params: {  // paramsでパラメータを指定
                    a: this.a,
                    b: this.b
                }
            })
            .then(res => this.result = res.data)  // データをresultに格納
        }
    }
});

Vueインスタンスを作ってaxiosでデータの受け渡しをする。
URLは ./cgi-bin/index.cgi という名前ならHTML同様ファイル名を指定しなくてもOKなよう。
また、返ってくるデータ res.data はオブジェクトとして取り出せるので JSON.parse() とかで直す必要がなかった。(JSONにして返したんだからJSON文字列で返ってくるだろうという思い込みにやられた。)
ちなみにやるとこんなエラーが出る。

SyntaxError: Unexpected token o in JSON at position 1

o ってなんぞやと思ったら objecto らしい...

Python CGI (index.cgi)

#!/usr/local/bin/python3
# -*- coding: UTF-8 -*-
import cgi
import cgitb
import math
import json

cgitb.enable()  # CGIのデバッグをオン

form = cgi.FieldStorage()  # GETで得たデータを格納
a = int(form['a'].value)   # データは文字列でやってくるので数値に変換
b = int(form['b'].value)

data = {
    'gcd': math.gcd(a, b),  # math.gcd() は Python 3.5 で追加されたらしい
    'lcm': a * b // math.gcd(a, b)
}

print('Content-Type: application/json')
print()
print(json.dumps(data))  # json形式にして返す

行頭でPythonのインタプリタの場所を指定。レンタルサーバの仕様ページに各言語のCGIの実行パスが書いてあると思うので確認する。XREAだとここ。
xrea_py_path.png
Context-Type 指定の行の後の print() は改行のためで、これを入れないとダメな仕様みたい。 普通に Context-Type 文字列の最後に \n してもOK。

アップロード後はCGIスクリプトを入れたディレクトリとそのファイルのパーミッションを指定する必要があり、XREAにも推奨パーミッションが書いてある。
xrea_permission.png
特に、CGIの実行ファイルは700以上(実行可能)にする必要があるのは注意(よく忘れた)
以上のXREAの仕様が書いてあるリンクも貼っておく。
仕様 | 無料から使える高機能・高品質レンタルサーバー | XREA(エクスリア)

実行

ファイルを上げて実際に試してみた画面がこちら
xrea_cgi_disp.png

いいかんじ。空欄や小数で送信したら普通にエラーになるけど、まあ試しなので。

以上。

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

vue-apolloでのsubscriptionの設定方法(リアルタイム同期のChatアプリのサンプル付き)

この記事はVue.js Advent Calendar 23日目の記事です。

リアルタイム更新というとFirebase Firestoreが思い浮かびますが、
実はGraphQLのsubscriptionを使っても手軽に出来ます!
vue-apolloでのsubscriptionの設定についての日本語記事があまりなかったので今回紹介します。

サンプルアプリ

「説明いらんから、はよコード」という方は、こちらから。
https://github.com/kawamataryo/hasura-vue-chat-app

subscriptionsの練習用に作ったリアルタイム同期のチャットアプリです。
HasuraでGraphQLサーバーを立てて、vue-apolloでsubscriptionを行っています。
READMEの通り起動すれば数分でhasura, vue-apolloのsubscriptionを体験できます。
(練習用に作ったので優しい目でお願いします。。)

Dec-21-2019 16-44-3.gif

Subscriptionとは?

GraphQLでのWebSocketを使ったリアルタイムデータ通信の手段です。
subscriptionsを使うと、更新された内容をページリロードなしにリアルタイムでサーバーから、Webページに直接通知することが出来ます。

vue-apolloの設定

では本題のvue-apolloのsubscriptionの設定です。
ポイントは、クエリの種別でhttp通信にするか、websocket通信にするか設定しているところです。
new HttpLinkでhttp、new WebSocketLinkでwebsocketのエンドポイントを設定し、
split関数で呼ばれるクエリによって向き先を変更しています。

import Vue from "vue";
import { ApolloClient } from "apollo-client";
import { HttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import { split } from "apollo-link";
import { WebSocketLink } from "apollo-link-ws";
import { getMainDefinition } from "apollo-utilities";
import VueApollo from "vue-apollo";

// 通常のquery, mutationsで使われるhttp通信のエンドポイントを指定
const httpLink = new HttpLink({
  uri: "http://localhost:8000/v1/graphql"
  // トークンによる認証を追加する場合は以下のように設定
  // headers: {
  //   authorization: `Bearer xxxxx`
  // }
});

// subscriptionで使われるwebsocket通信のエンドポイントを指定
const wsLink = new WebSocketLink({
  uri: "ws://localhost:8000/v1/graphql",
  options: {
    reconnect: true
 // トークンによる認証を追加する場合は以下のように設定
 // headers: {
 //   authorization: `Bearer xxxxx`
 // }
  }
});

// GraphQLのエンドポイントの向き先をクエリの種別で分離するための設定
// 参考 https://www.apollographql.com/docs/link/composition/#directional-composition
const link = split(
  ({ query }) => {
    // クエリから種別を取得してsubscriptionの場合は、wsLink(websocket)を利用する。
    const definition = getMainDefinition(query);
    return (
      definition.kind === "OperationDefinition" &&
      definition.operation === "subscription"
    );
  },
  wsLink,
  httpLink
);

// Apollo Clientの作成
const apolloClient = new ApolloClient({
  link,
  cache: new InMemoryCache(),
  connectToDevTools: true
});

Vue.use(VueApollo);

// vue-apolloへの接続
export const apolloProvider = new VueApollo({
  defaultClient: apolloClient
});

subscribeの実行

サンプルアプリのコードをもとに説明します。
通常のvue-apolloのquery内にsubscriptionsToMoreを追加することでそのqueryに紐づくデータをsubscriptionsの結果でリアルタイム更新することが出来ます。
documentにsubscriptionのクエリを設定し、対象のデータに変更がある場合、updateQueryで設定した関数が呼ばれ、queryに紐づくデータ(この場合はmessages)が更新されます。

export default {
  data() {
    return {
      messages: []
    }
  },
  apollo: {
    messages: {
      query: gql`
        query {
          messages {
            content
            userId: user_id
            iconColor: icon_color
            id
          }
        }
      `,
      subscribeToMore: {
        document: gql`
          subscription {
            messages {
              id
              userId: user_id
              iconColor: icon_color
              content
            }
          }
        `,
        updateQuery: (previousResult, { subscriptionData }) => {
          return subscriptionData.data;
        }
      }
    }
  .
  .
  .
}

終わりに

以上、vue-apolloでのsubscriptionsの設定方法でした。
リアルタイム更新をしようと思うとぱっと思いつくのはFirestoreですが、GraphQLのSubscriptionも手軽でおすすめです。(Hasuraを使えばなおさら)

vue-apolloは、上手くApolloをVue.js仕様にラップしていて僅かなコードでGraphQL触れるのでとても良いですね。

参考

https://vue-apollo.netlify.com/guide/apollo/subscriptions.html#setup
https://www.howtographql.com/vue-apollo/8-subscriptions/

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

初めてのtailwindcss (Vue.js + PurgeCSS)

この記事は

CSS Advent Calendar 2019 21日目の記事です。
気になってたtailwindcssを、Vue.jsでやってみましたという記事です。
リポジトリはこちら→ https://github.com/hisako135/tailwindcss_vue_demo
スクリーンショット 2019-12-22 3.10.21.png

やったことは以下のような感じです。

tailwindcssってなんぞ

"Bootstrapをゴリゴリいじってカスタマイズしても良いのだが、そこに時間かけるんならCSSフレームワーク使う利点なくない...?:thinking: " などと思ったことがある人はいませんか...?
グリッドシステムとForm周りさえあればよくて、他の必要なコンポーネントは自分で作った方が楽だよな、とかゴニョゴニョ...(小声)

そこでtailwindcssです!

Most CSS frameworks do too much.
(ほとんどのCSSフレームワークは多くのことをやり過ぎている)
https://tailwindcss.com/#what-is-tailwind

"あらゆる種類のコンポーネントが用意されているCSSフレームワークって初期段階に素早く作るのには確かにとっても便利。でもカスタマイズしようとするとしんどいよね。事前に設計されたコンポーネントの代わりに、低レベルなUtilityクラスを提供することによって、HTMLを離れることなくデザインをカスタマイズできるよ。"
というのがtailwindcssです。

ちなみにCSSでのUtilityクラスとは、.font-small {font-size: 10px;}.text-left {text-align: left;} のように、単一のプロパティを定義した汎用的なクラスを指します。Helperクラスとも呼ばれたり。
FLOCSSではUtilityレイヤーに、FLOCSSのベースとなっているMCSSではCosmeticsレイヤーあたりに記載されるやつです。

tailwindcssのUtility-firstの考え方についてはこちらの翻訳記事に詳細書いてありますので是非!
:book: CSS Utility Classes and "Separation of Concerns" 翻訳

セットアップ手順

何はともあれVue.jsでのtailwindcssのセットアップをやっていきましょう。
まずはvue-cliでVue.jsプロジェクトを作成します。

以降、tailwindcss/setup-examples を参考に進めます。

  • tailwindcssをインストール
npm install tailwindcss
  • postcss.config.js ファイルを作成しプラグインとして設定
postcss.config.js
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');

module.exports = {
  plugins: [
    tailwindcss,
    autoprefixer,
  ]
}
  • src/assets/tailwind.css というtailwindのスタイル用のCSSファイルを作成します
src/assets/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
  • main.jstailwind.css をimportします
src/main.js
import Vue from 'vue'
import App from './App.vue'
import '@/assets/tailwind.css' //これ

Vue.config.productionTip = false

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

セットアップは以上です! :tada:

今回はNavigationやCardの例を利用してサクッと画面を作っています。
あんまりtailwindcssで作り込まれてると <transition>コンポーネント使いづらいな...と、うっすら思いましたとさ(結果、使ってない)。

src/components/Header.vue
<template>
  <nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6">
    <div class="flex items-center flex-shrink-0 text-white mr-6">
      <svg class="fill-current h-8 w-8 mr-2" width="54" height="54" viewBox="0 0 54 54" xmlns="http://www.w3.org/2000/svg"><path d="M13.5 22.1c1.8-7.2 6.3-10.8 13.5-10.8 10.8 0 12.15 8.1 17.55 9.45 3.6.9 6.75-.45 9.45-4.05-1.8 7.2-6.3 10.8-13.5 10.8-10.8 0-12.15-8.1-17.55-9.45-3.6-.9-6.75.45-9.45 4.05zM0 38.3c1.8-7.2 6.3-10.8 13.5-10.8 10.8 0 12.15 8.1 17.55 9.45 3.6.9 6.75-.45 9.45-4.05-1.8 7.2-6.3 10.8-13.5 10.8-10.8 0-12.15-8.1-17.55-9.45-3.6-.9-6.75.45-9.45 4.05z"/></svg>
      <span class="font-semibold text-xl tracking-tight">Tailwind CSS</span>
    </div>
    <div @click='hideMenu=!hideMenu' class="block lg:hidden">
      <button class="flex items-center px-3 py-2 border rounded text-teal-200 border-teal-400 hover:text-white hover:border-white">
        <svg class="fill-current h-3 w-3" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Menu</title><path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/></svg>
      </button>
    </div>
    <div :class='{"hidden md:hidden sm:hidden":hideMenu}' class="w-full block flex-grow lg:flex lg:items-center lg:w-auto">
      <div class="text-sm lg:flex-grow">
        <a href="#responsive-header" class="block mt-4 lg:inline-block lg:mt-0 text-teal-200 hover:text-white mr-4">
          Docs
        </a>
        <a href="#responsive-header" class="block mt-4 lg:inline-block lg:mt-0 text-teal-200 hover:text-white mr-4">
          Examples
        </a>
        <a href="#responsive-header" class="block mt-4 lg:inline-block lg:mt-0 text-teal-200 hover:text-white">
          Blog
        </a>
      </div>
      <div>
        <a href="#" class="inline-block text-sm px-4 py-2 leading-none border rounded text-white border-white hover:border-transparent hover:text-teal-500 hover:bg-white mt-4 lg:mt-0">Download</a>
      </div>
    </div>
  </nav>
</template>

<script>
export default {
  data() {
    return {
      hideMenu: true
    }
  }
}
</script>

ざっくり紹介tailwindcss

さて、tailwindcssの特徴を極一部ですがご紹介します!
めちゃくちゃ詳しく書いて下さっている記事はこちらですので是非!
:book: TailwindCSS入門 ~ Utility First + デザインシステムの構築 ~i

:(コロン) でブレークポイントや擬似クラスの指定ができる

lg:inline-block hover:text-white でブレークポイントや擬似クラスへの指定をしています。1つのスタイルを使い回せるの便利だな〜。

src/components/Header.vue
<a href="#responsive-header" class="block mt-4 lg:inline-block lg:mt-0 text-teal-200 hover:text-white">Blog</a>

@apply

組み合わせて自由に見た目を作れるのが利点とはいえ、何度も同じクラスを並べるのは苦痛ですよね。@apply で、utilityの組み合わせをカスタムコンポーネントのクラスとしてまとめることができます。

src/assets/tailwind.css
@tailwind base;
@tailwind components;
/* Extracting Components */
.btn-teal {
  @apply bg-teal-500 text-white py-2 px-4 rounded w-full;
}
.btn-teal:hover {
  @apply bg-teal-700 transition-colors transition-250 transition-linear;
}
@tailwind utilities;

カスタムコンポーネントのクラスは、utilitiesの前に読み込まないとダメだそうです。

カスタマイズが簡単

tailwindcssで定義されているスタイルは、 tailwind.config.js でカスタマイズできます。

npx tailwind init //tailwind.config.jsファイルを作成するコマンド
tailwind.config.js
module.exports = {
  theme: {
    extend: {}
  },
  variants: {},
  plugins: []
}

作成された tailwind.config.js にカスタマイズしたいプロパティや追加したいプロパティを記載していきます。
tailwindcssでは現状 transition のようなアニメーションに関するプロパティが定義されていません。
そういうものの追加にも便利そうですね。拡張が簡単なのは嬉しい :relieved:
(今回はtransitionのutilityクラスを定義するのにtailwindcss-transitionsを使用しました。)

PurgeCSSで使ってないCSSを削除

さて、なんだかスマートに感じるtailwindcssですが、ファイル容量は実は結構重いです。
スクリーンショット 2019-12-22 2.08.01.png

PurgeCSSで、使ってないCSSを削除していきましょう。

  • PurgeCSSをインストール
npm install @fullhuman/postcss-purgecss --save-dev
postcss.config.js
const autoprefixer = require('autoprefixer');
const tailwindcss = require('tailwindcss');
const purgecss = require('@fullhuman/postcss-purgecss')({
  // テンプレートファイルへのパス。今回は'./src/**/*.vue'
  content: [
    './src/**/*.vue'
  ],
  // https://medium.com/@kyis/vue-tailwind-purgecss-the-right-way-c70d04461475
  defaultExtractor: (content) => {
    const contentWithoutStyleBlocks = content.replace(/<style[^]+?<\/style>/gi, '')
    return contentWithoutStyleBlocks.match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || []
  },
  whitelistPatterns: [ /-(leave|enter|appear)(|-(to|from|active))$/, /^(?!cursor-move).+-move$/, /^router-link(|-exact)-active$/ ],
})

module.exports = {
  plugins: [
    autoprefixer,
    tailwindcss,
    // 開発中はビルド時間がかかってしまうので、productionの時のみ実行
    ...process.env.NODE_ENV === 'production'
    ? [purgecss]
    : []
  ]
}

Before/After

npm build して、 dist/css/配下のCSS容量を見て見ます。

Before

スクリーンショット 2019-12-21 22.07.39.png

After

スクリーンショット 2019-12-21 22.18.58.png

644KB減!!
すごいぞ!PurgeCSS!! :tada:

今回はサンプルのためスタイリング自体が少ないので、実際のプロジェクトではもっと大きくなるんでしょうけれども、いやぁしかし気持ち良いですね :relieved:

まとめ

以上、初めてのtailwindcss (Vue.js + PurgeCSS)でした!
個人的にはめちゃくちゃ好きな思想のフレームワークです。
シンプルで良い。自由 is フリーダム。必要なものは自分で作る :relieved:

緻密かつ堅牢なCSSフレームワークに疲弊している方、ぜひトライしてみては如何でしょうか?

参考にさせて頂いたものまとめ

tailwindcssなのかTailwindcssなのか、はたまたTailwindCSSなのか、正しい表記が未だに分かりません :relieved:

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

Vue-CLI 4を使用したJavaScript開発環境構築(プロトタイプ版とプロジェクト版)

はじめに

この記事は JavaScript Advent Calendar 2019の22日目の記事になります。

こんばんは、きゅ〜ぶです。
少し前にVue CLI4がリリースされましたね。
Vue CLI4の特徴や3からの変更点などは以下が比較的分かりやすいと思います。
Vue.js CLI 4 Released

ローカル環境でJavaScriptを触りたかったのでせっかくなので新しくなったVue-CLI4で環境構築をしようと思います。
基本的には公式リファレンスに沿ってやっていきます。

↓Vue-CLI4のソースコード
https://github.com/vuejs/vue-cli/releases

2019年12月現在最新バージョンは、v.4.1.1です。

インストール

公式ドキュメントにも書かれているのですが、

Vue CLIインストールには、前提としてNode.jsのバージョン8.9以上(8.11.0以降を推奨)

が必要となります。
Node.jsが入ってない人はNode.jsからインストールしましょう。

以下記事が分かりやすいので参考にしてみてください。
MacにNode.jsをインストール

Vue-CLIインストール

プロトタイプ版

公式リファレンスのコマンドに沿って入力していきます。
vue servevue buildで簡単なプロトタイプが作れます。
ちょっと処理を確認したいとかそういったときに便利です。

$ npm install -g @vue/cli
$ vue --version
@vue/cli 4.1.1
$ npm install -g @vue/cli-service-global
$ touch App.vue
App.vue
<template>                                                                                                                                                                                              
 <h1>Hello!</h1>
</template>
$ vue serve
 INFO  Starting development server...
98% after emitting

 DONE  Compiled successfully in 1653ms                                                                                                                                                              11:18:54


  App running at:
  - Local:   http://localhost:8080/ 
  - Network: http://**.**.***.**:8080/

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

画面にもう表示できました!
スクリーンショット 2019-12-17 11.31.59.png

$ vue build
⠧  Building for production...

 DONE  Compiled successfully in 2432ms                                                                                                                                                              11:42:08

  File                                 Size               Gzipped

  dist/js/chunk-vendors.548bada6.js    65.55 KiB          23.58 KiB
  dist/js/app.e2adb5ac.js              1.78 KiB           0.89 KiB

  Images and other types of assets omitted.

 DONE  Build complete. The dist directory is ready to be deployed.
 INFO  Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html

簡単なJavaScriptを軽く動かしたいならここまでやってあれば問題ないです!
Vue.jsで本格的に開発したいなら以下のようなプロジェクトを作成することもできます。

プロジェクト版

Vue-CLI4でプロジェクト作成

$ vue create hello-world
?  Your connection to the default npm registry seems to be slow.
   Use https://registry.npm.taobao.org for faster installation? No
Vue CLI v4.1.1
? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint) 
  Manually select features 
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)Babel, Linter
? Pick a linter / formatter config: 
❯ ESLint with error prevention only 
  ESLint + Airbnb config 
  ESLint + Standard config 
  ESLint + Prettier 
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?

ざっくりこんな感じで作っていきます。

①使用する機能の選択

BabelESLintが入ってるデフォルトを選択するかマニュアルで必要な機能を選択した物を使うかの選択になります。

○デフォルト

? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint) 
  Manually select features 

○マニュアル

? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

②ESLintプリセットの選択

・ESLint with error prevention only(エラー防止のみ)
・ESLint + Airbnb config(Airbnb設定)
・ESLint + Standard config(標準設定)
・ESLint + Prettier(ESLintとPrettierの併用)

? 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(保存時にLint実行)
・Lint and fix on commit(コミット時にLint実行)

? 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などの設定の配置箇所の選択

・In dedicated config files(専用の設定ファイル内)
・In package.json(package.json内)

? 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)
```terminal
? Save preset as: 

ここで設定しておくと次以降、vue createでプロジェクトを作る際、また新しく設定する必要がなくなり、設定したプリセットをそのまま使用することができます。

以下の画面がターミナルで表示されていれば、無事インストール完了となります?

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

 $ cd hello-world
 $ npm run serve

サーバ起動

 $ cd hello-world
 $ npm run serve
 DONE  Compiled successfully in 1222ms                                  23:48:57


  App running at:
  - Local:   http://localhost:8080/ 
  - Network: http://xxx.xxx.x.xxx:8080/

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

以下画面が表示されれば成功です?

スクリーンショット 2019-12-21 23.51.14.png

では、楽しいJavaScript生活を。。。

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