はじめに
社内ツールを1から作っています。まだ実装途中ですが、その話をしようと思います。
目的
- 日報ツールや目標管理ツールが使いにくいという声の解消
- 社内ツール放置問題の解消
- という建前で、1から作り直し、勝手に社内仮想通貨を導入、社食のランチ代を会社側に少し負担してもらいたいという願望の実現
意識していること
-
ユーザ(社員)が幸せになってくれるツールであること
絶対条件です。使ってくれる人がいないと、会社がランチ代を出してくれません。
今回、ちょうど日報ツールの社内アンケート結果があったので、それを参考にしました。 -
エンジニアやデザイナーがワクワクするツールであること
社内ツールは後回しにされがちですが、その理由は、本当に「運営しているサービスに手一杯だから」だけですか?
エンジニアがワクワクするツールにしましょう!コミットしてくれる仲間がいると信じて。
ツールの概要
日報ツール×目標管理ツールになります
今までは、日報ツールと目標管理ツールが別々に存在し、人によっては、目標を意識せずに毎日を過ごしている、、、ということがありそうだったので、こんな感じにしてみました。
日報を書くときに、目標がすぐ横に見えるので、意図せずとも目標を意識して毎日が過ごせそうですね!
という利便性を訴えつつ、メインの機能は、、、
日報がいいね!されると、投稿者に社内仮想通貨が支払われる機能です。(この仮想通貨を集めると、、、ランチを会社が負担してくれるようになるといいのになー、、、(願望))
いわゆる、会社が取引所のような動きをして、社内仮想通貨を日本円(今回はランチ代)に替えてくれるって感じですかね。
現在の構成
さーそろそろ、技術の話をします。
インフラはDockerでNginx、MariaDBコンテナを作り、バックエンドはRailsで実装、フロントにVue.jsとTypeScriptを使い、SPAとして動作させています。
それと、社内仮想通貨取引をBlockchainで管理しています
今回は、
- インフラの話
- バックエンドの話
- フロントエンドの話
- ブロックチェーンの話
を順に話していこうと思います
インフラの話
インフラは自動化かつ共通化されていなければなりません。
理由は2つ
- 一人一人がバラバラの環境でやると、マージしたときに動かない可能性がある
- インフラに詳しくない人でもすんなり入れるようにして、参加者を増やす
以下のようなディレクトリ構成になっていて、docker-compose up -d
とするだけで、全てのコンテナが立ち上がり、開発準備が整います
raildock
|- workspace
|- nginx
|- mariadb
|- docker-compose.yml
|- docker-compose.dev.yml
|- .env.example
作るコンテナは3つで以下が概要です
コンテナ名 | 概要 |
---|---|
workspace |
rubyやnodeなど開発に必要なツールが入ったコンテナで、開発はここの中で行います |
nginx |
Nginxのプロセスのみが動いていて、workspaceの/tmp/sockets/puma.sock を参照しています |
mariadb |
MariaDBのプロセスのみが動いています |
人によってworkspace
に入れたい言語やパッケージ、バージョンが異なるかと思いますが、workspace
コンテナにNode.js
を入れたい場合、.env
ファイルのWORKSPACE_INSTALL_NODE
をtrue
にすると、
...
WORKSPACE_INSTALL_NODE=true
...
WORKSPACE_NODE_VERSION=stable
...
docker-compose.yml
でDockerfileに.env
の環境変数が渡され、
workspace:
build:
context: ./workspace
args:
...
- INSTALL_NODE=${WORKSPACE_INSTALL_NODE}
...
- NODE_VERSION=${WORKSPACE_NODE_VERSION}
...
Node.jsがインストールされます
# Check if NVM needs to be installed
ARG NODE_VERSION=stable
ENV NODE_VERSION ${NODE_VERSION}
ARG INSTALL_NODE=false
ENV INSTALL_NODE ${INSTALL_NODE}
ENV NVM_DIR /home/raildock/.nvm
RUN if [ ${INSTALL_NODE} = true ]; then \
# Install nvm (A Node Version Manager)
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | bash && \
. $NVM_DIR/nvm.sh && \
nvm install ${NODE_VERSION} && \
nvm use ${NODE_VERSION} && \
nvm alias ${NODE_VERSION} && \
npm install -g gulp bower vue-cli \
;fi
Dockerはsubmoduleとして追加できるようにしています。
公開しますので、ぜひ、使って見てください!
※ Welcome Contribute!! です!
バックエンドの話
バックエンドにRailsを使ったのは、みんながそれなりに使えそうだからという理由です。
RailsはAPIとして動作していて、そんなに話すことはありませんが、ユーザ認証のところだけ説明させていただきます
ユーザ認証にはdevise_token_auth
を使っています。
ApplicationControllerで
class ApplicationController < ActionController::Base
include DeviseTokenAuth::Concerns::SetUserByToken
end
このようにincludeしておけば
class Api::SamplesController < ApplicationController
before_action :authenticate_api_user!
def create
sample = current_api_user.samples.create!(sample_params)
render json: sample, root: false
end
end
Controllerのbefore_actionでこのようにしていすることで、tokenのないものは弾かれるようにできますcurrent_api_user
でそのtokenのユーザにアクセスできるので便利ですね!
フロントエンドの話
コンパイラ
webpacker
は使わず、自前でwebpackを用意しています(正確にはlaravel-mix
というwebpack
のラッパーを使っています)
理由は、
- Rails捨てたくなったときにフロントが依存しているのはよくない
-
webpacker
力不足そう、、 - デフォルトで
app/javascript
に作られて、その中しかみない感じが気に入らない -
webpack
が新しいバージョンになって本当にすぐにアップデートがかかるのか心配(メンテがきちんと行われるのか)
ルートディレクトリにwebpack.mix.js
ファイルをこんな感じで用意して、
const mix = require('laravel-mix')
mix.setPublicPath('public')
mix.ts('resources/assets/ts/application.ts', 'public/assets/js')
.sass('resources/assets/sass/application.scss', 'public/assets/css')
package.json
のスクリプトでコンパイルします
"scripts": {
"dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "yarn run watch -- --watch-poll",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
Access Token
先ほどバックエンドのところで紹介したdevise_token_auth
をTypeScriptの方でうまく扱ってやる必要があるので、そちらを紹介!
/auth/sign_in
にemail
とpassword
をパラメータで与えてやると、Response Headerにaccess-token
、client
、uid
が帰ってくるので、それをそれぞれのRequest Headerに与えてやればおkです
import axios from 'axios';
import store2 from 'store2';
export const http = {
async request(method : string, url : string, data : object) {
const lowerCaseMethod = method.toLowerCase();
try {
return await axios.request({ url, data, method : lowerCaseMethod });
} catch (err) {
return Promise.reject(err);
}
},
...
async post(url : string, data : object) {
try {
return await this.request('post', url, data);
} catch (err) {
return err;
}
},
...
init() {
...
axios.interceptors.request.use((config) => {
config.headers['access-token'] = store2.get('access-token');
config.headers['client'] = store2.get('client');
config.headers['uid'] = store2.get('uid');
return config;
});
axios.interceptors.response.use((response) => {
if (response.request.responseURL === axios.defaults.baseURL + '/auth/sign_in') {
const token = response.headers['access-token'] || response.data['access-token'];
token && store2.set('access-token', token);
...
}
return response;
}, (error) => {
return Promise.reject(error);
});
},
};
sign_in
リクエストを送った時に、Response Headerから先ほどの値を取り出し、localStorageに保存、リクエストを送る時にはlocalStorageからそれぞれの値を取り出し、Request Headerに付与しています
Vue.jsとTypeScript
Vue.jsはSPAとして動作しています。
先に使ったライブラリをまとめます
名前 | 概要 |
---|---|
vue-router | routingを行う |
vuex | コンポーネント間のデータ通信 |
vue-class-component | コンポーネントをクラスベースで定義するときに使う |
vuex-class | vue-class-componentで定義したコンポーネントにvuexをinjectするときに使う |
vue-style-loader | vue.js内でScssを使うときに使います |
vue-router
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from './views/home/Home.vue';
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{
path: '/',
component: Home,
name: 'home',
meta: { requiresAuth: false },
},
]
})
router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
if (requiresAuth) {
try {
await // トークンの確認
next();
} catch (err) {
next('/');
}
} else {
next();
}
});
export default router;
Home.vue
のようにそれぞれのviewをimportし、routesの中に入れていきます
meta
タグにrequiresAuth
を付与しておき、true
の場合は、router.beforeEach
の中でトークンを所持しているかの確認を行なっています
vuex
難しいので、詳細は https://vuex.vuejs.org/ja/getting-started.html をご覧ください
それぞれの役割は
state
: データストレージ
getters
: コンポーネントからアクセスするときのgetter
actions
: 非同期を含めた処理を行うことができ、コンポーネントから呼び出すことができる
mutations
: state
の状態変更の際に使う(非同期処理などはダメ)action
からはcommit('mutation名', data)
で呼び出すことができる
こんな感じです
例えば、ユーザの情報をコンポーネント間で共有したい時
import { http } from '@/api/http';
import { userStub } from '@/stubs/user';
const state = {
me: userStub
};
const getters = {
me: (state: any) => state.me,
};
const actions = {
...
async signIn ({ commit } : any, data : object) {
try {
const response = await http.post('/auth/sign_in', data);
commit('changeProfile', response.data);
} catch (error) {
return Promise.reject(error);
}
},
...
};
const mutations = {
changeProfile (state: any, me : object) {
state.me = me;
},
};
export default {
state,
getters,
actions,
mutations,
};
こんな感じになります
vue-class-componentとvuex-classの話
Vue コンポーネントオプション内部で TypeScript が型を適切に推測できるようにするには、Vue.component または Vue.extend でコンポーネントを定義する必要があります:
From : https://jp.vuejs.org/v2/guide/typescript.html#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9
なので、公式にメンテナンスされてるvue-class-component
を使うのが一般的です
また、vuex-classは先ほど、vuexで実装したactionやmutation、getterなどをvue-class-component内にinjectすることができます
以下が使用例です
<template>
...
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { Getter, Action } from 'vuex-class'
import { userStub } from '@/stubs/user';
@Component
export default class AwesomeUser extends Vue {
@Getter me : object
@Action signIn : any
user = userStub
async created () {
this.user = await this.signIn()
console.log(this.user)
console.log(this.me)
}
}
</script>
vue-class-component
は@Component
decoratorをexportするclassにつければokです
vuex-class
も@Getter
や@Action
decoratorをつければinjectさせることができます
Sassの話
CSSのフレームワークとして、Bluma
を使っています
今回、jQueryは入れたくなかったので、jQuery依存が全くないCSSフレームワークを選定しています
ブロックチェーンの話
ブロックチェーン(&分散アプリケーション)のメリットは以前の記事をご覧くださいhttps://qiita.com/anneau/items/3be8cd7cd8c34e28f90a
開発手法に関しても以前、記事を書かせていただきましたので、こちらをご覧くださいhttps://qiita.com/anneau/items/3a65691c8870761292ee
現在、Ethereumを用いた多くのサービスはだいたいこのような構成になっていると思います。
ブロックチェーンで管理されたデータは分散され、改ざんすることはできず、また、消えることはないという素晴らしい機能を備えていますが、まだまだ足りていない機能が多いです(検索機能など、、)、また、今回、社内ツールでは、プライベートなEthereum環境で動かすので、関係ないですが、一般的にはあまり多くの処理をさせると、それだけのコスト(手数料)がかかります。
そのため、普通のデータはRailsなどのアプリケーションを介し、DBに保存し、改ざんを防ぎたいデータ(仮想通貨の取引履歴などはまさにそうですね)はブロックチェーンに記録します
社員のアカウントを作る時、一緒にEthereumのアカウントも作成するようにして、アカウントをRailsのDBに入れておきます
<template>
...
</template>
<script lang="ts">
@Action register : any
...
async created () {
const address = await window.web3.eth.personal.newAccount(this.password)
const response = await this.register(
{ name: this.name, email: this.email, password: this.password, ethereum_address: address })
}
</script>
そのアカウントを用い、目的のアカウントに仮想通貨を送るようにします
仮想通貨の移動は開発手法の記事で書いたものをそのまま使ってますので、そちらを参照してください
結び
長々と失礼しました
省略している部分も多いので、わかりづらいところがありましたら、コメントでお願いします。
余談
Ethereumのアカウントを作った時
const address = await window.web3.eth.personal.newAccount(this.password)
アドレスしか帰ってきていないことに違和感を感じた人はいますでしょうか?
Coincheckの事件が記憶に新しいですが、一般的に、Ethereumを含め、Blockchainサービスにアカウントを作った時、秘密鍵が帰ってきて、それで署名を行い取引を行います。
もちろんEthereumにも用意されており、
http://web3js.readthedocs.io/en/1.0/web3-eth-accounts.html#create
ここにあるように
web3.eth.accounts.create([entropy]);
とすれば、privateKeyが帰ってくるようになっています
それで署名を行い、取引を行うことが一般的です
今回そうしなかったのは、TypeScriptの型定義ファイルが間違っていて、使えなかったからです
プルリクがマージされたので、きっとそのうちnpmに上がってTypeScriptからも使えるようになると思います。
急ぎの方は、submoduleで入れましょう!
もちろん、privateKeyはコールドウォレットで管理しましょう 笑
ではでは〜
終わり