会社の新しいプロジェクトの実装を今のところ1人でやっている。Androidのクライアントアプリはもちろん、フロントエンドのWebページとかバックエンドのサーバー、 Cloud Function と言った物の実装も必要で、それならばいっそのこと自分の好きなkotlinを使って実装をしてみようかと思って挑戦をしている。いわゆる Full stack kotlin プロジェクト。
プロジェクト構成作りと手を付けられる部分の実装を並行して進めていて、1週間くらい経ってプロジェクトの構成がまとまったのでここまでの感想を書いてみる。
1年後、同じようにkotlinを Full stack に利用しているかどうかはわからないが、それならばそれで今の気持ちを思い返すことで何が駄目だったのかが分かる。あるいは、1年後にプロジェクトそのものが閉じていた場合には、それはそれで Full stack kotlin プロジェクトに敗因があったのかもしれないと反省できる。いずれにせよ、少なくとも1年後の自分のための備忘録として役立つはずという気持ちでこの記事を書く。
プロジェクトの構成
ソースコードやパッケージを明かすことは今は、やりたくない(経営判断)なのでディレクトリや利用しているサービスが分かる程度に。
├── android
│ ├── android.iml
│ ├── app
│ │ ├── app.iml
│ │ ├── build
│ │ │ ├── generated
│ │ │ ├── intermediates
│ │ │ ├── kotlin
│ │ │ ├── outputs
│ │ │ ├── reports
│ │ │ └── tmp
│ │ ├── build.gradle
│ │ ├── cred.gradle
│ │ ├── proguard-rules.pro
│ │ └── src
│ │ ├── androidTest
│ │ ├── main
│ | | └── kotlin
│ │ └── test
│ └── build.gradle
├── appengine
│ ├── Dockerfile
│ ├── appengine.iml
│ ├── build
│ │ ├── classes
│ │ │ └── kotlin
│ │ ├── libs
│ │ │ └── appengine.jar
│ │ └── tmp
│ │ └── jar
│ ├── build.gradle
│ ├── node_modules
│ ├── package.json
│ └── src
│ └── main
│ └── kotlin
├── build.gradle
├── common-js
│ ├── build
│ │ ├── classes
│ │ │ └── kotlin
│ │ ├── libs
│ │ │ └── common-js.jar
│ │ └── tmp
│ │ └── jar
│ ├── build.gradle
│ ├── common-js.iml
│ └── src
│ └── main
│ └── kotlin
├── firebase
│ ├── build.gradle
│ ├── firebase.iml
│ ├── firebase.json
│ ├── functions
│ │ ├── build
│ │ │ ├── classes
│ │ │ ├── libs
│ │ │ ├── public
│ │ │ └── tmp
│ │ ├── build.gradle
│ │ ├── functions.iml
│ │ ├── index.js
│ │ ├── package.json
│ │ └── src
│ │ └── main
│ | └── kotlin
│ └── public
│ ├── build
│ │ ├── classes
│ │ ├── libs
│ │ ├── public
│ │ └── tmp
│ ├── build.gradle
│ ├── firebase-debug.log
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ ├── public.iml
│ ├── src
│ │ └── main
│ ├── webpack.common.js
│ ├── webpack.config.js
│ └── wepack.dev.js
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── local.properties
└── settings.gradle
プロジェクト自体はgradleで管理を行っている。サブプロジェクトは、settings.gradle
に記述を行った物を利用している。
rootProject.name = 'himitsu'
include ':common-js'
include ':firebase', ':firebase:public', ':firebase:functions'
include ':android', ':android:app'
include ':appengine'
内訳を詳しく見てみよう。
common-js
kotlin.js を利用したプロジェクトを構成しているので共通化可能な部分についてはこのライブラリプロジェクトにまとめている。具体的にはPromise
をラップしてkotlinのcoroutineに変換するものとか。また、共通のデータエンティティやjsonへのシリアライザーもここにある。他のプロジェクトからは compile project(":common-js")
を使って参照する。
firebase
バックエンドとフロントエンドにfirebaseを利用している。
firebase:public
Hosting を利用してフロントエンドのWebページを作った。利用している技術スタックはいくつかある。
- React
- コンテンツ作成のために利用している。バックエンドとして起動している
appengine
のAPIを呼び出してコンテンツの描画を行わせる
- コンテンツ作成のために利用している。バックエンドとして起動している
- Kotlin.js
- React をkotlinでラップしたkotlin-reactとkotlin-react-domを利用している。
- Webpack
-
require
を解決するために利用している。最初はbrowserifyを使っていた。Kotlin.jsのビルド結果がbuild/dependencies
以下に展開されて、node_modulesと一緒に運用するのがbrowserifyを利用するとうまく行かなかったのでwebpackに変えた。
-
firebase:functions
バックエンドとして利用している Cloud Functions for Firebase のためのプロジェクト。技術的なスタックとしては何も考えずにnode.jsを使っているのだけれど、これもKotlin.jsのビルド結果がbuild/classes/kotlin/main/dependencies
いかに展開されるので、build.gradleに次のようなコードを書いている。ビルド結果を無理やりnode_modules
以下にコピーしている。
task copyKotlinJs(type: Copy, dependsOn: compileKotlin2Js) {
def workDir = "$buildDir/classes/kotlin/main/dependencies"
print workDir
from(workDir) {
include "*.js"
}
into "$projectDir/node_modules/"
}
afterEvaluate {
copyDependenciesKotlinJs.dependsOn copyKotlinJs
assemble.dependsOn copyDependenciesKotlinJs
}
このやり方が正しいのかどうかは知らない。ちなみに、 Cloud Functions for Firebase のエントリーポイントはindex.js
となってしまうため、次のコードを書いてルーティングだけをするコードとしている。
const functions = require('firebase-functions')
const admin = require('firebase-admin')
const functionKt = require("./build/classes/kotlin/main/functions.js")
admin.initializeApp(functions.config().firebase)
exports.search = functionKt.com.covelline.himitsu.search
appengine
バックエンドは Cloud Functions for Firebase 以外にも Google Appengine も使っている。というのも、Webページのスクレイピングが必要でそのためには puppeteer を利用するのがてっとり早そうだったから。というわけでこちもらKotlin.jsを利用している。サーバー機能はexpress。Custom Runtimes for the Flexible Environmentを使っているのでエントリーポイントは自由にできるのがうれしい。そんなわけでpackage.jsonの一部はこんな感じ。
"scripts": {
"start": "NODE_PATH=build/classes/kotlin/main/dependencies node build/classes/kotlin/main/appengine.js",
"watch-start": "NODE_PATH=build/classes/kotlin/main/dependencies nodemon build/classes/kotlin/main/appengine.js"
}
android
これは特に特別なことはない。
感想
良かったこと
好きな言語であるkotlinを利用したコードを書くことができている点が一番気持ち良い。あと、開発環境を Android Studio 一本で行ける点も良い。言語が統一されているので、コンテキストスイッチも不要。以前やってたサービスは、javascriptがメインだったけど、Rubyが入り込んでいたりしたのでコンテキストスイッチが必要だった。
共通化とかについては今はあんまり深くは考えていないけれど、将来的にやり用はあるなと思っている。
ビルド時間も遅くなるかな?と思ったけど、言語のコンパイル自体は別に遅くなかったし、気にならない程度。今のところ遅いと感じているのは、
- Android (しょうがない)
- Webpack
って感じ。Android Studioのサブプロジェクトだけのビルドにショートカットを割り当てていないのでマウス操作が発生している。これは、ショートカットの割当をするだけで解決するのでそのうちやる。
あと、やっぱり型がちゃんとあってコンパイルされる言語である点はjavascriptを書く上で有利だと思った。javascriptのモジュールの全てに対して型を割り当てているわけではないけれど、主だって利用する部分は自分で@JsModule
アノテーションを使ってobject
やclass
を宣言している。そのため、ドキュメントがしっかりと整っているモジュールについては型の間違えに起因するコードのミスは無くなった。
良くないこと
どうしても同じ名前のファイルがいくつもでてきてしまう。これはkotlinが原因ではなくて、一つのリポジトリで作業をしていることが原因なんだけど。Android Studioのファイルを開くショートカット(Cmd + Shift + O
)を使ってファイルをタイプすると一応どのサブプロジェクトに所属しているのか表示されるので助かる。しかし、その補完処理が遅いのかEnterキーを押す瞬間に別のプロジェクトのファイルにフォーカスが当たることがある。いや、それを開きたいわけじゃねえんだよと。
あと、プロジェクトを作るときにはどうしても試行錯誤があった。1週間でまとまったのは奇跡に近い。嘘です、JetBrains/kotlinconf-app: KotlinConf Schedule Applicationをかなりパクりました。ただ、複数のマシンで作業をしていると.iml
ファイルの内容とプロジェクトの構成が違うので、git pull
をしたときにAndroid Studioで正しくプロジェクトをロードできない現象は起きた。そのときは、すべての.iml
ファイルの削除とAndrid Studioの再起動でことなきを得た。これはまあ、初期には発生する問題かもしれない。
あと、未調査だけどRectの中でjsxなら本来はビルドできないコードがビルドできたような気がする。気のせいかもしれない。
杞憂
alt-jsとしてkotlinを利用している点が一番気になる。今後、プロジェクトが進展していく中でチームへの展開を正しく行うことができるのかどうかと言った不安は残る。それもふまえて、こういう文章を残しているのだけれど。
今後
一応、デメリットよりもメリットが上回っているのでこのままこのプロジェクト構成を継続しようと思う。大きく方向転換が発生したら続編の記事を書くかも。あと、色々とまとまったらプロジェクト構成をOSSとして公開したい。