初めてのVue-CLI（CDNのVue.jsからの移行をメインに）

# はじめに

CDNからVue-CILへの移行方法。Vue.jsをCDNを利用していた人が、Vue-CLIへ移行する際に、「こういう対応関係にある、と理解すると分り易いかも？」という話。

Node.js環境は導入済みで、「Vueには、Vue CLIを使う方法があるのね？」って興味を持ち始めた人が対象。ブラウザで直接動作するCDN版からスタートするので、ES5前提。

サンプルコードとしてToDoアプリを扱う。Vue.js公式のDoDoサンプルコードをベースに、機能を落として「追加と削除だけ」としたコードを用いる。

- Vue.js公式のDoDoサンプルコード
    -  [TodoMVC ? Vue.js](https://jp.vuejs.org/v2/examples/todomvc.html)

## 移行の大雑把な流れ

CDNのVue.jsを用いて書いたToDoのWebアプリを、Vue-CLIにへ置き換える。

1. 次のようにな構成で作成したCDN版のToDoアプリから開始（ファイル名は任意）
    - index.html
    - vue_main.js
    - vue_main.css
2. Vue-CLI環境を導入する
    - Vueファイル（`*vue`）という形式でHtmlとJavaScriptとCSSを記述
    - しかし、ブラウザはVueファイルを理解できない
    - 実行時にhtmlファイル他へトランスパイルする環境を作成することで対応
3. CDN版で作成した3ファイルを、MyClient.vueファイルにまとめる（ファイル名は任意）
4. Vue-CLIのスタートファイルとなるApp.vueに、「MyClient.vueを表示して」と記述
5. devトランスパイルして（開発時の動作確認用）、ローカルのブラウザで表示を確認
    - vueファイルが、Htmlとjsファイルに適切に変換される
    - 開発時確認用のhttpサーバーが起動され、その上で確認
6. buildトランスパイルして（公開用）、実際にhttpサーバーに配置して、ブラウザ表示を確認
    - ローカルファイルとしてアクセスすると表示エラーする（※ファイル読込APIの都合、と思われる）


## 動作確認環境

- Node.js : v8.11.3
- Vue.js  : v2.9.6
- OS : Windows 7 / 10

# 移行前のCDN版サンプルコード

ToDoアプリを次のように作成する。

- textareaで複数行のテキスト（ToDoアイテム）入力を受け取る
- ul+liタグでテキストを一覧表示する
    - 入力時の日付を保持しておく
- ToDoアイテム一覧のテキスト部分がクリックされたら、打ち消し線を表示する

![ToDoアプリのサンプル](https://gyazo.com/1eff7db8adf7f34b14ef6a7e01b3a35d.jpeg)

※都合で、実際のコードは**アイコンでは無くボタン**で「追加」と「削除」を表現している。後述。

**デモ画面**は、[こちら](https://jsfiddle.net/wcrLn63m/2/)を参照。

```html:index.html
<html>
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" id="id_viewport" content="width=device-width" >
    
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
    <script src="./javascripts/vue_main.js"></script>
    <link href="./stylesheets/vue_main.css" rel="stylesheet"></link>
</head>
<body>

<div id="id_app1">
    <div id="id_input_area">
        <div id="id_input_textarea">
            <textarea v-model="input_message" placeholder="ここに入力する。複数行可。"></textarea>
        </div>
        <div id="id_input_command">
            <div id="id_input_additional">
                リストに追加する
            </div>
            <div id="id_input_button" v-on:click="clickInputButton">
                <input type="button" value="追加"></input> 
            </div>
        </div>
    </div>
    <div id="id_todolist">
        <ul>
            <li v-for="(item,index) in todo_list" v-bind:key="index"> 
                <!-- (要素、配列番号)で受け取れる仕様 -->
                <div class="item_text" v-on:click="clickItem(index)"><span v-bind:style="item.styleStr">{{ item.text }}</span></div>
                <div class="item_date">{{ item.dateStr }}</div>
                <div v-on:click="clickDeleteButton(index)">
                    <input type="button" value="削除"></input> 
                </div>
            </li>
        </ul>
    </div>
</div>

</body>
</html>

```

```js:vue_main.js
var NoteItem = function (rowtext, createDateMiliSec) {
    var now = (createDateMiliSec) ? new Date(createDateMiliSec) : new Date();

    this.text = rowtext;
    this.utcSec = now.getTime();
    this.dateStr = now.toString();
    this.styleStr = "";
};
NoteItem.prototype.toggleTextStyle = function (styleStr) {
    this.styleStr = (this.styleStr.length==0) ? styleStr : "";
};
var createNoteItem = function (rowtext) {
    return new NoteItem( rowtext );
};


var STORAGE_KEY = "todo-sample-vuejs20190623"
var itemStorage = {
    fetch: function () {
        var todo_list = [];
        var saved_list = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
        if( saved_list.length > 0 ){
            saved_list.forEach(function (item) {
                todo_list.push(
                    createNoteItem(item.text, item.createDateMiliSec)
                )
            });
        }else{
            // 動作検証時、最初からアイテムがあったほうが都合が良いので。
            todo_list.push( createNoteItem("アイテムを動的にリスト表示1") );
            todo_list.push( createNoteItem("アイテムを動的にリスト表示2") );
            todo_list.push( createNoteItem("アイテムを動的にリスト表示3") );
        }
        return todo_list;
    },
    save : function (todo_list) {
        var saving_list = [];
        todo_list.forEach(function (item) {
            saving_list.push({
                "text" : item.text,
                "createDateMiliSec" : item.utcSec
            })
        });
        localStorage.setItem(STORAGE_KEY, JSON.stringify(saving_list));
    }
};


window.onload = function () {
    var app1 = new Vue({
        el : "#id_app1",
        data : {
            input_message : "",
            todo_list : itemStorage.fetch()
        },
        watch : {
            todo_list : {
                handler: function (todo_list) {
                    itemStorage.save(todo_list);
                },
                deep : true
            }
        },
        methods : {
            clickInputButton : function () {
                var new_text = this.input_message;
                if( new_text.length > 0 ){
                    this.todo_list.push( createNoteItem(new_text) );
                    this.input_message = "";
                }
            },
            clickItem : function (index) {
                this.todo_list[index].toggleTextStyle("text-decoration: line-through;");
                // ToDo: クリックでのトグル動作時の扱いを『暫定』としたいので、このような実装にする。
            },
            clickDeleteButton : function (index) {
                this.todo_list.splice(index, 1);
            }
        }
    });
};
```

```css:vue_main.css
#id_input_area textarea{
    width : 480px;
    height: 80px;
}

#id_todolist ul li {
    margin: 16px;
    padding: 4px;
    background-color: burlywood;
}

.item_text {
    white-space: pre-wrap;
    cursor: pointer;
}


```

# Vue CLIへ移行の前準備

Node.js環境にて、以下のコマンドを実行してVue-CLIを導入する。
なお、ここでは「ローカル」にインストールする。
（※「グローバル」にインストールしたい場合は「`-g`」をオプションを付けること）

```
npm install vue-cli
```

つづいて、以下のコマンドを実行して、vue-cliの**スケルトン的なもの**を作成する。これは「プロジェクト」などと呼ばれる一式。プロジェクト名は、ここでは「samplevueclie」とする。
（※上述のvue-cliを「グローバル」にインストールした場合は「`vue init webpack samplevueclie`」で実行する）

```
node_modules\.bin\vue init webpack samplevueclie
```

コマンドを実行すると、いくつかの質問を聞かれる。デフォルトのまま何も変更せずにEnterを押して良い。作者名は？のところには自分の名前を入れる。「なるべく簡単にしたい」場合は、build形式に「runtime」を選び、「Vue-routerを使うか？」以降の問いには「No」を答えるのもよい（私はそうした）。

```
$ Project name samplevueclie
$ Project description A Vue.js project
$ Author hoshimado
$ Vue build (Use arrow keys) runtime
$ Install vue-router? No
$ Use ESLint to lint your code? No
$ Set up unit tests No
$ Setup e2e tests with Nightwatch? No
```

フォルダ「`samplevueclie`」の中に、Vue-CLIのプロジェクト（vueトランスパイル用の環境）が作成される。

次のコマンドを実行して、ブラウザで次のURLを開いて、先ずは動作確認をする。

```
cd samplevueclie
npm run dev
```

`http://localhost:8080/`

ブラウザにVue.jsのサンプルページが表示されることを確認したら、コマンドライン上で「Ctrl+C」キーを押す。これにより、一旦Vueが止まる。


# 移行後のVue-CLIのコード

先に作成したVue.jsのプロジェクト内に、CDN版で作成したhtmlファイル、javacriptファイル、cssファイルを配置する。

## 考え方

先のVue-CLI導入直後の動作確認時に表示したページの実態は、フォルダ「`samplevueclie\src\components\`」配下にある、`HelloWorld.vue`である（中身はテキストファイル）。

これを開くと、先のブラウザで表示された内容であることがなんとなく見える。

そして、ファイル「`HelloWorld.vue`」を表示するように指定しているのは、フォルダ「`samplevueclie\src\`」直下にある`App.vue`である。

この `App.vue` ファイルにて、次のように「`HelloWorld.vue`を表示せよ」と記述されている。（正確には、main.jsにてApp.vueを描画するよう記述されていて、App.vueに記載されたVueコンポーネントが表示される。この例では、HelloWordlコンポーネント**のみ**が記述されているので、「実態はHelloWorld」という表現をした。）

```vue:App.vue
<template>
  <div id="app">
    <img src="./assets/logo.png">
    <HelloWorld/> <!-- ★★ここで指定★★ -->
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld' // ★★ここで指定★★ 

export default {
  name: 'App',
  components: {
    HelloWorld  // ★★ここで指定★★ 
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

```

したがって、表示したい内容のVueファイルを作成して、`App.vue`に「そのファイルを表示せよ（読み込め）」と記載すればよい。

## コード配置の例

今回に表示したいのはCDN版で作成したToDoアプリである。CDN版では、htmlファイルと、JavaScriptファイル、Cssファイルに分けて書いた。Vue-CLI版では、これを１つのVueファイルにまとめて記述することができる（推奨される）。

ここではVueファイルとして「`MyClient.vue`」を作成し、ここに先のCDN版のファイルからコードを移植する。「`MyClient.vue`」ファイルは、「`HelloWorld.vue`」ファイルと同じ「`samplevueclie\src\components\`」配下に格納する。

次のルールに従って、移植する。

- htmlファイルの、Vue用のdivタグ配下を、templateタグ配下へコピー
- JavaScriptは、scriptタグ配下へコピー
    - `Vue({})`の部分は`{}`を取り出して、`export default {}`としてコピー
        - その際に、elタグに代わってnameタグを用いる
        - `data : {}`部分は、`data : function(){return: {}; }`のように、関数の戻り値としてオブジェクトを返却するように変更する 
    - `Vue({})`以外のJavaScript部分は、そのままコピーする
- Cssは、styleタグ配下へコピー

```vue:MyClient.vue
<template>
<div><!-- 「id="id_app1"」としていたdivタグ配下をココへ配置。id_app1自体の定義は不要 -->
    <div id="id_input_area">
        <div id="id_input_textarea">
            <textarea v-model="input_message" placeholder="ここに入力する。複数行可。"></textarea>
        </div>
        <div id="id_input_command">
            <div id="id_input_additional">
                リストに追加する
            </div>
            <div id="id_input_button" v-on:click="clickInputButton">
                <!-- 
                <a href="#"><i class="fas fa-pen fa-2x"></i></a>
                -->
                    <input type="button" value="追加"></input> 
            </div>
        </div>
    </div>
    <div id="id_todolist">
        <ul>
            <li v-for="(item,index) in todo_list" v-bind:key="index"> 
                <!-- (要素、配列番号)で受け取れる仕様 -->
                <div class="item_text" v-on:click="clickItem(index)"><span v-bind:style="item.styleStr">{{ item.text }}</span></div>
                <div class="item_date">{{ item.dateStr }}</div>
                <div v-on:click="clickDeleteButton(index)">
                    <!-- 
                    <a href="#"><i class="fas fa-trash-alt"></i></a>
                    -->
                        <input type="button" value="削除"></input> 
                </div>
            </li>
        </ul>
    </div>
</div>
</template>

<script>
// javascriptファイルをココへ。
var NoteItem = function (rowtext, createDateMiliSec) {
    var now = (createDateMiliSec) ? new Date(createDateMiliSec) : new Date();

    this.text = rowtext;
    this.utcSec = now.getTime();
    this.dateStr = now.toString();
    this.styleStr = "";
};
NoteItem.prototype.toggleTextStyle = function (styleStr) {
    this.styleStr = (this.styleStr.length==0) ? styleStr : "";
};
var createNoteItem = function (rowtext) {
    return new NoteItem( rowtext );
};


var STORAGE_KEY = "todo-sample-vuejs20190623"
var itemStorage = {
    fetch: function () {
        var todo_list = [];
        var saved_list = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
        if( saved_list.length > 0 ){
            saved_list.forEach(function (item) {
                todo_list.push(
                    createNoteItem(item.text, item.createDateMiliSec)
                )
            });
        }else{
            // 動作検証時、最初からアイテムがあったほうが都合が良いので。
            todo_list.push( createNoteItem("アイテムを動的にリスト表示1") );
            todo_list.push( createNoteItem("アイテムを動的にリスト表示2") );
            todo_list.push( createNoteItem("アイテムを動的にリスト表示3") );
        }
        return todo_list;
    },
    save : function (todo_list) {
        var saving_list = [];
        todo_list.forEach(function (item) {
            saving_list.push({
                "text" : item.text,
                "createDateMiliSec" : item.utcSec
            })
        });
        localStorage.setItem(STORAGE_KEY, JSON.stringify(saving_list));
    }
};

// 「var app1 = new Vue({})」としていた部分の、「{}」の中だけを「export default {}」の部分は位置する。
// また、「el」キーは削除して、代わりに「name」キーを配置する。値にはファイル名を入れる（とりあえず）。
export default {
    name : "MyClient",
    data : function () { // オブジェクト{}で定義していた値を、「その値を返却する（無名）関数」に書き換える。
        return {
            input_message : "",
            todo_list : itemStorage.fetch()
        }
    },
    watch : {
        todo_list : {
            handler: function (todo_list) {
                itemStorage.save(todo_list);
            },
            deep : true
        }
    },
    methods : {
        clickInputButton : function () {
            var new_text = this.input_message;
            if( new_text.length > 0 ){
                this.todo_list.push( createNoteItem(new_text) );
                this.input_message = "";
            }
        },
        clickItem : function (index) {
            this.todo_list[index].toggleTextStyle("text-decoration: line-through;");
            // ToDo: クリックでのトグル動作時の扱いを『暫定』としたいので、このような実装にする。
        },
        clickDeleteButton : function (index) {
            this.todo_list.splice(index, 1);
        }
    }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/* Cssファイルはここへ配置する。 */
#id_input_area textarea{
    width : 480px;
    height: 80px;
}

#id_todolist ul li {
    margin: 16px;
    padding: 4px;
    background-color: burlywood;
}

.item_text {
    white-space: pre-wrap;
    cursor: pointer;
}
</style>

```

続いて、`App.vue`ファイルを、`HelloWorld`に代わって`MyClient`を呼びだすように変更する。具体的には次のように変更する（CSS部分で元々のは「中央寄せ」が指定されているが、ここでは不要なので解除する）。

```vue:APp.vue
<template>
  <div id="app">
    <MyClient/>   // ★★ここで指定★★
  </div>
</template>

<script>
import MyClient from './components/MyClient'   // ★★ここで指定★★

export default {
  name: 'App',
  components: {
    MyClient   // ★★ここで指定★★
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
```

新しく作成したファイルと、変更したファイルの配置は以下。

```
samplevueclie\src\App.vue
                 \components\MyClient.vue
```

ここまでの変更を終えたら、次のコマンドを実行してVue-CLIを開発時検証用にBuildする。

`npm run dev`

ブラウザで「`http://localhost:8080/`」にアクセスすると、今度は先ほどのCDN版のToDoアプリと同じ内容が表示される。

なお、以降のソースコードの編集時は、`npm run dev`したままにしておくと、変更後のファイルを保存した時点で**即座に**ブラウザの表示に反映される（ホットリロード機能）。保存する度にBuildコマンドを明示的に実行したり、ブラウザ表示を更新する操作は不要。

`npm run dev`の実行を終了したいときは、「Ctrl＋C」で止めることが出来る。

### 余白

「*.vue」ファイルの編集にVSCodeを利用している場合、VSCodeが「Veturプラグイン入れるかい？」って聞いてくる。これを導入すると、Vueファイルも色分けで表示されるようになって便利なのでインストールお勧め。


## Vue-CLIのプロジェクトから、公開用のファイルをビルドする。

公開時には「`*.vue`」ファイルから毎回トランスパイルする必要はなく、ブラウザが理解することが出来るHtml、js、cssファイルのみがあれば良い。

次のコマンドを実行して、公開用のHtml、js、cssファイルを作成する。

`npm run build`


distフォルダ配下に、トランスパイルされたファイルが出力される。

```
dist\index.html
    \static\css\
           \js\
```

このdistフォルダ配下のファイル一式を、任意のhttpサーバーにおけば良い。ブラウザで配置先のurlをひらいて、表示を確認する。

※出力時のメッセージ「`Tip: built files are meant to be served over an HTTP server. Opening index.html over file:// won't work.`」に記載あるように、**ローカルファイルとして**ブラウザで開いても表示エラーするので、注意。

※httpサーバーの公開フォルダのルートに配置する事（root/index.html）。これは、index.htmlからjsとcssファイルへのリンク記述が、**ルートからの相対パス表記**に成っているため。



以上ー。

# 参考サイトや本など

- Vue.js公式サイト　- Web
    - https://jp.vuejs.org/
- 基礎から学ぶVue.js - 本
    - https://www.amazon.co.jp/dp/4863542453/


## 余白

長くなったので、以下については後日に別記事にて記載予定。

- `*.vue`ファイルでは、`requre()`コマンド、`import`コマンドを使うことができる。
    - つまり、javascriptファイルのモジュール化＆スコープのローカル化を簡単にできる♪
    - モジュール化できるってことは、ファイル単位でのUnitTestも簡単にできるってこと。
        - Vue-CLIでのMochaによるTest実行方法も [こちら](https://qiita.com/hoshimado/items/abf813fc9457992fed86) に書いてみた。
        - なお↑は、Vue-CLI自身が「Karma＋Mocha」でのテストを準備してくれているので、その仕組みに沿って「やってみた」って内容。
- fontawesomeを使おうとすると、どこでlink読み込む？問題にぶつかる。
    - `*.vue`ファイル内で直に読み込む方法もあるようだが、nodejsのモジュールとして追加で`npm install`する方法が望ましい。