20200802のvue.jsに関する記事は7件です。

文章から単語を取り出して数えるサービスを作成してみた

概要

「単語を数えるプログラミング」をテーマにお勉強してみました。
単語を数えるということで形態素解析を用いて文章から単語を取り出して単語数を数えてみるサービスを作ることにしました。

お勉強した結果ですが以下のサービスとして公開しておりますので、以下の 作ったモノ からご確認いただければ幸いです。

image.png

使用しているフレームワークとライブラリ

今回はGlitchというNode.jsのアプリを公開するためのサービスを使用して公開しています。

Glitch

https://glitch.com/

フレームワークとライブラリについては以下を使用しております。

フレームワーク

ライブラリ

作ったモノ

公開しているサイトとコードは以下のアドレスでご確認いただけます。

公開先(作ったモノの動作を確認する)

https://morphological-analysis.glitch.me/

コード

https://glitch.com/edit/#!/morphological-analysis

作ったモノの説明

コードについては上記のコードをご確認いただければ幸いです。
以下に特記したい内容を記載します。

バックエンド

image.png

const express = require("express");
var compression = require('compression')
const app = express();
const kuromoji = require("kuromoji");
const analyze = require("negaposi-analyzer-ja");
app.use(compression())
app.use(express.static("public"));
app.set("json spaces", 2);

const builder = kuromoji.builder({
  dicPath: "node_modules/kuromoji/dict"
});

app.get("/", (request, response) => {
  response.sendFile(__dirname + "/views/index.html");
});

app.get("/morphological", (request, response) => {
  builder.build(function(err, tokenizer) {
    if (err) {
      throw err;
    }
    let reqMsg =
      request.query.msg ||
      "形態素解析とは文章を意味を持つ最小の単位に分けるイメージです";
    let tokenizedResult = tokenizer.tokenize(reqMsg)
    let result = {
      send_msg: reqMsg,
      morphological_result: tokenizedResult,
      negaposi_score: analyze(tokenizedResult)
    };

    console.log("tokened:" + result);
    response.json({ result });
  });
});

app.get("/negaposi", (request, response) => {
  builder.build(function(err, tokenizer) {
    if (err) {
      throw err;
    }
    let reqMsg =
      request.query.msg ||
      "形態素解析とは文章を意味を持つ最小の単位に分けるイメージです";

    const score = analyze(tokenizer.tokenize(reqMsg));
    builder = null;
    response.json({
      message: reqMsg,
      negaposi_score: score
    });
  });
});

// listen for requests :)
const listener = app.listen(process.env.PORT, () => {
  console.log("Your app is listening on port " + listener.address().port);
});

ExpressによるAPIサーバーです。文章を単語ずつに区切る形態素解析するためのAPIとして実装されています。ウェブサーバーとしてURLのトップにアクセスされた場合は、views/index.htmlを表示するようにしています。

形態素解析

https://ja.wikipedia.org/wiki/形態素解析

上記のWikipediaから抜粋させていただきます。
辞書と呼ばれる単語の品詞等の情報にもとづき、形態素(Morpheme, おおまかにいえば、言語で意味を持つ最小単位)の列に分割し、それぞれの形態素の品詞等を判別する処理です。

今回の場合はkuromoji.jsという形態素解析のライブラリを使用しています。
kuromoji.builderで辞書を読み込むことで形態素解析の処理を行っています。

なお、形態素解析の品質は辞書に依存することがほとんどで、今回はデフォルトの辞書を使用しているため、流行語などで単語を区切ったりすることはできません。

オマケ:ネガポジ判定

kuromoji.jsで形態素解析された結果を使ってネガポジ判定もできるようなので実装してみました。ネガポジ判定とは、主に人の発言や発想などが、前向き(ポジティブ)か後ろ向き(ネガティブ)かを判定するモノになります。

使用させていただいた negaposi-analyzer-ja では、形態素解析された結果を 単語感情極性対応表を使用してネガポジ判定をしているようです。こちらも単語感情極性対応表に依存することになります。

ネガポジ判定自体はPythonなどの機械学習などの方が向いていると思います。BertWord2Vec などを使えば、自分だけのネガポジなどを作れそうですね。

フロントエンド

image.png

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>morphological-analysis</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
    />
    <link
      href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,700"
      rel="stylesheet"
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/bulma@0.8.2/css/bulma.min.css"
    />
  </head>

  <body>
    <section class="hero is-info is-fullheight" id="app">
      <div class="hero-body">
        <div class="container has-text-centered">
          <div class="column">
            <div class="box">
              <div class="field is-grouped">
                <p class="control is-expanded">
                  <input
                    class="input"
                    v-model="sendMsg"
                    type="text"
                    placeholder="形態素解析する文章を入力してください"
                  />
                </p>
                <p class="control">
                  <a class="button is-info" @click="morphoExec">
                    実行する
                  </a>
                </p>
              </div>
            </div>
            <div class="subtitle" v-if="isLoading">
              <progress
                class="progress is-large is-warning"
                max="100"
              ></progress>
            </div>
            <h2 class="subtitle has-text-left" v-if="sendMsg">
              {{sendMsg}}
            </h2>

            <h2 v-if="dataset.length > 0" class="bar-graph">
              <chartjs-horizontal-bar
                :labels="labels"
                :data="dataset"
                :bind="true"
                :datalabel="dataLabel"
                :option="chartOption"
                :height="chartHeight"
              ></chartjs-horizontal-bar>
            </h2>
            <h1 class="subtitle" v-if="negaposiScore">
              ネガポジスコア: {{negaposiScore}}
            </h1>

            <h3 class="subtitle has-text-left" v-if="meishiCountList">
              <p class="subtitle has-text-left">単語一覧</p>
              <li v-for="(item, key) in meishiCountList" class="has-text-left">
                {{ key }} : {{ item }}
              </li>
            </h3>
          </div>
        </div>
      </div>
    </section>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://unpkg.com/vue-chartjs@2.6.0/dist/vue-chartjs.full.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.js"></script>
    <script src="https://unpkg.com/hchs-vue-charts@1.2.8"></script>
    <script>
      "use strict";
      Vue.use(VueCharts);
      new Vue({
        el: "#app",
        data: {
          sendMsg: null,
          meishiCountList: null,
          negaposiScore: null,
          isLoading: false,
          labels: [],
          dataset: [],
          dataLabel: "名詞 カウント",
          chartOption: {
            legend: {
              display: false
            },
            responsive: true,
            maintainAspectRatio: false,
            scales: {
              xAxes: [
                {
                  ticks: {
                    max: 5,
                    min: 0,
                    stepSize: 1
                  }
                }
              ]
            }
          },
          chartHeight: 500
        },
        methods: {
          morphoExec() {
            this.negaposiScore = null;
            this.labels = [];
            this.dataset = [];
            this.sendMsg =
              this.sendMsg ||
              "何も文字が入力されなかったのでサンプルの文章で形態素解析を実行します。何か文字を入力することで単語をカウントします。";
            const sendMsg = this.sendMsg;
            this.isLoading = true;
            const vueThis = this;
            axios
              .get("/morphological?msg=" + sendMsg)
              .then(function(response) {
                const morphoResult = response.data.result.morphological_result;
                let meishiList = [];
                morphoResult.filter(function(item, index) {
                  if (item.pos == "名詞") {
                    meishiList.push(item.surface_form);
                    return true;
                  }
                });
                let meishiCount = {};

                for (let i = 0; i < meishiList.length; i++) {
                  let key = meishiList[i];
                  meishiCount[key] = meishiCount[key]
                    ? meishiCount[key] + 1
                    : 1;
                }

                vueThis.labels = Object.keys(meishiCount);
                vueThis.dataset = Object.values(meishiCount);
                vueThis.negaposiScore = response.data.result.negaposi_score;
                vueThis.meishiCountList = meishiCount;
                vueThis.chartOption.scales.xAxes[0].ticks.max = Math.max.apply(
                  null,
                  Object.values(meishiCount)
                );

                vueThis.chartHeight = 30 * meishiList.length;
                console.log(
                  "最大値:" + Math.max.apply(null, Object.values(meishiCount))
                );
                console.log(vueThis.meishiCountList);
              })
              .finally(() => (this.isLoading = false));
          }
        }
      });
    </script>
    <style>
      .bar-graph {
        margin-left: auto;
        margin-right: auto;
        padding-top: 10%;
        padding-bottom: 10%;
        background-color: #f5f5f5;
      }
    </style>
  </body>
</html>

Vue.js

Expressでもテンプレートエンジンとして使用できるのですが、今回のフロントエンドのフレームワークはVue.jsを使用しています。単純にVue.jsが書きやすかったので選択(いろいろとダメですが・・・)しました。

バックエンドのAPIへの接続から、形態素解析された後のデータの処理をVue.jsmethodsで処理しています。返ってくるデータを整理するために、いろいろと試行錯誤していますが、もう少しイイカンジに書けそうな気がします。

Bulma

CSSや見た目のデザイン面ではBulmaを使用しています。今回は以下のテンプレートから参考させていただきました。

https://bulmatemplates.github.io/bulma-templates/templates/landing.html

Charts.js

以下のサイトを参考にグラフを追加しました。

https://vuejsexamples.net/vue-bar-chart/

当初はなかなかChart.jsのプロパティ(設定)の理解ができませんでした。
しかしながら元のライブラリのコードを読んだり、ドキュメントをちゃんと読むことで少しずつ理解することできました。具体的にやったことはOptionを設定する(上書きする)ことでグラフの設定をカスタマイズすることができました。

課題

形態素解析するときに辞書を読み込むので極端にメモリを消費しています。
image.png

今後辞書を良いものに変えると辞書の容量も増えることになり、もっとメモリを消費することになります。もし品質を向上させる場合は、Glitchをアップグレードするか他のサービスを使うことを検討する必要がありそうです。

ちなみに当初は Netlify Functions を使おうとしていて、辞書ファイルの読み込みが上手くいきませんでした。これはファイルの読み込み(fs.readFile)に工夫がいるとのことで、今回はGlitchを選択しています。

コードも改善余地があり形態素解析した結果の処理などはもっとイイカンジにできると思います。そもそもExpressに統一した方がよいかもしれません。ただ、今回はお勉強が目的でどんどん動かしていくモノを作っていけたので一旦は良い結果が得られたと思います。

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

『パクってOK』transformプロパティで圧倒的に動くCSSアニメーション28選

スクリーンショット 2020-08-02 22.15.41.png

こちらの記事に記載のデザイン・コードは全てオリジナルなので自由に使っていただいて大丈夫です(筆者が作成したため)

Web制作で使える。コピペOK。HTML, CSSだけ

まずは動きを確認してみてください(これがコピペOKなのかと驚くはず)

コードの説明や実際の動きも詳しくまとめてみたので自由にお使いください。


【rotate×scale】画像が回転・拡大するCSSアニメーション3選(解説あり)

動きは下の画像のような感じになります

1. rotate×box-shadowでリアルすぎる画像回転アニメーション

images-rotate-animation-3picks1 (1).png

2. rotate×scale(拡大)で失敗しない画像回転アニメーション

images-rotate-animation-3picks2 (1).png

3. rotate×scale(拡大)×filterで想像を上回る画像回転アニメーション

images-rotate-animation-3picks3 (1).png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.13.11.png

【transform rotate(360deg)×flexbox】画像が回転+伸縮するCSSアニメーション3選

動きは下の画像のような感じになります

1. 【縦回転】transform rotateX(360deg)×flexbox画像アニメーション

transform-rotate-flexbox-animation1-228x300 (1).png

2. 【横回転】transform rotateY(360deg)×flexbox画像アニメーション

transform-rotate-flexbox-animation2-265x300 (1).png

3. 【平面回転】transform rotateZ(360deg)×flexbox画像アニメーション

transform-rotate-flexbox-animation3-275x300 (1).png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.16.43.png

【真似したくなる】transform:rotate×scaleでCSS画像アニメーション3選

動きは下の画像のような感じになります

1. transform rotateX×scaleで【縦回転】CSS画像アニメーション

スクリーンショット 2020-07-27 23.27.44.png

2. transform rotateY×scaleで【横回転】CSS画像アニメーション

スクリーンショット 2020-07-27 23.27.51.png

3. transform rotateY/Z×scaleで【3D回転】画像アニメーション

スクリーンショット 2020-07-27 23.27.57.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.27.32.png

transform:translateで絵画デザインのスライドショーを実現!CSSスライダーアニメーション3選

動きは下の画像のような感じになります

1. クリックで右方向からスライドしてくるアニメーション

スクリーンショット 2020-07-27 23.31.41.png

2. クリックで上からスライドしてくるアニメーション

スクリーンショット 2020-07-27 23.31.46.png

3. クリックで斜め上からスライドしてくるアニメーション

スクリーンショット 2020-07-27 23.31.52.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.29.23.png

【transform:rotate×scale】回転×拡大するCSS画像アニメーション3選

動きは下の画像のような感じになります

1. 【縦回転】transform rotateXで画像アニメーション

スクリーンショット 2020-07-27 23.34.00.png

2. 【横回転】transform rotateYで画像アニメーション

スクリーンショット 2020-07-27 23.34.07.png

3. 【3D回転】transform rotateYZで画像アニメーション

スクリーンショット 2020-07-27 23.34.16.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.33.36.png

filterとtransformでリアルな絵画デザインのCSSスライダーアニメーション3選

動きは下の画像のような感じになります

1. クリックで横幅が伸び縮みしながらスライドするアニメーション

スクリーンショット 2020-07-27 23.36.33.png

2. クリックで縦幅が伸び縮みしながらスライドするアニメーション

スクリーンショット 2020-07-27 23.36.38.png

3. クリックで思いっきり伸び縮みしながらスライドするアニメーション

スクリーンショット 2020-07-27 23.36.43.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.36.04.png

filterとtransformで超動くCSSスライダーアニメーション4選【Web制作者必見】

動きは下の画像のような感じになります

1. 縦回転するシンプルなスライダーアニメーション

スクリーンショット 2020-07-27 23.39.00.png

2. 滑らかに横回転するスライダーアニメーション

スクリーンショット 2020-07-27 23.39.05.png

3. 平面回転するスタイリッシュなスライダーアニメーション

スクリーンショット 2020-07-27 23.39.11.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.38.38.png

【transform×回転『rotate』】CSS画像アニメーション3選

動きは下の画像のような感じになります

1. 【縦回転】transform rotateXで画像アニメーション

スクリーンショット 2020-07-27 23.41.19.png

2. 【横回転】transform rotateYで画像アニメーション

スクリーンショット 2020-07-27 23.41.25.png

3. 【3D回転】transform rotateYZで画像アニメーション

スクリーンショット 2020-07-27 23.41.31.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.40.43.png

【transform rotate×画像背景】CSS回転アニメーション3選

動きは下の画像のような感じになります

1. 【縦回転】transform rotateX画像背景アニメーション

スクリーンショット 2020-07-27 23.43.31.png

2. 【横回転】transform rotateY画像背景アニメーション

スクリーンショット 2020-07-27 23.43.37.png

3. 【3D回転】transform rotateYZ画像背景アニメーション

スクリーンショット 2020-07-27 23.43.42.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.42.55.png

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

『パクってOK』transformプロパティで圧倒的に動くCSSアニメーション28選【Vueでも使える】

スクリーンショット 2020-08-02 22.15.41.png

こちらの記事に記載のデザイン・コードは全てオリジナルなので自由に使っていただいて大丈夫です(筆者が作成したため)

Web制作で使える。コピペOK。HTML, CSSだけ

まずは動きを確認してみてください(これがコピペOKなのかと驚くはず)

コードの説明や実際の動きも詳しくまとめてみたので自由にお使いください。


【rotate×scale】画像が回転・拡大するCSSアニメーション3選(解説あり)

動きは下の画像のような感じになります

1. rotate×box-shadowでリアルすぎる画像回転アニメーション

images-rotate-animation-3picks1 (1).png

2. rotate×scale(拡大)で失敗しない画像回転アニメーション

images-rotate-animation-3picks2 (1).png

3. rotate×scale(拡大)×filterで想像を上回る画像回転アニメーション

images-rotate-animation-3picks3 (1).png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.13.11.png

【transform rotate(360deg)×flexbox】画像が回転+伸縮するCSSアニメーション3選

動きは下の画像のような感じになります

1. 【縦回転】transform rotateX(360deg)×flexbox画像アニメーション

transform-rotate-flexbox-animation1-228x300 (1).png

2. 【横回転】transform rotateY(360deg)×flexbox画像アニメーション

transform-rotate-flexbox-animation2-265x300 (1).png

3. 【平面回転】transform rotateZ(360deg)×flexbox画像アニメーション

transform-rotate-flexbox-animation3-275x300 (1).png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.16.43.png

【真似したくなる】transform:rotate×scaleでCSS画像アニメーション3選

動きは下の画像のような感じになります

1. transform rotateX×scaleで【縦回転】CSS画像アニメーション

スクリーンショット 2020-07-27 23.27.44.png

2. transform rotateY×scaleで【横回転】CSS画像アニメーション

スクリーンショット 2020-07-27 23.27.51.png

3. transform rotateY/Z×scaleで【3D回転】画像アニメーション

スクリーンショット 2020-07-27 23.27.57.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.27.32.png

transform:translateで絵画デザインのスライドショーを実現!CSSスライダーアニメーション3選

動きは下の画像のような感じになります

1. クリックで右方向からスライドしてくるアニメーション

スクリーンショット 2020-07-27 23.31.41.png

2. クリックで上からスライドしてくるアニメーション

スクリーンショット 2020-07-27 23.31.46.png

3. クリックで斜め上からスライドしてくるアニメーション

スクリーンショット 2020-07-27 23.31.52.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.29.23.png

【transform:rotate×scale】回転×拡大するCSS画像アニメーション3選

動きは下の画像のような感じになります

1. 【縦回転】transform rotateXで画像アニメーション

スクリーンショット 2020-07-27 23.34.00.png

2. 【横回転】transform rotateYで画像アニメーション

スクリーンショット 2020-07-27 23.34.07.png

3. 【3D回転】transform rotateYZで画像アニメーション

スクリーンショット 2020-07-27 23.34.16.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.33.36.png

filterとtransformでリアルな絵画デザインのCSSスライダーアニメーション3選

動きは下の画像のような感じになります

1. クリックで横幅が伸び縮みしながらスライドするアニメーション

スクリーンショット 2020-07-27 23.36.33.png

2. クリックで縦幅が伸び縮みしながらスライドするアニメーション

スクリーンショット 2020-07-27 23.36.38.png

3. クリックで思いっきり伸び縮みしながらスライドするアニメーション

スクリーンショット 2020-07-27 23.36.43.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.36.04.png

filterとtransformで超動くCSSスライダーアニメーション4選【Web制作者必見】

動きは下の画像のような感じになります

1. 縦回転するシンプルなスライダーアニメーション

スクリーンショット 2020-07-27 23.39.00.png

2. 滑らかに横回転するスライダーアニメーション

スクリーンショット 2020-07-27 23.39.05.png

3. 平面回転するスタイリッシュなスライダーアニメーション

スクリーンショット 2020-07-27 23.39.11.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.38.38.png

【transform×回転『rotate』】CSS画像アニメーション3選

動きは下の画像のような感じになります

1. 【縦回転】transform rotateXで画像アニメーション

スクリーンショット 2020-07-27 23.41.19.png

2. 【横回転】transform rotateYで画像アニメーション

スクリーンショット 2020-07-27 23.41.25.png

3. 【3D回転】transform rotateYZで画像アニメーション

スクリーンショット 2020-07-27 23.41.31.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.40.43.png

【transform rotate×画像背景】CSS回転アニメーション3選

動きは下の画像のような感じになります

1. 【縦回転】transform rotateX画像背景アニメーション

スクリーンショット 2020-07-27 23.43.31.png

2. 【横回転】transform rotateY画像背景アニメーション

スクリーンショット 2020-07-27 23.43.37.png

3. 【3D回転】transform rotateYZ画像背景アニメーション

スクリーンショット 2020-07-27 23.43.42.png

:point_down:コードを確認する

スクリーンショット 2020-07-27 23.42.55.png

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

【Vue.js】CDN版でも単一ファイルコンポーネント(.vue)を使いたい!

こんにちは。

CDN版ライブラリの使用については皆さん賛否両論あると思いますが、私は容認派です。
手軽に使える便利さは正義!

そんなわけで、ビルド不要なCDN版Vue.jsでも単一ファイルコンポーネントシステム(.vueファイル)を実現したくね!?と思ったので色々と試してみたお話です。

そもそもそんなことできるの?

できました。
しかし一筋縄では行かず、クライアントサイドで多少のゴリ押しが必要となります。
ゴリ押すためには、以下のキーポイントを押さえる必要があります。

非同期通信

いわゆるAjaxと呼ばれるやつです。
XMLHTTPRequestfetchなど色々ありますが、ここではaxiosという便利な非同期通信ライブラリを使用します。

非同期コンポーネントとミックスイン

Vue.jsは、非同期でコンポーネントを追加できる、非常に柔軟な機構を持っています。
これらを使用し、非同期通信で取得した.vueファイルをパースし、既存のコンポーネントへ追加します。

VueRouter

Vue.jsで仮想ルーティングを実装するためのプラグインです。
実はVueRouterもCDN版が存在し、ビルド不要で仮想ルーティングを実装できます。
(更に言うとステート管理プラグインのVuexもCDN版が存在します!)

VueRouterと単一ファイルコンポーネントを組み合わせ、ビルドシステムさながらのページを構築することができます。

サンプルコード

  • /index.html: メインページ
  • /component.vue: Vueコンポーネントファイル
  • /paths.json: 仮想パスとコンポーネントファイルの物理パスを列挙したマッピングデータ

これら3個のファイルのみで、Vue.jsアプリケーションを構築してみます。
UIはVuetifyを使用しました。

なぜマッピングデータが必要かというと、静的配信においてクライアントはサーバーがどのファイルを配信しているかを知る術がないからです。
マッピングデータを用意してやることにより、クライアントがコンポーネントファイルの場所を解決できるようになります。

最初のうちはマッピングデータをメインページのJavaScript内で記述しても問題ないと思いますが、拡張性を考えると外へ書き出しておいたほうが良いかなと思います。

index.html

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="height=device-height, width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">

        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vuetify@latest/dist/vuetify.min.css">
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">

        <script src="https://cdn.jsdelivr.net/npm/vue@latest/dist/vue.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/vue-router@latest/dist/vue-router.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/vuetify@latest/dist/vuetify.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/axios@latest/dist/axios.min.js"></script>

        <title>Vue</title>
    </head>

    <body>
        <div id="vue">
            <v-app>
                <v-app-bar app dark height="52" color="secondary">
                    <v-toolbar-title>Vue</v-toolbar-title>
                </v-app-bar>

                <v-main>
                    <router-view></router-view>
                </v-main>
            </v-app>
        </div>
    </body>

    <script>
        const loadComponent = axios.create({
            method: "get",
            baseURL: ".",
            responseType: "text",
            transformResponse(data){
                const doc = new DOMParser().parseFromString(data, "text/html");

                const template = doc.head.getElementsByTagName("template")[0]?.innerHTML;
                const script = doc.head.getElementsByTagName("script")[0]?.innerText
                const style = doc.head.getElementsByTagName("style")[0];

                if(style){
                    document.head.appendChild(style);
                }

                return {
                    template(){
                        return template ? template : "";
                    },
                    script(){
                        return script ? new Function(script.trim().replace(/^(export\s+default|module\.exports\s*?=)\s*?/i, "return"))() : {};
                    }
                };
            }
        });

        const loadMap = axios.create({
            method: "get",
            baseURL: "./components",
            responseType: "json",
            transformResponse({parents, paths}){
                return Object.entries(paths).map(([virtual, physical]) => [`${parents.virtual}${virtual}`, `${parents.physical}${physical}`]);
            }
        });

        const vue = (async()=>{
            const maps = [
                "/paths.json"
            ];

            const paths = [];
            for await(const {data} of maps.map(map => loadMap(map))){
                paths.push(...data);
            }

            return new Vue({
                el: "#vue",
                vuetify: new Vuetify(),
                router: new VueRouter({
                    routes: paths.map(([virtual, physical])=>{
                        return {
                            path: virtual,
                            async component(res){
                                const {data} = await loadComponent(`${physical}.vue`);
                                res({
                                    template: data.template(),
                                    mixins: [data.script()]
                                });
                            }
                        };
                    })
                })
            });
        })();
    </script>
</html>

コンポーネントローダー

最初にaxios.create()で、コンポーネントファイルをロードするためのaxiosインスタンスを作ります。
以後、このインスタンスをコンポーネントローダーと呼称します。

レスポンスデータはtransformResponse()でVueコンポーネントとして使えるよう加工します。

加工する内容は、まずVueコンポーネントファイルをテキスト形式で取得し、DOMツリーへと変換します。
これは、Vueコンポーネント内の<template>/<script>/<style>を分離するためで、正規表現マッチによるタグ検出も不可能ではないですが、より簡単で正確なDOMParserを使用します。

なお、VueコンポーネントはHTMLとして解析されるため<template>/<script>/<style>は自動的にDOMツリーの<head>内へ移動されます。

テンプレート文字列<template>はそのままinnerHTMLで中身を返します。
問題はJavaScript文字列の<script>で、これは少しテクニカルです。

JavaScript文字列の評価にはしばしばeval()が用いられますが、このeval()は速度面でも安全面でも良くないという話は周知の事実かと思います。

そこで、同じくJavaScript文字列から評価を行える、関数コンストラクタnew Function()を使います。
関数を作成する場合、通常であればfunction xxx(){}と書きますが、関数コンストラクタを用いることでも作成できます。

関数コンストラクタは、中身となるJavaScript文字列を引数として受け取り、それを評価し関数オブジェクトとして返します。
eval()とは違い、評価結果が関数スコープ内へ封じ込められるため安全という訳です。

関数コンストラクタは関数を返すためnew Function()()としてやれば、評価した関数をそのまま即時実行することもできます。

なお、評価の直前でJavaScript文字列の先頭export defaultまたはmodule.exportsreturnへと書き換えています。
これは、コンポーネントオブジェクトをexportへ出力するのではなく、関数の返り値とするためです。

この時の動作を表すと、以下のようになります。

// "return" へ書き換えた後に関数コンストラクタの引数へ代入
// "export default{data(){},...}" => "return{data(){},...}"
return new Function("return{data(){},...}")();
// => {data(){},...}

// 上記の関数コンストラクタの評価結果(等価)
return (function(){
    return {
        data(){
        },
        ...
    };
})();
// => {data(){},...}

この、評価後に返されるコンポーネントオブジェクトを後述のミックスインとして追加している訳です。

もし書き換えなければ、コンポーネントオブジェクトは正体不明のexportへ出力されることになってしまいます。

なお、テンプレートやスクリプトの処理結果を、オブジェクトプロパティではなく関数として返しているのは、参照渡しではなく値渡しするためです。

<style>は問答無用でドキュメント内へ挿入しています。
(将来的に乱数もしくはハッシュ値を取ってscoped対応もできれば良いなと考えています)

マップローダー

再びaxios.create()を使い、マッピングデータをロードするためのaxiosインスタンスを作成します。
以後、このインスタンスをマップローダーと呼称します。

このレスポンスデータもtransformResponse()でマッピングデータとして使えるよう加工します。

マッピングデータは 後述 のようなデータ構造となっています。
parentsプロパティで仮想パスと物理パスそれぞれに親パスを宣言できるため、親パスとpathsオブジェクト内の子パスを結合してあげます。

返り値はArray<["仮想パス", "物理パス"]>のような連想配列とすることで、その後のイテレーション処理を簡単にしています。

非同期コンポーネント

まず、VueRouterのパスが非同期で取得されるマッピングデータへ依存しているため、Vueインスタンス全体をasync即時関数でラップします。

なお、複雑なパスの構築へ備え、複数のマッピングデータファイルを結合できるような作りになっています。
マップローダーでマッピングデータを取得し終えたら、Vueインスタンスの構築へ移ります。

VueRouterのpathプロパティへ仮想パスを、コンポーネントローダーへ物理パスを渡すことで、それぞれマッピングの整合をとっています。

Vue(VueRouter)のcomponentプロパティは、コンポーネント内容を直接記載する場合はオブジェクトとしますが、コンポーネント内容が非同期な場合は、引数resolveで解決されるPromiseを返すcomponent()関数となります。

templateプロパティには、コンポーネントローダーの返り値template()でテンプレート文字列を渡します。
mixinsプロパティには、コンポーネントローダーの返り値script()でコンポーネントオブジェクトを渡します。

component.vue

component.vue
<template>
<div>
    <v-btn>foo</v-btn>
    <v-btn>{{test}}</v-btn>
</div>
</template>

<script>
export default {
    data(){
        return {
            test: "aaa"
        };
    }
};
</script>

普通のコンポーネントファイルと同様に記述していきます。
ただ実際にロードされるときは、上述の通り先頭がexport defaultまたはmodule.exportsからreturnへ自動で書き換えられます。

paths.json

paths.json
{
    "parents": {
        "virtual": "",
        "physical": ""
    },
    "paths": {
        "/": "/component"
    }
}

pathsプロパティで"仮想パス":"物理パス"のようにマッピングしていきます。
なおparentsプロパティで一括して親パスを設定できるため、実際のパスはparents+pathsとなります。

仮想パスについては:idのようなVueRouterの動的ルートマッチング機能も使えます。
物理パスに拡張子を含めるか含めないかは好みですが、今回は実質.vue以外には成り得ないため、ここでは記述しないほうがミスは減るかなと思います。

まとめ

だいぶパワープレイになりましたが、無事CDN版Vue.jsでも単一コンポーネントシステムを実現することができました。

と言うかこれもう完全に人力ビルドシステムだなおい

もっと良い方法があるよ!とか参考になった!とかありましたらコメント頂ければ幸いです!

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

なんでVue.jsではコンポーネント内のデータは関数で書くのか?

こんにちは、Yuiです。

私はVue.jsを使って業務委託で開発を行っているのですが、最初ナニモワカランという状態から参入させてもらったため、Vue.jsを雰囲気で書いてました。(今もだいぶ雰囲気で書いちゃってるので、徐々に精進していきます。)

コンポーネント内ではデータは関数で定義する。
Vueを書く人からしたら当たり前かもしれませんが、そのへんも理屈を全くわからないままそういうもんだとおもって開発してました。
ようやく最近そのへんの理屈がわかるようになってきたので、今回はそのことについて書きます。

参考:超Vue.js 完全パック - もう他の教材は買わなくてOK! (Vue Router, Vuex含む)

そもそもコンポーネントとは?

コンポーネントとは、何回も使える要素のことを指します。

コーディングをしていると、再利用したい要素というのが出てくるかと思うのですが、その度に新しくコーディングするのは大変です。

例えば、下記のコードを見てください。

<div id="app"></div>

new Vue({
    el:'#app',
    data: {
        hello: 'hello'
   },
   template: '<p>{{hello}}</p>'
})

この場合、出力はhelloと出力されますが、もしこれを複数回表示させたい場合、どうなるでしょうか?
つまり、'hello'を何回も表示させたい場合です。

通常で考えれば、

<div id="app"></div>
<div id="app"></div>
<div id="app"></div>

new Vue({
    el:'#app',
    data: {
        hello: 'hello'
   },
   template: '<p>{{hello}}</p>'
})

このように、<div id="app"></div>の部分を何回も書けばいいと思うかもしれません。
ただ、Vue.jsでは実はこれでは一回しか表示がされないのです。

なぜかというと、Vueインスタンスでは同じものは表示しないという性質があるからです。
そこでコンポーネントの出番です。

というわけで、下記のように書き直します。

<div id="app">
  <component></component>
  <component></component>
  <component></component>
</div>

Vue.component('component', {
    data: function() {
        return {
           hello:'hello'
        }
     },
    template: '<p>{{hello}}</p>'
})

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

こうすることで、出力結果は

hello
hello
hello

となります。

コンポーネントでは、最初に第一引数でコンポーネント名を定義する必要があります。
今回は簡単にcomponentとしたので、そのコンポーネントを表示するときは<component></component>と書きました。

これを例えば

Vue.component('hello_component', {
    data: function() {
        return {
           hello:'hello'
        }
     },
    template: '<p>{{hello}}</p>'
})

このようにした場合は、出力する際に<hello_component></hello_component>と書くことができます。

コンポーネント内のデータを無理やりオブジェクト形式で書いてみる

それでは本題の、なぜコンポーネント内のdata部分は関数で定義するのかに関して。

そもそも、オブジェクト形式では書けないのでしょうか。
書いてみましょう。

Vue.component('component', {
    data: {
        hello:'hello'
    },
    template: '<p>{{hello}}</p>'
})

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

出力しても何も表示されません。
vueではコンポーネント内では関数を使わないとエラーになるみたいですね。
ただ、data部分をオブジェクト形式で無理やり書くことは出来るので、書いてみます。

var data = {
    hello: 'hello'
}

Vue.component('component', {
    data: function() {
        return data
     },
    template: '<p>{{hello}}</p>'
})

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

無事出力されました!

何が問題なのか

上記のように、書いた場合、何が問題なのでしょうか?
表示自体は問題なくできているように感じます。

ただ上記の場合だとvar dataで中身となるデータ部分を一定のものに定義してしまっています。
つまり、今後データが変更されたらすべての部分で変更が起こってしまうということです。

具体的なコードを出します。

Vue.component('component', {
    data: function() {
        return data
     },
    template: '<p>{{hello}}<button @click="bye">bye</button></p>',
    methods: {
        bye: function() {
            this.hello = 'bye';
        }
    }
})

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

このような形で、ボタンを作ってアクションを起こした場合、
4dfc8556864bb9f83614124d5b87af8c.gif

こんな感じで、全部のデータ(hello)がbyeに変わってしまうんです。

ただ、できればボタンを押したところだけのデータを変えたいですよね。
というわけで前置きが長くなりましたが、やはりきちんとデータは関数で書いてあげます。

Vue.component('component', {
    data: function() {
        return {
           hello:'hello'
        }
     },
    template: '<p>{{hello}}<button @click="bye">bye</button></p>',
    methods: {
        bye: function() {
            this.hello = 'bye';
        }
    }
})

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

こうすることで、きちんとデータが独立して変化するようになりました!

2db813a76c114ac2b4cc1f32002b4523.gif

まとめ

というわけで、なぜVue.jsではコンポーネント内のデータは関数で書くのか?に対する答えとしては、レイアウトなどは同じでも、データをそれぞれで変化させる必要があるからということになります。

そもそも、いつコンポーネントを使うのかと考えた際に、レイアウトは同じでも、中に入ってるデータまで全く同じ表示をすることをないですよね。データだけを変更できるように再利用可能にしたものがコンポーネントです。

当たり前のようですが、あまり意識せずに使っていたのでまとめてみました。

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

Vuetify v-comboboxをアクティブ状態で送信ボタン押すと以前の値が取得される

再現手順
①v-comboboxで入力または項目を選択
②v-comboboxをアクティブ状態のまま送信ボタン押下
→以前の入力データが取得されてしまう

問題のコード



.vue
<div id="app">
  <v-app>
   <v-combobox
               v-model="userValue"
               :items="userItems"
               ref="userComboBox"
               >
     </v-combobox>
    <v-btn color="primary" dark large @click="submit()">送信</v-btn>
  </v-app>
</div>
.js
new Vue({
  el: '#app',
  vuetify: new Vuetify(),
  data () {
    return {
      userValue: "",
      userItems: [
        "A","B","C"
      ],
    }
  },
  methods:{
    submit(){
      alert(this.userValue);
    }
  },
})

submit関数内のconsole.logで出力されるデータは現在入力されているデータではなく、
以前のデータが取得されてしまう。
v-comboboxは非アクティブにならないと入力情報がv-modelに伝わらない為だと思われる。。。

解決法はコンボボックスを非アクティブ(blur)してnextTickを使用する

.js
 submit(){
      this.$refs.userComboBox.blur();
       this.$nextTick(() => {
         console.log(this.userValue);
       })
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vuetify】v-comboboxをアクティブ状態で送信ボタン押すと以前の値が取得される問題

再現手順
①v-comboboxで手入力する
②v-comboboxをアクティブ状態のまま送信ボタン押下
→以前の入力データが取得されてしまう

問題のコード



上記でコンボボックスに何か入力(項目選択ではない)してアクティブ状態のまま送信ボタンを押してみてください
アラートに何も表示されませんが、2回目送信ボタンを押すと表示されます。

.vue
<div id="app">
  <v-app>
   <v-combobox
               v-model="userValue"
               :items="userItems"
               ref="userComboBox"
               >
     </v-combobox>
    <v-btn color="primary" dark large @click="submit()">送信</v-btn>
  </v-app>
</div>
.js
new Vue({
  el: '#app',
  vuetify: new Vuetify(),
  data () {
    return {
      userValue: "",
      userItems: [
        "A","B","C"
      ],
    }
  },
  methods:{
    submit(){
      alert(this.userValue);
    }
  },
})

submit関数内のconsole.logで出力されるデータは現在入力されているデータではなく、
以前のデータが取得されてしまう。
v-comboboxは非アクティブにならないと入力情報がv-modelに伝わらない。
v-text-fieldではこの現象はもちろん発生しない。v-modelへの反映は入力されたら即時行われる為。

解決法はコンボボックスを非アクティブ(blur)してnextTickを使用する

.js
 submit(){
      this.$refs.userComboBox.blur();
       this.$nextTick(() => {
         console.log(this.userValue);
       })
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む