[Obsoleted] Kotlin/JSでマルチプラットフォーム対応のWeb開発を始めよう

### 2020/03/29更新

公式からGradleを使う方法を強く推奨するという注意書きが足されました。Kotlin/JSは既存のフロントエンドに溶け込むのではなく、Kotlin/JSのプロジェクトにフロントエンドの資産を活用していくスタイルが推奨のようです。

https://kotlinlang.org/docs/tutorials/javascript/getting-started-command-line/command-line-library-js.html

### 2019/03/02更新

Kotlin/JSのビルドを [@jetbrains/kotlin-webpack-plugin](https://www.npmjs.com/package/@jetbrains/kotlin-webpack-plugin) でビルドすると、 __watchしていても毎回フルビルドが走る__ 仕様らしく規模が大きくなるにつれてビルド時間が掛かります。ログ上では `Compiling Kotlin sources because the following files were changed: xxx.kt` と表示が出るのでプラグイン側はきちんと差分ファイルを認識しているはずなのですが。
お試しなら大丈夫ですが本格開発する場合は __現状はGradleを使ったビルドをお勧めします__ （設定方法はいつかまとめたいと思います）。

# 対象者

* バックエンド/AndroidとKotlinのコードを共有したいフロントエンド開発者
* **Android開発者**でChrome Extensionや**PWA**の開発をKotlinで始めてみたい方 (UIはマークアップできる人と組む想定)
* 既存のフロントエンドプロジェクトにKotlinを導入してみたい開発者

などなど。サーバーサイドでNode.jsを使われる方も部分的/全面的に利用できると思います。

# 実現できること

* Kotlinを使った堅牢なマルチプラットフォーム開発
    * サーバーサイドやネイティブアプリとモデルクラスやビジネスロジックを共有
        * JSに変換したKotlinのクラス・コードを他のJSからも呼べるように
* webpackのwatchでKotlinのコードの更新に連動してビルド (ここ大事)
* **Dockerさえインストールされていたらすぐビルド可能**
    * デザイナーなど非開発者でも簡単にプレビューできるようにしたい
    * Node.js / IntelliJ IDEAなどのインストール不要！Automatorとかでシェル操作も隠蔽したら完璧:star:

Kotlinの公式とか当たってみたけどJSとかと組み合わせて `webpack --watch` できる方法については [こちらの記事](https://link.medium.com/3pxSuDCTRR) くらいしか見つからなかったのでまとめてみました。
この記事ではReactやVueといったライブラリを使った開発まではスコープに含めていません。

# ソースコード

手っ取り早く動かしてみたい方はこちらからどうぞ。
https://github.com/cubenoy22/kotlin-js-webpack-sample

# 環境構築編

npmなどは説明しません。Dockerについて不明の場合は [こちらの記事](https://qiita.com/cubenoy22/items/63b0a69a89e62b6e7c5e) を参照してください。
`Webわかんないよーという方、Dockerのインストールを済ませつつリポジトリのコードを落として次の開発編にお進みください。` WebViewでちょっとJS連携したりできるレベルの知識があれば問題ありません。

(Web開発者はNode.jsインストールするのは当然と思っているけど、Docker化することで他分野のエンジニアでもすぐにフロントエンド開発に参加できるのはとてもありがたいですね:pray:)

## プロジェクト構成

node_modules含めすべてのファイルはコンテナ側ではなくホスト側に持たせます(コンテナは使い捨て)。Dockerはいらないよという方、appの中だけで問題ありません。

* your-project
    * app ... コンテナと共有するフォルダ
        * dist ... 成果物
            * MyProduct.js ... webpackでビルドされた最終JS
        * js
            * index.js ... エントリポイント (今回は MyProductKt.js をimportするだけ)
            * MyProductKt.js ... KotlinをビルドしてできたJSファイル
        * kt
            * main.kt ... ディレクトリで階層になっていてもOK
        * webpack.config.js
  * Dockerfile
  * docker-compose.yml

### docker-compose.yml

[上記のDocker記事](https://qiita.com/cubenoy22/items/63b0a69a89e62b6e7c5e) とまったく同じ内容のため省略します

### Dockerfile

```dockerfile
# 最新LTSの公式イメージ
FROM node:10.13-alpine
# webpackとKotlinに必要なパッケージのインストール
RUN apk --update add bash openjdk8
# カレントディレクトリ
WORKDIR /app
# nodeの代わりにbashを起動
ENTRYPOINT [ "bash" ]
```

### npm init & install

一番最初だけalpine linuxイメージのダウンロードやbash/openjdk8のインストールが走るので少々時間がかかりますが、一度イメージが出来上がってしまえばすぐに起動してコンテナ内のbashに入れます。

```shell
$ docker-compose run --rm app
bash-4.4# npm init
bash-4.4# npm i -D webpack webpack-cli kotlin @jetbrains/kotlin-webpack-plugin
```

### webpack.config.js

ミニマム設定を用意しました。これでも動きますがかなり端折ったので適宜追記してください。

```javascript
const KotlinWebpackPlugin = require('@jetbrains/kotlin-webpack-plugin');

const BUILD_MODE = process.env.NODE_ENV || 'production';
// const IS_DEVEROPMENT = BUILD_MODE === 'development';

module.exports = {
	mode: BUILD_MODE,
	entry: {
		"MyProduct": __dirname + '/js/index.js'
	},
	plugins: [
		new KotlinWebpackPlugin({
			src: __dirname + '/kt',
			output: 'js',
			optimize: false,
			moduleName: 'MyProductKt', // ビルド時のJSファイル名
			moduleKind: 'commonjs',
			librariesAutoLookup: true,
			verbose: true,
		})
	]
};
```

### package.json

watchとbuildできるようにscriptsを登録しておきましょう。

```json
{
  "前": "略",
  "scripts": {
    "test": "echo ...",
    "watch": "webpack -d --watch --watch-poll --progress --env.NODE_ENV=development",
    "build": "webpack --progress"
  },
  "後": "略"
}
```

## 環境構築完了

これでwebpackを使ってKotlinのコードをビルドできるようになりました :tada: 
JSとは簡単にコラボレーションできますので必要に応じてBabelとかも入れてください。

### 開発再開、ビルドするためには

(Dockerのコンテナ立ち上げて) `npm` 呼ぶだけです。簡単。

```
$ docker-compose run --rm app
bash-4.4# npm install # 初回起動時は必須
bash-4.4# npm run watch # 開発する場合 or ↓
bash-4.4# npm run build # デプロイする場合
```

# 開発編

Kotlin/JVM では存在しない DOM 操作を体験するため、Qiita にちょっとした機能を付加する Chrome Extension を作ってみましょう:muscle:
快適な Developer Experience を体感するため、Kotlinファイルを編集する際は最後の方で紹介しているIntelliJ IDEAを使ってタイプしてみていただきたいです:bow:

```shell
bash-4.4# mkdir kt && touch kt/main.kt
bash-4.4# mkdir js && touch js/index.js
```

↑とりあえずディレクトリとファイルを作成します

### js/index.js

いちおう既存Webプロジェクトへの追加を想定して、あえてindex.jsを作りました。importすればKotlinのクラスがJSから使えたりします。

```javascript
import './MyProductKt';
```

### kt/main.kt

importは補完を確定すると自動で追加されますので、import文とコメント行以外をコピペせずタイプしてみてください。npm run watchしながら書いて保存すると都度ビルドが走りますよ。

```kotlin
import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLTextAreaElement
import org.w3c.dom.asList
import kotlin.browser.document

fun copyToClipboard(text: String) {
    (document.createElement("textarea") as HTMLTextAreaElement).apply {
        value = text
        document.body!!.append(this)
        select()
        document.execCommand("copy")
        remove()
    }
}

fun main() {
    document.getElementsByClassName("code-frame").asList().forEach {
        val codeFrame = it as HTMLElement
        codeFrame.style.position = "relative"
        (document.createElement("button") as HTMLButtonElement).apply {
            val code = codeFrame.innerText
            innerText = "Copy"
            style.apply {
                right = "0"
                top = "0"
                position = "absolute"
                margin = "4px"
            }
            onclick = {
                copyToClipboard(code)
            }
            codeFrame.appendChild(this)
        }
    }
}
```

`A.apply { method(); member = "" }` というのは `A.method(); A.member = ""` と同じです。onclickのevent引数はここでは *it* で参照可能です。

## ビルド

(環境構築編を飛ばされた方は開発編すぐ上にある「開発再開、ビルドするには」を参照してください)

```shell
bash-4.4# npm run build
```

distフォルダが出来上がりましたか？最後にChrome Extension用のマニフェストを書いておきます。

```shell
bash-4.4# touch dist/manifest.json
```

### manifest.json

```json
{
    "manifest_version": 2,
    "name": "QiitaCodeCopy",
    "version": "1.0.0",
    "description": "",
    "icons": {},
    "content_scripts": [{
        "matches": ["https://qiita.com/*/items/*"],
        "js": ["MyProduct.js"]
    }],
    "author": "YOUR_NAME_HERE"
}
```

さあ [この辺り](https://developer.chrome.com/extensions/getstarted) を参考にChrome Extensionをロードしてみてください。ディレクトリは `dist` を指定します。このページをリロードするとコードブロックの右上にCopyというボタンが現れましたね？おもむろにクリックしてエディタにペーストしてみると、ブロックの内容がコピーされているかと思います :tada:

# (おまけ) IntelliJ IDEA の支援を受ける

いまのところ快適にKotlinがかけるIDEはJetBrains社製しかないと思うので、とりあえずIntelliJ IDEAで補完が効く状態まで持っていきたいと思います。

検証は IntelliJ IDEA 2018.2.7 (Ultimate Edition) と Kotlin プラグイン (1.3.10) で行いました。

### プロジェクトの作成

Kotlin > Kotlin/JS を選びます

![new_proj.png](https://qiita-image-store.s3.amazonaws.com/0/282588/57c20cf6-5d49-30e7-a444-708da8bf71cc.png)

Project name を自由に設定します、Project locationはktフォルダにしてください。Module nameは `kt` 、 Create source root はチェックを外して手動で行います。
(注: キャプチャは相対パスになっていますが実際は絶対パスにしておいてください)

![proj_options.png](https://qiita-image-store.s3.amazonaws.com/0/282588/d95a7c26-82b6-aa88-5695-a41beecea58c.png)

プロジェクトが出来上がりますが、source root がない状態なので指定します、Fileメニュー > Project Structure... を選びます。

![module.png](https://qiita-image-store.s3.amazonaws.com/0/282588/9793b0a9-91c4-00cd-5169-b6cb220660a9.png)

Modules > kt を選んで Sources タブのルートにあるフォルダを選択し Mark as: `Sources` をクリックしてOKを押せばビルドが動くようになり /out に出力されるようになります！