vuexを用いたnpmパッケージの作り方

vue.jsのコンポーネントライブラリをnpmパッケージとして公開するやり方は多くありますが、vuexを用いたライブラリの作り方の記事は少なかったので、今回私が作ったライブラリを例に、ここに書き残しておこうと思います。

# 自己紹介

wh.im というサービスを運営しています @aitaro です。普段は大学院生やっています。wh.imの詳細は[こちらの記事](https://qiita.com/aitaro/items/8a97d320f5586c6e7bb6)から。

# wh.im用の開発用ライブラリを作りました
リポジトリは[Github](https://github.com/whimRTC/whim-client-vue)にあります。
npmパッケージを作ったのは今回が初めてのため、間違いやよりベターな方法があればコメントで指摘いただけると嬉しいです。

# npmパッケージを作った背景
昨日リリースした[wh.im](https://wh.im)ですが、複数のエンジニアの方から、自分もゲーム作ってみたいという声を頂きました。(ありがとうございます！)
ただ、wh.imとの連携部分など、少しクセの強いところがあったので、ライブラリ化して開発者が連携部分を意識せず開発できるようにに、今回ライブラリ作成に踏み切りました。

# npｍパッケージ公開までのステップ
1. VueCLIで新規プロジェクト作成
2. 不要なファイル削除
3. package.jsonの編集
4. storeの読み込み & ゴリゴリ実装
6. npmにアカウント作成
7. build & deploy
8. ENJOY!

今回のメインは4番ですが、順を追って解説しようと思います。

# 1. VueCLIで新規プロジェクト作成
いつものやつです。下を実行します。`whim-client-vue`の部分は適宜読み替えてください。

```bash
$ vue create whim-client-vue
```

色々聞かれますが、必要なのはBabelとVuexです。(Linterはお好みで)

# 2. 不要なファイル削除
普通にcreateすると下のようなフォルダ構造になっているはずです。(.git, node_modulesは除外)

```
whim-client-vue
├── .browserslistrc
├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.js
│   └── store
│       └── index.js
└── yarn.lock
```
今回はVuexの部分しか使わないので、vueコンポーネント等関係ない部分は消していきます。最終的に以下のようになりました。

```
whim-client-vue
├── .browserslistrc
├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── src
│   ├── main.js
│   └── store
│       └── index.js
└── yarn.lock
```

# 3. package.jsonの編集
編集後のpackage.jsonを下に載せます。(devDependenciesはlint等の設定で変わります。)
"private" はモジュールとして公開しないという設定なので、消しておきます。
"scripts" の部分はbuildだけ下記の内容に変更し、serveやlintを消しておきます。
また、"version"は慣習として、開発中は 1.0.0 未満に設定し、開発が完了して初めて 1.0.0 に上げるようです。

```package.json
{
  "name": "whim-client-vue",
  "version": "0.1.0",
  "scripts": {
    "build": "vue-cli-service build --target lib --name whim-client-vue ./src/main.js"
  },
  "dependencies": {
    "core-js": "^3.6.4",
    "vue": "^2.6.11",
    "vuex": "^3.1.3"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.3.0",
    "@vue/cli-plugin-eslint": "~4.3.0",
    "@vue/cli-plugin-vuex": "~4.3.0",
    "@vue/cli-service": "~4.3.0",
    "@vue/eslint-config-prettier": "^6.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.1.1",
    "eslint-plugin-vue": "^6.2.2",
    "prettier": "^1.19.1",
    "vue-template-compiler": "^2.6.11"
  }
}
```

# 4. storeの読み込み & ゴリゴリ実装
本記事のメインディッシュです。こちらの記事を参考にしました。
- https://forum.vuejs.org/t/should-i-use-vuex-when-creating-a-component-library/57589

### store/index.js
まず、`store/index.js`を編集します(簡略化したコードを使っています)。

```store/index.js
// initial state
const state = {
  appState: {}, // room information
};

// mutations
const mutations = {
  // appStateに値を代入
  setAppState(state, appState) {
    state.appState = appState;
  }
};

// actions
const actions = {
  // appStateの値をリセット
  deleteState() {
    setAppState({});
  }
};

const getters = {
  // appStateの値を取得
  appState: state => {
    return state.appState;
  },
};

export default {
  // 名前空間を使うように
  namespaced: true,
  state,
  mutations,
  actions,
  getters
};

```
コードの中身は setAppState関数でstateのappStateを変更、stateはappStateの情報を保持し、gettersでappStateの情報を取り出します。actionsのdeleteStateではappStateの情報をクリアしています。
また、namespaced: true を使うことで、このライブラリが導入された先で名前の衝突がおこらないようにしています。

### main.js
その後、`main.js`を編集します。

```main.js
// store/index.js の読み込み
import store from "./store";

export default {
  install(Vue, options) {
    // storeが渡されなかったら、エラーを出力
    if (!options.store) {
      throw new Error("Please provide vuex store.");
    }

    // vuex module を名前空間付きで登録
    options.store.registerModule("whimClient", store);

    // $whimにメソッドを生やす
    Vue.prototype.$whim = {
      getState() {
        return options.store.getters["whimClient/appState"];
      },
      deleteState() {
        return options.store.dispatch("whimClient/deleteState");
      }
    };
  }
};

```
順を追って解説します。

まずはじめに`store/index.js`を読み込みます。

```js
import store from "./store";
```

次にexportするObjectにinstall関数を定義します。
このinstallはライブラリがmain.jsで
`Vue.use(whimClientVue, { store })`
と読み込まれるときに、呼び出されます。したがって install以下のスクリプトはこのライブラリが読み込まれたユーザー側で実行されます。
optionsには、Vue.useで呼び出したときの第２引数が入ります。(上の例では`{ store }`)


```js
export default {
  install(Vue, options) {
    // ...
  }
};
```

今回はvuexが必須なので、vuexがoptionsに渡されていることを確認します。
渡されなかった場合はerrorを投げます。
私は普段ズボラなので、エラーは適当に済ませますが、今回は公開するライブラリで、なおかつ(storeの渡し忘れといった)発生が想定されうるエラーなので、エラーが受けた人がなぜエラーになったかをわかるようにします。

```js
if (!options.store) {
   throw new Error("Please provide vuex store.");
}
```

次に、moduleの登録をします。今回はwhimでもよかったのですが、名前衝突を避けるためにちょっと捻ります。
基本的にここで登録するstoreは、ユーザーは直接使わないので、ややこしめの名前ぐらいで良いと思います。

```js
// Register vuex module
options.store.registerModule("whimClient", store);
```

最後に、ユーザーが`$whim`経由でアクセスできるメソッドを登録します。このように登録することで、Vue上の任意の場所で、`this.$whim.getState()`でappStateの値がとれたり、`this.$whim.deleteState()`でappStateの値を消すことができます。

```js

    Vue.prototype.$whim = {
      getState() {
        return options.store.getters["whimClient/appState"];
      },
      deleteState() {
        return options.store.dispatch("whimClient/deleteState");
      }
    };
```

### 蛇足(飛ばしてもOK)
`this.$whim.getState()`でappStateの値が取れるようになりましたが、使いづらくないですか？私は使いづらいです。例えば、stateのxxxプロパティにアクセスするのに、`this.$whim.getState().xxx`と書くのはちょっと抵抗があります。私は`this.$whim.state.xxx`という感じでアクセスしたい！と思ったので、どうしたらそうかけるかを調べました。
上のコードを以下のように変更します。

```js
    let prototypeWhim = {
      deleteState() {
        return options.store.dispatch("whimClient/deleteState");
      }
    };

    Object.defineProperty(prototypeWhim, "state", {
      enumberable: true,
      get: function() {
        return options.store.getters["whimClient/appState"];
      }
    });

    Vue.prototype.$whim = prototypeWhim;
```

prototypeWhim Objectに対して、definePropertyを用いて動的に、propertyをセットしております。
こうすることで、property風にappStateにアクセスすることができます！

# 5. npmにアカウント作成
ゴリゴリとライブラリが完成したら次は公開します。
まずは準備として、npmアカウントを[こちらから](https://www.npmjs.com)を作ります。その後、コンソールで以下のコマンドを叩き、ログインします。

```bash
$ npm login
```

ここまでできたら準備完了です！

# 6. build & deploy
最後の仕上げにnpmでライブラリを公開します。
まず、buildをします。

```bash
$ npm run build
# or
$ yarn build
```
そうすると、`dist/whim-client-vue.common.js`といったファイルが生成されます。
これがbuildされたスクリプトファイルなので、これをpackage.jsonの "main" に登録します。
また、ライセンスもお好みで作っておきます。今回は`"license": "MIT"`を設定しました。
最終的なファイルは以下になります。(devDependenciesはlint等の設定で変わります。)

```package.json
{
  "name": "whim-client-vue",
  "version": "1.0.1",
  "main": "dist/whim-client-vue.common.js",
  "license": "MIT",
  "scripts": {
    "build": "vue-cli-service build --target lib --name whim-client-vue ./src/main.js"
  },
  "dependencies": {
    "core-js": "^3.6.4",
    "vue": "^2.6.11",
    "vuex": "^3.1.3"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.3.0",
    "@vue/cli-plugin-eslint": "~4.3.0",
    "@vue/cli-plugin-vuex": "~4.3.0",
    "@vue/cli-service": "~4.3.0",
    "@vue/eslint-config-prettier": "^6.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.1.1",
    "eslint-plugin-vue": "^6.2.2",
    "prettier": "^1.19.1",
    "vue-template-compiler": "^2.6.11"
  }
}
```
その後以下のコマンドで公開完了です🍺

```shell
$ npm publish
```
[npm](https://www.npmjs.com)のサイトからデプロイされたことが確認できます。
自分のプロジェクトに追加して、楽しみましょう！

# 今回わからなかったこと
今回一通りやってわからなかったこと、やり残したことをメモしておきます。
### 動作確認について
ライブラリを作るとき、実際にimportして動作確認しながら作りたかったのですが、そのやりかたがわかりませんでした。結局毎回npmにpublishしてバグってたら修正していきました。

### CI/CDについて
今回のように手元でpublishしていると、ヒューマンエラーでpublishし忘れといった事態が考えられます。そうするとgithubのコードと実際に動いているコードが変わってしまい、そのライブラリのユーザーが混乱します。
CI/CDは導入しましょう(自分に念押し)。

### テストコードについて
途中で力尽きて、ここまでやり遂げられませんでした。
己の力不足です。反省。
やっぱあった方がいいよね。


# まとめ
vue.jsのライブラリの作り方を1から一通りまとめました。jsのライブラリは初めてでしたが、思ってたよりすんなりいった印象です。ただ一方いくつかの課題も見えてきたので、もう少し色々探って行きたいと思います。

## 最後に
今回題材として使ったのは [wh.im](https://qiita.com/aitaro/items/8a97d320f5586c6e7bb6) というサービスを使ってゲームを作るときにあると便利な [whim-client-vue](https://github.com/whimRTC/whim-client-vue) です。[こちらのドキュメント](https://docs.wh.im/developer/)を読むと簡単につくれると思います。興味があれば、ぜひゲームを作ってみてください。