Goでの小さいweb開発について
依頼されて開発をして、3年近く小さめのwebサイトをGo + Revelで運用してきて正直そこまで恩恵を受けれなかったように感じる。ので以下のように構成変更した。
Before
- 言語: Go v1.9
- パッケージ管理: dep
- フレームワーク: Revel v0.21.0
- ORM: GORM
- ミドルウェア: なし、cacheはrevel cache
- DB: MYSQL
- 開発環境: Docker
- CI/CD: CircleCI
こうなるまでにも3年間でいろいろしたんですが、いろいろ問題がありました。
Beforeの問題点
- Revelが思ったほど盛り上がらなかった
- GoでのフルスタックなMVCフレームワークとして前は盛り上がってたけど・・・
- Web開発のためのモジュール追加があまりなかったので自前で実装した部分がとても多かった
- Goの変遷がめんどかった
- Goを取り巻く環境はバージョンのアップデートともにパッケージ管理や開発環境のアップデートが必須でした。
あんま本質的ではない保守みたいのがどうしても増えた - 2ヶ月に機能追加しようとすると新しく作った環境でgoのバージョン固定に失敗しててアップデートで2日かかったり
- Goを取り巻く環境はバージョンのアップデートともにパッケージ管理や開発環境のアップデートが必須でした。
- GORMが使いにくかった
- いまいちDSL的な部分がわかりにくくて結局直接SQL書いた部分が多くて本質的じゃなかった
- 管理画面を作りたいみたいなケースでちょうどいい仕組みが見つからなかった
- 結局自前で実装した
- セキュリティ等
- Revelはそこまで考えてくれない
- CSRFの対策なども別パッケージである
- Revelが各種サービスにおいて標準的じゃない故に
- Azureのデプロイとかで自前でpowershellを書いた
- Debugしにくい
Beforeの良かった点
- Goの恩恵
- 標準ライブラリの強さ・優秀さ
- シングルバイナリデプロイ
- **「Go書いている」**という気分の良さ
- stripeの決済系の実装などはSDKもあってサンプルもなぜかGoが結構あり良かった
帰ろう、俺たちのPHPへ
After
- 言語: PHP v7.2
- パッケージ管理: composer
- フレームワーク: Laravel (Laravel admin)
- ORM: Eloquent (Laravel)
- ミドルウェア: redis
- DB: MYSQL
- フロントエンド: vue + nuxt.js
- 開発環境: Docker(Laradock)
- CI/CD: CircleCI
だいたい2週間くらいで実装は終わりました。
Afterのいい点
500年ぶりにちゃんとPHP書いたんですがLaravel便利すぎて泣きそうでした。
Laradock
ミドルウェア含むLaravelの動く環境をすぐ作れるし必要なconfファイルなどももろもろ手に入る。
buildしてupしたら動くので瞬殺である。
Laravel admin
これがあるだけでLaravelにしたくらい便利。CRUDが
php artisan admin:make StaffController --model=App\Staff
でroutingするだけで簡単なものがすぐ作れる、ロールの設定やリソースアクセスやS3へのアップロード系のプラグインも入っているのでもう脳がいらない。
Laravel Passport
認証までデフォルト実装されている。
Goの時はいいライブラリが見つからなくてがんばって自前でauthとかtoken管理とか書いたので泣いた。
AfterのFrontendが特にいい
Afterを支えたフロントエンド技術スタック
- Vue
- Nuxt
- apiblue print
- drakov
- aglio
- Atomic components
一部複雑になったjsの内部でjqueryが使っており、紐解く元気が無くてこっそりjqueryが動いている・・・
nuxt.js
Laravelのプロジェクト内に client
ディレクトリがあり、そこにフロントエンドリソースが入っている。
nuxt.jsはvueを使ってサーバサイドレンダリング可能なSPAなどのweb appliactionを実装するためのフレームワークである。
今回のプロジェクトの内部は以下のようになっている。
% tree -L 2 [10:27:15]
.
├── README.md
├── api-blueprint // localhost:3001でblueprintが、localhost:9080でmock apiが動く
│ └── src
├── assets // webpackで管理されるリソース
│ ├── README.md
│ ├── css
│ ├── icon
│ └── img
├── components // vueのパーツたち
│ ├── AppLogo.vue
│ ├── README.md
│ ├── atoms
│ ├── molecules
│ ├── organisms
│ └── templates
├── layouts // pegeを構成する共通テンプレート
│ ├── README.md
│ ├── default.vue
│ ├── error.vue
│ └── static.vue
├── middleware
│ └── README.md
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages // 構成によって自動でルーティングが決まる
│ ├── README.md
│ ├── company
│ ├── creators
│ ├── guideline
│ ├── index.vue
│ ├── list
│ ├── movies
│ ├── privacy
│ └── user
├── plugins
│ ├── README.md
│ └── axios.js
├── static // webpackで管理されないリソース
│ ├── README.md
│ ├── assets
│ └── favicon.ico
└── store // vuexでいうstate
├── README.md
└── user.js
nuxtによる恩恵: 構造化されたテンプレート
1. ルーティング
nuxtによるSPAはディレクトリ構造から自動でルーティングが決まる。
例)localhost/movies
- pages
- movies
- index.vue
例)localhost/movies/1
- pages
- movies
- _id.vue
2. テンプレート
例えば以下のようなdom構造のページに対して
- head
- meta1
- meta2
- body
- header
- section1
- content1
- content2
- footer
nuxtでは以下のように分類して実装が可能になる
- head // nuxt.config.jsに記述
- meta1 // nuxt.config.jsに記述
- meta2 // nuxt.config.jsに記述
- body // layout/default.vueに記述
- header // components/melocules/Header.vue
- section1 // components/template/Section1.vue
- content1 // components/melocules/Content1.vue
- content2 // components/melocules/Content2.vue
- footer // components/melocules/Footer.vue
それぞれのコンポーネントを分離してCSSのカプセル化、ロジックの分類が可能になる。
module.exports = {
env: {
nodeEnv: process.env.NODE_ENV,
apiBaseURL: apiBaseURL
},
/*
** Headers of the page
*/
head: {
title: 'client',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: 'Nuxt.js project' },
],
link: [
{ rel: 'shortcut icon', type: 'image/x-icon', href: '@/assets/img/favicon.ico' },
{ rel: 'apple-touch-icon', type: 'image/x-icon', href: '@/assets/img/apple-touch-icon.png' }
]
},
css: [
{ src: '@/assets/css/main.css'},
],
....
API Mockの自動化
開発時にAPIを実際に作る前にフロントからモックのAPIを叩ける状態を作りつつAPIドキュメントを作りたい。
正直swaggerはね、めんどくさいんですよ我々フロントエンジニアには・・・
- aglio
- API Blueprintをファイルを分割しつつもいい感じにmarkdownで書ける
- drakov
- API Blueprintのmdからmock apiを起動する
*ただdrakovは複数ファイルに対応してないので、aglioで以下のようにビルドして対応
// blueprintの起動
"blueprint": "aglio -i ./api-blueprint/src/index.apib --server ",
// mockのビルド、drakovで参照するまとめたファイルを作る
"apimock-build": "aglio -i ./api-blueprint/src/index.apib -c -o api-blueprint/src/compiled.apib ",
// まとまったファイルからmock api起動
"apimock": "drakov -f 'api-blueprint/src/compiled.apib' --watch -p 9080 --autoOptions"
ルーティング
SPAとしてのJSによるルーティングとPHPによるルーティングが混在する問題がある。
PHPはAPIのみを提供するようにしたかったけど、Facebookログインのコールバックとかでどうしても口を用意しないといけなかったので以下のようにした。
Route::get('/logout', 'Auth\SocialLiteController@logout');
Route::get('/auth/signin', 'Auth\SocialLiteController@signin');
Route::get('/auth/login', 'Auth\SocialLiteController@login');
Route::get('/auth/callback', 'Auth\SocialLiteController@callback');
Route::get('/{any}', function () {
return view('spa');
})->where('any', '.*');
基本的には全てSPAで受けてvue-routerでルーティングする。これは基本的にSPAがベースであるのでいけるが、逆の場合は結構めんどくさい・・・
axios + Laravel
axiosはモダンなJSのAPI clientである。
Laravelでもサポートされており、CSRFの対策などがデフォルトで入るようになっている。
Tip!! デフォルトでAxios HTTPライブラリにより、csrf-tokenメタタグの値が、resources/assets/js/bootstrap.jsへ保持されます。このライブラリを使用しない場合、自分のアプリケーションでこの振る舞いを実現する必要があります。
他の選択肢などないのでは・・・
ただこれはあくまでLaravel Mixを使ってLaravel/resources配下をちゃんとビルドしている前提であり、nuxtが別で管理されているとビルド忘れがち。
その他の工夫
- seedはCSVからできるようにした
- Factoryからもできるようにした
くらいか・・以上。
やはりデプロイとかでいうとビルド環境と稼働環境が前者さえあれば後者には余計なセットアップが入らないので楽だったけど、前者が結構手間だった感じ。PHPにすると両方それなりだが圧倒的低い知識でいいので楽。