Spring WebFlux で開発した REST API を Swagger Editor から実行するために CORS を有効にする
概要
Swagger Editor の Try it out から REST API を実行できる様にするために、Spring の設定を変更して CORS を有効にする方法を説明します。但し、Spring MVC ではなく、Spring WebFlux を使う場合の方法です。コードは Kotlin で書かれています。
目次
確認環境
- Spring Boot 2.0.3
- Kotlin 1.2.50
参考情報
- オリジン間リソース共有 (CORS) - HTTP | MDN
- オリジン間リソース共有 (Cross-Origin Resource Sharing, CORS) について
- java - Enable CORS in Spring 5 Webflux? - Stack Overflow
- Spring WebFlux で CORS を有効にする方法
解説
Swagger Editor で発生するエラー
Swagger Editor の Try it out から REST API を実行すると CORS に関するエラーが発生します。ブラウザの画面の Swagger Editor には、以下のエラーが表示されます。(読みやすくなるよう、途中に改行を入れています。)
Possible cross-origin (CORS) issue? The URL origin (http://localhost:8080) does not match the page (http://editor.swagger.io). Check the server returns the correct 'Access-Control-Allow-*' headers.
Spring のデフォルト設定では CORS は無効になっており、swagger.io のドメイン(オリジン)で動作している Web アプリケーションから、REST API を実行するドメイン(オリジン)へのリソース要求は拒否されます。
CORS を有効にする方法
WebFilter を実装したクラスを作成
CORS を有効にするには以下のとおりにします。
import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.web.cors.reactive.CorsUtils import org.springframework.web.server.ServerWebExchange import org.springframework.web.server.WebFilter import org.springframework.web.server.WebFilterChain import reactor.core.publisher.Mono @Component class CorsFilter : WebFilter { private val allowedOrigin = "http://editor.swagger.io" private val allowedMethods = "GET, PUT, POST, DELETE, OPTIONS" private val allowedHeaders = "Content-Type" private val maxAge = "3600" override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> { val request = exchange.request if (CorsUtils.isCorsRequest(request)) { val response = exchange.response response.headers.apply { add("Access-Control-Allow-Origin", allowedOrigin) add("Access-Control-Allow-Methods", allowedMethods) add("Access-Control-Allow-Headers", allowedHeaders) add("Access-Control-Max-Age", maxAge) } if (request.method == HttpMethod.OPTIONS) { response.statusCode = HttpStatus.OK return Mono.empty<Void>() } } return chain.filter(exchange) } }
上記のコードでは WebFilter インターフェースを実装したクラスを作成して、Bean として登録しています。filter メソッドの処理では、request に Origin ヘッダーが設定されているとき (CorsUtils.isCorsRequest(request)
が true のとき)、response header に Access-Control-* ヘッダーを追加しています。更に、プリフライト要求の場合には OPTIONS メソッドでの request が送られるため、その場合には response body を空のままで応答します。単純要求の場合には Access-Control-* ヘッダーを追加して、次のフィルタに処理を委譲します。CORS 要求でない場合にも、次のフィルタに処理を委譲します。フィルタが委譲を繰り返し、いずれ応答します。
この WebFilter によって、異なるオリジンから Origin ヘッダーを付加して送信された要求に対して、Access-Control-* ヘッダーを付加した応答が返されます。
Configure で Bean を登録
別の方法として、以下の方法でも設定できます。
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.web.cors.reactive.CorsUtils import org.springframework.web.server.WebFilter import reactor.core.publisher.Mono @Configuration class CorsConfiguration { private final val allowedOrigin = "http://editor.swagger.io" private final val allowedMethods = "GET, PUT, POST, DELETE, OPTIONS" private final val allowedHeaders = "Content-Type" private final val maxAge = "3600" @Bean fun corsFilter(): WebFilter = WebFilter { exchange, chain -> val request = exchange.request if (CorsUtils.isCorsRequest(request)) { val response = exchange.response response.headers.apply { add("Access-Control-Allow-Origin", allowedOrigin) add("Access-Control-Allow-Methods", allowedMethods) add("Access-Control-Allow-Headers", allowedHeaders) add("Access-Control-Max-Age", maxAge) } if (request.method == HttpMethod.OPTIONS) { response.statusCode = HttpStatus.OK return@WebFilter Mono.empty<Void>() } } return@WebFilter chain.filter(exchange) } }
WebFilter の適用順をカスタマイズしたい場合には、こちらの方法よりも WebFilter を実装したクラスを作成した方が簡単に対応できます。
SSH で Python を実行したら UnicodeEncodeError になった
概要
ローカル環境で動作していた Python プログラムを Raspberry Pi にコピーして、 ローカル環境から SSH 経由でそのプログラムを実行したところ、 UnicodeEncodeError が発生しました。
確認環境
参考情報
- ロケール - ArchWiki
- LC_CTYPE
- 23.2. locale — 国際化サービス — Python 3.5.4 ドキュメント
- Python 3の各種エンコーディングについて - Qiita
解説
以下のプログラム (sample.py) を作成します。
print('\xa7')
IntelliJ IDEA の Terminal にて、Raspberry Pi Zero にコピーして、SSH で実行します。
intellij$ scp sample.py pi@raspberrypi.local: intellij$ ssh pi@raspberrypi.local python3 sample.py Traceback (most recent call last): File "sample.py", line 1, in <module> print('\xa7') UnicodeEncodeError: 'ascii' codec can't encode character '\xa7' in position 0: ordinal not in range(128)
ちなみに、macOS の Terminal にて、同様に実行してもエラーは発生しません。 また、Raspberry Pi に SSH でログインしてから実行してもエラーは発生しません。
Terminal によって違うのは、環境変数に違いがあると思ったので比較すると、言語設定に違いが見られます。 上から順に、IntelliJ IDEA の Terminal、 macOS の Terminal、 IntelliJ IDEA の Terminal で Raspberri Pi に SSH、 macOS の Terminal で Raspberry Pi に SSH。
intellij$ printenv LC_CTYPE=ja_JP.UTF-8 ...
mac$ printenv LANG=ja_JP.UTF-8 ...
intellij$ ssh pi@raspberrypi.local printenv LANG=en_GB.UTF-8 LC_CTYPE=ja_JP.UTF-8 ...
mac$ ssh pi@raspberrypi.local printenv LANG=en_GB.UTF-8 ...
ローカル環境のロケールを確認します。
intellij$ locale -a | grep UTF-8 ... ja_JP.UTF-8 ...
数が多いので、UTF-8
を文字に含む行だけを表示しています。
それでも、沢山のロケール名が表示されます。
Raspberry Pi Zero 環境のロケールを確認します。
intellij$ ssh pi@raspberrypi.local locale -a locale: Cannot set LC_CTYPE to default locale: No such file or directory C C.UTF-8 en_GB.utf8 POSIX
Raspberry Pi Zero 環境のロケールは全部で 4 つです。
LC_CTYPE をデフォルトロケールに使用できず、エラーが発生しています。
IntelliJ IDEA の Terminal では LC_CTYPE=ja_JP.UTF-8
ですが、ロケール ja_JP.UTF-8
は Raspberry Pi Zero 環境には存在していません。
ロケールの指定に問題があり UnicodeEncodeError が発生しています。
解決策の 1 つとしては、IntelliJ IDEA の Terminal で LC_CTYPE を unset する方法があります。
intellij$ unset LC_CTYPE
もしくは、適切な LC_CTYPE に設定すればよいでしょう。
kotlin-frontend-plugin のソースコードを読んで要約してみた
概要
以下の記事の続きです。以下の記事では、ソースコードの内容をクラス単位で整理しました。 クラス単位では全体像が見えづらいため、図とリストを使って全体を要約します。
目次
確認環境
- Kotlin 1.1.4
- kotlin-frontend-plugin 0.0.21
解説
タスクの依存関係
タスクの dependsOn による依存関係を整理すると以下の図のようになる。
データフロー
各タスクと一部のクラスでの処理と入出力の関係を図にすると以下のようになる。 前者は npm 関連のタスク、後者は webpack 関連のタスク。
requiredDependencies には以下の 3 つの依存関係が含まれている。
- webpack (version: *, scope: DevelopmentScope)
- webpack-dev-server (version: *, scope: DevelopmentScope)
- source-map-loader (version: *, scope: DevelopmentScope)
- kotlinFrontend.sourceMaps が true の場合のみ
入出力の関係
全てのタスクの入力と出力の関係を以下のリストに整理する。
- 環境変数
- PATH
- node コマンドのパスとして使われる
- PATH
- プロパティ
- org.kotlin.frontend.node.dir
- node コマンドのパスとして使われる
- org.kotlin.frontend.node.dir
- project
- sourceSets
- main.output.resourcesDir
- 設定している場合には、"$buildDir/package.json" と同じファイルが resourcesDir に作成される
- main.output.resourcesDir
- dependencies
- compile Kotlin/JS library
- “$buildDir/node_modules_imported” に展開される
- compile project
- “$buildDir/webpack.config.js” の resolve に使われる
- compile Kotlin/JS library
- compileKotlin2Js
- kotlinFrontend
- sourceMaps: Boolean = false
- “$buildDir/WebPackHelper.js” の sourceMap に使われる
- “$buildDir/webpack-dev-server-run.js” の sourceMap に使われる
- downloadNodeJsVersion: String = “”
- 空でなければ nodejs-download タスクを行う
- nodejs-download.version に設定される
- nodeJsMirror: String = “”
- nodejs-download.mirror に設定される
- define(name: String, value: Any?)
- “$buildDir/webpack.config.js” の defined (plugin) に使われる
- “$buildDir/.run-webpack-dev-server.txt” の exts に記録される
- sourceMaps: Boolean = false
- npm
- dependencies: MutableList
- dependency(name: String, version: String = “*”) で設定する
- “$buildDir/package.json” の dependencies に使われる
- developmentDependencies: MutableList
- devDependency(name: String, version: String = “*”) で設定する
- “$buildDir/package.json” の devDependencies に使われる
- versionReplacements: MutableList
- replaceVersion(name: String, version: String) で設定する
- dependencies: MutableList
- webpackBundle
- bundleName: String = project.name
- “$buildDir/webpack.config.js” の entry key に使われる
- sourceMapEnabled: Boolean = kotlinFrontend.sourceMaps
- “$buildDir/WebPackHelper.js” の sourceMap に使われる
- “$buildDir/webpack-dev-server-run.js” の sourceMap に使われる
- contentPath: File? = null
- “$buildDir/WebPackHelper.js” の contentPath に使われる
- “$buildDir/webpack-dev-server-run.js” の contentPath に使われる
- publicPath: String = “/”
- “$buildDir/webpack.config.js” の publicPath に使われる
- “$buildDir/WebPackHelper.js” の publicPath に使われる
- “$buildDir/webpack-dev-server-run.js” の publicPath に使われる
- port: Int = 8088
- “$buildDir/WebPackHelper.js” の port に使われる
- “$buildDir/webpack-dev-server-run.js” の port に使われる
- “$buildDir/.run-webpack-dev-server.txt” の port に記録される
- proxyUrl: String = “”
- “$buildDir/WebPackHelper.js” の proxyUrl に使われる
- “$buildDir/webpack-dev-server-run.js” の proxyUrl に使われる
- stats: String = “errors-only”
- “$buildDir/WebPackHelper.js” の stats に使われる
- “$buildDir/webpack-dev-server-run.js” の stats に使われる
- webpackConfigFile: Any? = null
- 設定されている場合、webpack-config タスクを行わない
- 設定されている場合のみ、webpack-helper タスクを行う
- “$buildDir/WebPackHelper.js” の webPackConfig に使われる
- “$buildDir/webpack-dev-server-run.js” の webPackConfig に使われる
- bundleName: String = project.name
- “$projectDir/package.json.d” 配下の JSON ファイル
- “$buildDir/package.json” にマージされる
- “$projectDir/webpack.config.d” 配下の JavaScript ファイル
- “$buildDir/webpack.config.js” の拡張スクリプトとして使われる
まとめ
kotlin-frontend-plugin を使うにあたって重要なファイルは、以下の 4 つであると思われる。
- node コマンド
- package.json
- webpack.config.js
- webpack-dev-server-run.js
kotlin-frontend-plugin では、 package.json の設定により環境を整え、 webpack.config.js の設定により JavaScript のバンドルを作成し、 webpack-dev-server-run.js の設定により webpack-dev-server を起動する。 これらを使う上で、node コマンドは必須である。
これら 4 つのファイルに関わる主要な入力は以下のようになっている。
- node コマンド
- プロパティ org.kotlin.frontend.node.dir でパスを設定
- 最優先されるパス
- 別途 node.js をインストールして使用する場合に使うとよい
- ~/.gradle/nodejs
- Gradle でインストールさせる場合のパス
- kotlinFrontend.downloadNodeJsVersion を指定することで有効になる
- 環境変数 PATH でパスを設定
- プロパティ org.kotlin.frontend.node.dir でパスを設定
- package.json
- webpack.config.js
- dependencies で compile 指定した Project が resolve に含まれる
- compileKotlin2Js.kotlinOptions.outputFile が複数の設定に使用される
- kotlinFrontend の define(name: String, value: Any?) メソッドで plugin の設定を追加する
- webpackBundle の bundleName, publicPath, webpackConfigFile
- webpackConfigFile を指定すれば、指定したファイルが使用される
- webpackConfigFile を指定しなければ、生成したファイルが使用される
- “$projectDir/webpack.config.d” 配下の JavaScript ファイルで設定を拡張する
- webpack-dev-server-run.js
- compileKotlin2Js.kotlinOptions.outputFile が bundlePath, moduleName に使用される
- kotlinFrontend の sourceMaps が source-map-loader を追加する
- webpackBundle の各設定
- スクリプトのなかでは HotModuleReplacement の設定を追加する
また、実行されるコマンドは以下のとおりであり、設定が上手くいかに場合には以下のコマンドを試すとよい。
// npm-install npm install // webpack-bundle node $buildDir/node_modules/webpack/bin/webpack.js --config $buildDir/webpack.config.js // webpack-run node $buildDir/webpack-dev-server-run.js // webpack-stop (実際には curl は使用せず URL にアクセスしている) curl http://localhost:$port/webpack/dev/server/shutdown
このあたりの関係を押さえておくと設定に迷うことが少なくなるのではないかと思う。
kotlin-frontend-plugin のソースコードを読んでみた
概要
kotlin-frontend-plugin は Kotlin/JS を用いてのフロントエンド開発を助ける Gradle プラグインです。 node.js を使うようになっており、webpack, karma などの node.js ライブラリを使った開発を助けます。 Kotlin の公式プラグインですが、現在は EAP となっています。
文書が少なく、何をどのように行っているのか分からなかったため、ソースコードを読みました。 本記事では、ソースコードから読み取った処理の要点を紹介します。 各々の関係を把握するには適していないため、別途図を作成する予定です。
目次
- 概要
- 目次
- 確認環境
- 解説
確認環境
- Kotlin 1.1.4
- kotlin-frontend-plugin 0.0.21
解説
class FrontendPlugin
org.jetbrains.kotlin.gradle.frontend.FrontendPlugin で行われている処理を書き記す。
project.extensions に kotlinFrontend (型は KotlinFrontendExtension) を追加する。
KotlinFrontendExtension で指定されている設定項目は以下のとおり。
- sourceMaps: Boolean = false
- downloadNodeJsVersion: String = “”
- nodeJsMirror: String = “”
project.tasks に以下のタスクを追加する。
- packages (group: build)
- bundle (group: build)
- run (group: run)
- stop (group: run)
- nodejs-download (型は NodeJsDownloadTask)
- kotlinFrontend.downloadNodeJsVersion が空でない場合のみ追加
- タスクの以下の項目を設定する
- version = kotlinFrontend.downloadNodeJsVersion
- mirror = kotlinFrontend.nodeJsMirror (nodeJsMirror の設定が存在する場合のみ)
タスクに依存関係を追加する。
ATask.dependsOn(BTask)
を BTask > ATask
として書くと以下になる。
nodejs-download > packages packages > compileKotlin2Js packages > compileTestKotlin2Js packages > bundle packages > run compileKotlin2Js > compileTestKotlin2Js compileKotlin2Js > bundle compileKotlin2Js > run bundle > assemble stop > clean
NpmPackageManager に対して以下を行う。
- apply(task packages)
- packages タスクを渡しているが使用されていない
- install(project)
- buildFinished & not failure & taskGraph is null の場合のみ実行される
kotlinFrontend extension で設定した *Bundle に対応する Bundler に対して以下を行う。 Bundler は webpack (WebPackBundler) か rollup (RollupBundler) のいずれか、もしくは両方。 * 部分は webpack か rollup を指定する。
- apply(project, NpmPackageManager, task bundle, task run, task stop)
Launcher に対して以下を行う。 Launcher は WebPackLauncher, KtorLauncher, KarmaLauncher の 3 つ。
- apply(NpmPackageManager, project, task run, task stop)
sourceSets.main.output に compileKotlin2Js.kotlinOptions.outputFile を登録する)。 但し、compileKotlin2Js.kotlinOptions.outputFile が存在するときのみ。
以降では、NpmPackageManager、Bundler から 1 つ (WebPackBundler)、Launcher から 1 つ (WebPackLauncher) 、 関連しているタスクについての調査結果を書く。 (全て調べるのは大変なので)
nodejs-download: NodeJsDownloadTask
~/.gradle/nodejs
ディレクトリに node.js をダウンロードする。
node 実行ファイルのパスを “$buildDir/nodePath.txt” に書き込む。
class NpmPackageManager
org.jetbrains.kotlin.gradle.frontend.npm.NpmPackageManager で行われている処理を書き記す。
apply
project.extensions に npm (型は NpmExtension) を追加する。
NpmExtension で指定されている設定項目は以下のとおり。
- dependencies: MutableList
- developmentDependencies: MutableList
- versionReplacements: MutableList
設定するためのメソッドは以下のとおり。
- dependency(name: String, version: String = “*”)
- devDependency(name: String, version: String = “*”)
- replaceVersion(name: String, version: String)
以下のいずれかの条件に該当する場合のみ、後に示す様々な処理を行う。
条件:
- npm.dependencies が空ではない
- npm.developmentDependencies が空ではない
- “$projectDir/package.json.d” ディレクトリが存在する
- requiredDependencies が空ではない
- require メソッドで設定される
処理:
compile.dependencies に NpmDependenciesTask.results のディレクトリを追加する。 (AbstractFileCollection として追加されており、具体的なディレクトリは遅延評価されると思われる。)
project.tasks に以下のタスクを追加する。
- npm-preunpack (型は UnpackGradleDependenciesTask)
- タスクの以下の項目を設定する
- dependenciesProvider = requiredDependencies (を返す関数)
- タスクの以下の項目を設定する
- npm-configure (group: NPM, 型は GeneratePackagesJsonTask)
- タスクの以下の項目を設定する
- dependenciesProvider = requiredDependencies (を返す関数)
- packageJsonFile = “$buildDir/package.json”
- npmrcFile = “$buildDir/.npmrc”
- タスクの以下の項目を設定する
- npm-install (group: NPM, 型は NpmInstallTask)
- タスクの以下の項目を設定する
- packageJsonFile = “$buildDir/package.json”
- タスクの以下の項目を設定する
- npm-index (型は NpmIndexTask)
- npm-deps (型は NpmDependenciesTask)
- npm (group: NPM)
タスクに依存関係を追加する。
nodejs-download > npm-configure npm-preunpack > npm-configure npm-configure > npm-install npm-install > npm-index npm-index > npm-deps npm-deps > npm npm > packages
install
以下のタスクのうち、state が EXECUTED, SKIPPED, UP-TO-DATE ではないタスクの execute() を呼ぶ。
- UnpackGradleDependenciesTask (= npm-preunpack)
- GeneratePackagesJsonTask (= npm-configure)
- NpmInstallTask (= npm-install)
- NpmIndexTask (= npm-index)
- NpmDependenciesTask (= npm-deps)
npm-* タスク
npm-preunpack: UnpackGradleDependenciesTask
npm.dependencies、npm.developmentDependencies、dependenciesProvider の返す Dependency、 いずれかの依存関係が存在するときのみ以下を行う。
compile configuration (build.gradle の compile 指定の依存設定) のうち、 Kotlin Java Script Library を “$buildDir/node_modules_imported” ディレクトリに展開する。 展開する際に package.json を生成する。
resultNames (型は MutableList
npm-configure: GeneratePackagesJsonTask
npm.dependencies、npm.developmentDependencies、dependenciesProvider の返す Dependency、 いずれかの依存関係が存在するときのみ以下を行う。
“$buildDir/package.json” ファイルを生成する。package.json の内容は以下のとおり。
- name
- moduleNames が 1 つならば、その名前
- moduleNames は compileKotlin2Js.kotlinOptions.outputFile のファイル名 (拡張子除く) のリスト
- 1 つでなければ、project.name
- いずれの名前も使えなければ、noname
- moduleNames が 1 つならば、その名前
- version
- project.version
- main
- moduleNames が 1 つならば、その名前
- 1 つでなければ、null
- dependencies
- 以下の 3 つを合わせた内容
- npm.dependencies
- resultNames (npm-preunpack タスクの結果) から生成された Dependency (RuntimeScope に設定)
- dependenciesProvider が生成した Dependency (RuntimeScope のみ)
- 以下の 3 つを合わせた内容
- devDependencies
- 以下の 2 つを合わせた内容
- npm.developmentDependencies
- dependenciesProvider が生成した Dependency (DevelopmentScope のみ)
- 以下の 2 つを合わせた内容
- 他
“$buildDir/.npmrc” ファイルを生成する。.npmrc の内容は以下のとおり。
progress=false # cache-min=3600
sourceSets.main.output.resourcesDir が設定されている場合には、 “${sourceSets.main.output.resourcesDir}/package.json” にも書き込む。
npm-install: NpmInstallTask
npm install
を実行する。
使用する npm コマンドは以下の順番で検索される。
- org.kotlin.frontend.node.dir プロパティで指定したパス
- “$buildDir/nodePath.txt” (NodeJsDownloadTask.nodePathTextFile) のパス
- 環境変数 PATH で指定したパス
npm-index: NpmIndexTask
“$buildDir/.modules.with.kotlin.txt” と “$buildDir/.modules.with.types.txt” を生成する。
“$buildDir/node_modules” のファイルを検査して、モジュールの絶対パスを .modules.with.*.txt ファイルに保存する。 kotlin と types それぞれで、以下に該当するファイルを探し、[module] の絶対パスを保存する。
- kotlin
- [module].jar
- [module]/META-INF/*.kotlin_module
- [module]/*.meta.js (但し、
// Kotlin.kotlin_module_metadata
で始まる)
- types
npm-deps: NpmDependenciesTask
“$buildDir/.modules.with.kotlin.txt” に記載されているディレクトリを results に読み込む。 results は NpmPackageManager の apply メソッドで使用される。
object WebPackBundler
apply(project, NpmPackageManager, task bundle, task run, task stop) で行われている処理を書き記す。
NpmPackageManager.require により以下のモジュール依存関係を追加する。
- webpack (version: *, scope: DevelopmentScope)
- webpack-dev-server (version: *, scope: DevelopmentScope)
- source-map-loader (version: *, scope: DevelopmentScope)
- kotlinFrontend.sourceMaps が true の場合のみ
project.tasks に以下のタスクを追加する。
- webpack-config (型は GenerateWebPackConfigTask)
- webpack-helper (型は GenerateWebpackHelperTask)
- webpack-bundle (groupd: webpack, 型は WebPackBundleTask)
タスクに依存関係を追加する。
webpack-config > webpack-helper webpack-config > webpack-bundle webpack-helper > webpack-bundle RelativizeSourceMapTask > webpack-bundle webpack-bundle > bundle
webpack-* タスク
webpack-config: GenerateWebPackConfigTask
kotlinFrontend extension で WebPackExtension が設定されており、 WebPackExtension.webpackConfigFile が設定されていない場合のみ以降の処理を行う。
以下に示す “$buildDir/webpack.config.js” を生成する。
<Part A|B|C>
は後述する内容が挿入される。
'use strict'; var webpack = require('webpack'); var config = <Part A>; var defined = <Part B>; config.plugins.push(new webpack.DefinePlugin(defined)); module.exports = config; <Part C>
Part A には以下の内容を持つ JSON が挿入される。
- context: compileKotlin2Js.kotlinOptions.outputFile の親ディレクトリ
- entry: 以下の内容を持つ JSON
- WebPackExtension.bundleName: “./${compileKotlin2Js.kotlinOptions.outputFile (拡張子を除く)}”
- output: 以下の内容を持つ JSON
- path: “$buildDir/bundle”
- filename: “[name].bundle.js”
- chunkFilename: “[id].bundle.js”
- publicPath: WebPackExtension.publicPath
- module: { rules: [ ] }
- resolve: { modules: <resolveRoots> }
- plugins: [ ]
Part B には kotlinFrontend extension の define(name: String, value: Any?) メソッドで設定された内容を持つ JSON が挿入される。
Part C には “$projectDir/webpack.config.d” 配下のファイルの内容が以下の形式で挿入される。 it はファイルを指す。 挿入順序はファイル名により決定される (拡張子前の数字で整列、数字がなければ 0 扱い)
// from file ${it.path} <ファイルの内容>
WebPackExtension の設定
kotlinFrontend extension には、bundle メソッドと allBundles メソッドが存在する。 いずれも BundleConfig を設定するメソッドである。
- bundle(id: String, configure: BundleConfig.() -> Unit)
- id: Bundler の種別 (webpack, rollup)
- id で指定した Bundler の設定を行う
- allBundles(block: BundleConfig.() -> Unit)
- 全ての Bundler の共通設定を行う
BundleConfig のインスタンスは WebPackBundler クラスで生成される WebPackExtension インスタンスである。 WebPackExtension クラスは BundleConfig インターフェースを実装している。
kotlinFrontend extension にて webpackBundle { ... }
を実行すると bundle("webpack") { ... }
が実行される。
WebPackExtension で設定できる項目は以下のとおり。
- bundleName: String = project.name
- sourceMapEnabled: Boolean = project.kotlinFrontend.sourceMaps
- contentPath: File? = null
- publicPath: String = “/”
- port: Int = 8088
- proxyUrl: String = “”
- stats: String = “errors-only”
- webpackConfigFile: Any? = null
webpack-helper: GenerateWebpackHelperTask
WebPackExtension.webpackConfigFile が設定されている場合のみ以降の処理を行う。
“$buildDir/WebPackHelper.js” を生成する。内容は以下のとおり。
module.exports = <JSON>
<JSON> には以下の内容を持つ JSON が挿入される。
- port: WebPackExtension.port
- shutDownPath: “/webpack/dev/server/shutdown” (WebPackRunTask.ShutDownPath)
- webPackConfig: WebPackExtension.webpackConfigFile
- contentPath: WebPackExtension.contentPath
- proxyUrl: WebPackExtension.proxyUrl (空ならば null に設定される)
- publicPath: WebPackExtension.publicPath
- sourceMap: kotlinFrontend.sourceMaps && WebPackExtension.sourceMapEnabled
- stats: WebPackExtension.stats
- bundlePath: compileKotlin2Js.kotlinOptions.outputFile
- moduleName: compileKotlin2Js.kotlinOptions.outputFile のファイル名 (拡張子を除く)
webpack-bundle: WebPackBundleTask
node $buildDir/node_modules/webpack/bin/webpack.js --config $buildDir/webpack.config.js
を実行する。
WebPackExtension.webpackConfigFile が設定されている場合は、"$buildDir/webpack.config.js" は設定したファイルが指定される。
object WebPackLauncher
apply(NpmPackageManager, project, task run, task stop) で行われている処理を書き記す。
project.tasks に以下のタスクを追加する。
- webpack-run (group: webpack, 型は WebPackRunTask)
- タスクの以下の項目を設定する
- start = true
- タスクの以下の項目を設定する
- webpack-stop (group: webpack, 型は WebPackRunTask)
- タスクの以下の項目を設定する
- start = false
- タスクの以下の項目を設定する
タスクに依存関係を追加する。
webpack-config > webpack-run RelativizeSourceMapTask > webpack-run webpack-run > run webpack-stop > stop
webpack-run: WebPackRunTask
node $buildDir/webpack-dev-server-run.js
を実行する。
実行する際に、起動するサーバーの情報を JSON 形式で “$buildDir/.run-webpack-dev-server.txt” に保存する。
“$buildDir/webpack-dev-server-run.js” はサーバー起動前に生成される。
kotlin-frontend-plugin 中の resource である kotlin/webpack/webpack-dev-server-launcher.js をテンプレートとして、
テンプレート中の require('$RunConfig$')
を GenerateWebpackHelperTask で生成している JSON に置き換える。
但し、JSON 中の内容において、WebPackExtension.webpackConfigFile が設定されていない場合は “$buildDir/webpack.config.js” が使用される。
サーバーの起動に成功すると “$buildDir/.run-webpack-dev-server.txt” が生成される。 以下の内容を持つ JSON ファイルである。
- port: WebPackExtension.port
- exts: kotlinFrontend extension の define メソッドで設定した内容
- hashes: 以下の内容を持つ JSON
webpack-stop: WebPackRunTask
http://localhost:$port/webpack/dev/server/shutdown
にリクエストを送信する。
$port
は “$buildDir/.run-webpack-dev-server.txt” が保持している port。
ts2kt を Gradle で実行する
概要
TypeScript の型定義を Kotlin ファイルに変換する Node.js スクリプトである ts2kt を Gradle で実行できるようにします。
2017/8/31 現在、ts2kt は使える状態ではありません。 生成された Kotlin ファイルでは多数のエラーが発生します。
目次
確認環境
- IntelliJ IDEA Community 2017.2
- Kotlin 1.1.4-2
- Gradle 3.5
- Groovy 2.4.10
参考情報
- gradle-node-plugin/docs at master · srs/gradle-node-plugin · GitHub
- gradle-node-plugin の文書
- GitHub - Kotlin/ts2kt: Converter of TypeScript definition files to Kotlin declarations (stubs)
- ts2kt の文書
解説
例として Vue.js の型情報 (*.d.ts
) を Kotlin ファイル (*.kt
) に変換する。
手順としては以下の通り。
- node をプロジェクト用にダウンロード
- npm を使い ts2kt と vue をインストール
- ts2kt で
*.d.ts
を*.kt
に変換
まずは build.gradle
を以下の通りに記述する。
buildscript { ext.gradle_node_version = '1.2.0' repositories { jcenter() maven { // gradle-node-plugin url "https://plugins.gradle.org/m2/" } } dependencies { classpath "com.moowork.gradle:gradle-node-plugin:$gradle_node_version" } } apply plugin: 'com.moowork.node' node { // node をプロジェクト用にダウンロード download = true } task ts2kt(type: NodeTask) { String nodeDir = "${project.projectDir}/node_modules" script = file("$nodeDir/ts2kt/ts2kt.js") String destDir = 'src/main/kotlin/org/vuejs' file(destDir).mkdir() args = ['-d', destDir] + file("$nodeDir/vue/types").listFiles().collect { it.absolutePath } }
npm の設定として pakage.json
を以下の通りに記述する。
{ "name": "Vue.kt", "version": "2.4.2", "description": "Vue.kt that is Vue.js in Kotlin", "devDependencies": { "ts2kt": "0.0.14", "vue": "2.4.2" } }
設定が完了したら以下のコマンドを実行する。
$ ./gradlew npmInstall ts2kt
src/main/kotlin/org/vuejs
ディレクトリに Kotlin ファイルが生成される。
例えば vue.kt
の一部。
external interface `T$0` { @nativeInvoke operator fun invoke(): VNode @nativeInvoke operator fun invoke(tag: String, children: VNodeChildren): VNode @nativeInvoke operator fun invoke(tag: String, data: VNodeData? = definedExternally /* null */, children: VNodeChildren? = definedExternally /* null */): VNode @nativeInvoke operator fun invoke(tag: Component, children: VNodeChildren): VNode @nativeInvoke operator fun invoke(tag: Component, data: VNodeData? = definedExternally /* null */, children: VNodeChildren? = definedExternally /* null */): VNode @nativeInvoke operator fun invoke(tag: AsyncComponent, children: VNodeChildren): VNode @nativeInvoke operator fun invoke(tag: AsyncComponent, data: VNodeData? = definedExternally /* null */, children: VNodeChildren? = definedExternally /* null */): VNode } ... <snip> ... open fun <T> `$watch`(expOrFn: (this: Vue /* this */) -> T, callback: WatchHandler<Vue /* this */, T>, options: WatchOptions? = definedExternally /* null */): () -> Unit = definedExternally ... <snip> ...
元は vue.d.ts
のこの部分。
export type CreateElement = { // empty node (): VNode; // element or component name (tag: string, children: VNodeChildren): VNode; (tag: string, data?: VNodeData, children?: VNodeChildren): VNode; // component constructor or options (tag: Component, children: VNodeChildren): VNode; (tag: Component, data?: VNodeData, children?: VNodeChildren): VNode; // async component (tag: AsyncComponent, children: VNodeChildren): VNode; (tag: AsyncComponent, data?: VNodeData, children?: VNodeChildren): VNode; } ... <snip> ... $watch<T>( expOrFn: (this: this) => T, callback: WatchHandler<this, T>, options?: WatchOptions ): (() => void); ... <snip> ...
生成された Kotlin ファイルにはいくつか問題がある。
T$0
という名前が同一パッケージ内で複数定義されることになり名前が衝突する。@nativeInvoke
は DeprecatedVNodeChildren
,Component
,AsyncComponent
などの型が定義できていない- Union Type に対応できていないため型を定義できない
- パラメータ名に
this
(Kotlin のキーワード) が使われている
Union Type に関する議論はされている様子。
既存の JavaScript ライブラリを使うのであれば ts2kt による型情報を補足が欲しいところ。
Kotlin/JS TIPS - this の扱い
概要
JavaScript の関数中の this は関数を保持しているオブジェクトによって決定されます。 この振る舞いが Kotlin/JS を使う上での問題になります。 this を扱ういくつかの方法を説明します。 最適な方法とは言えないので、より良い方法があれば教えてください。
ソースコードは前回の記事の続きになっています。 詳しくは前回の記事を参照してください。
目次
確認環境
- IntelliJ IDEA Community 2017.2
- Kotlin 1.1.3-2
- Gradle 3.5
- Groovy 1.4.10
参考情報
- 算出プロパティとウォッチャ - Vue.js
- Vue.js の算出プロパティ
解説
this の問題点
以下の JavaScript のコードは 算出プロパティとウォッチャ - Vue.js から引用したコードに少しだけ手を加えたコードである。firstName
と lastName
に値を設定すると fullName
が算出されるサンプルである。
var vm = new Vue({ el: '#example', data: { firstName: 'Foo', lastName: 'Bar' }, computed: { fullName: function () { return this.firstName + ' ' + this.lastName } } }) vm.firstName = 'Taro' vm.lastName = 'Yamada'
この処理を Kotlin/JS で書いてみる。SampleClient.kt
を以下に変更する。
@JsNonModule @JsModule("vue") external open class Vue(option: VueOption) external interface VueOption : Json { var el: Any var data: Json var computed: Json } fun <T : Json> Json(): T = js("({})") fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init) fun Vue(init: VueOption.() -> Unit): Vue = Vue(Json<VueOption>().apply(init)) external interface Model : Json { var firstName: String var lastName: String } fun run() { val vm: dynamic = Vue { el = "#example" data = Json<Model> { firstName = "Foo" lastName = "Bar" } computed = Json { // this は Json クラスのインスタンス this["fullName"] = { // this は Vue クラスのインスタンス this as Model // firstName, lastName を参照したいのでキャスト "${this.firstName} ${this.lastName}" } } } vm.firstName = "Taro" vm.lastName = "Yamada" }
sample-client/index.html
には以下を記述して、ブラウザで開いてみる。
<div id="example">{{ fullName }}</div>
ブラウザに表示される内容は undefined undefined
である。
残念ながら上記のコードではやりたいことは行えない。
${this.firstName} ${this.lastName}
の this
に問題がある。
JavaScript と Kotlin での this の違い
先の例では、JavaScript の関数中の this
は関数を保持しているオブジェクトを示している。
実行時に決定されるので、this
は Vue クラスのインスタンスを示すことになる。
一方 Kotlin の例では、Kotlin のラムダ中の this
はそのラムダの外側のスコープの this
になっている。
つまり、Json クラスのインスタンスを示すことになる。
レシーバ付き関数リテラル
実行時に this
を決定したいのであれば、レシーバ付き関数リテラルを使用する。
SampleClient.kt
を以下に変更する。
fun run() { val vm: dynamic = Vue { el = "#example" data = Json<Model> { firstName = "Foo" lastName = "Bar" } computed = Json { // this は Json クラスのインスタンス this["fullName"] = fun Model.(): String { // this は実行時に決定される return "${this.firstName} ${this.lastName}" } } } vm.firstName = "Taro" vm.lastName = "Yamada" }
external との併用における問題点
external interface
を使うことで、firstName =
, lastName =
の様な簡潔な記述を実現している。
fullName
も同様に簡潔な記述にするために以下のコードを追加したくなる。
external interface Computed : Json { var fullName: Model.() -> String }
残念ながらコンパイルエラーとなる。
external
宣言中ではレシーバ付き関数型は禁止されているとエラーメッセージが表示される。
(禁止している理由を知っている方がいたら教えてください。)
external との併用
レシーバ付き関数リテラルを使えば this
の扱いを動的に変更できるが、
external
宣言と併せて使うことができない。
external
宣言を使う場合にはレシーバなしの関数リテラルにする必要がある。
例えば、以下のとおり。
external interface Computed : Json { var fullName: () -> String }
そうすると最初の問題に戻ってしまう。
回避するための苦肉の策として js("this")
を使う方法がある。
コード生成時に JavaScript の this
をそのまま出力させる。
以下がその例である。
inline fun <T> thisAs(): T = js("this") fun run() { val vm: dynamic = Vue { el = "#example" data = Json<Model> { firstName = "Foo" lastName = "Bar" } computed = Json<Computed> { // this は Json クラスのインスタンス fullName = { val self = thisAs<Model>() // self は実行時に決定される "${self.firstName} ${self.lastName}" } } } vm.firstName = "Taro" vm.lastName = "Yamada" }
inline
を付けないと thisAs
関数が生成され、thisAs
関数を保持しているオブジェクトが存在しないために thisAs
関数の結果は undefined
になる。
ソースコード
Kotlin/JS TIPS - JavaScript ライブラリへの型情報の付与
概要
JavaScript ライブラリを使用する際に、型情報を付与する方法について説明します。 前回の記事で JavaScript ライブラリの使用方法を説明しましたが、 静的型付けによる恩恵を得ずに JavaScript の柔軟性を重視した使用方法としていました。 型情報を付与することによって、以下の恩恵を得られる様にしていきます。
ライブラリ開発者の手間を増やして、ライブラリ利用者の手間を減らしていきます。
前回の記事:
目次
確認環境
- IntelliJ IDEA Community 2017.2
- Kotlin 1.1.3-2
- Gradle 3.5
- Groovy 1.4.10
参考情報
- kotlin.js - Kotlin Programming Language
- Kotlin/JS 標準ライブラリ
- Calling JavaScript from Kotlin - Kotlin Programming Language
external interface
の使い方
解説
json 関数と Json インターフェース
JavaScript では {}
によってオブジェクト生成を行う。
Kotlin/JS では json 関数によってオブジェクトを生成できる。
生成されたオブジェクトは Json インターフェース を実装したオブジェクトであり、
[]
による操作を行える。
以下は SampleClient.kt
の一部である。
fun run() { val option = json() option["el"] = "#example" option["data"] = json("message" to "Hello World") }
生成される JavaScript は以下の様になっている。
function run() { var option = json([]); option['el'] = '#example'; option['data'] = json([to('message', 'Hello World')]); }
json 関数のソースコード (Kotlin) は以下になっており、js("({})")
でオブジェクトを生成していることがわかる。
public fun json(vararg pairs: Pair<String, Any?>): Json { val res: dynamic = js("({})") for ((name, value) in pairs) { res[name] = value } return res }
また、Json インターフェースのソースコード (Kotlin) は以下になっている。
public external interface Json { operator fun get(propertyName: String): Any? operator fun set(propertyName: String, value: Any?): Unit }
external
を使うことで JavaScript のコードをそのまま使用することができる。
get/set
を定義することで []
による参照/代入を可能にしている。
型情報の付与
json 関数の仕組みを応用して以下のとおりに記述することで、Json コンストラクタに見せかけることができる。
fun Json(): Json = js("({})") fun run() { val option = Json() option["el"] = "#example" }
さらに、external
を使うことで JavaScript のコードをそのまま使用できる性質を利用して、
[]
を使わずにオブジェクトに値を設定できる様にする。
例えば、以下の様に。
fun <T : Json> Json(): T = js("({})") external interface VueOption : Json { var el: Any var data: Json } external interface Model : Json { var message: String } fun run() { val option = Json<VueOption>() option.el = "#example" option.data = Json<Model>().apply { message = "Hello World" } option["computed"] = Json() // VueOption は Json インターフェースを実装しているので [] を使える }
生成された JavaScript は以下のとおり。
external
を使用した部分は簡潔なコードとなっている。
コード上に VueOption
や Model
インターフェースは現れない。
function Json() { return {}; } function run() { var option = Json(); option.el = '#example'; var $receiver = Json(); $receiver.message = 'Hello World'; option.data = $receiver; option['computed'] = Json(); }
さらに、初期化処理を行う関数を引数 (init
) にとる Json
関数を追加することで、
()
や apply
を省略できる。
fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init) fun run() { val option = Json<VueOption> { el = "#example" data = Json<Model> { message = "Hello World" } this["computed"] = Json() }
external interface
を使うことで型情報を付与し、
補完機能の恩恵を得えつつ、
誤った型の値を代入する危険性を減らすことができる。
Vue.js に適用する
さらに、Vue
関数を SampleClient.kt
に追加すると、Vue の初期化が簡潔になる。
@JsNonModule @JsModule("vue") external class Vue(option: VueOption) fun <T : Json> Json(): T = js("({})") fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init) external interface VueOption : Json { var el: Any var data: Json } external interface Model : Json { var message: String } fun Vue(init: VueOption.() -> Unit): Vue = Vue(Json<VueOption>().apply(init)) fun run() { val vm: dynamic = Vue { el = "#example" data = Json<Model> { message = "Hello World" } } vm.message = "Hello Kotlin World" }
前回の記事と比べると随分と簡潔になったと思う。
しかし、検討すべき課題は残っている。
補完機能の恩恵は受けられるようになったが、型安全という面では十分とは言えない。
上記の例で言えば、el
は String
もしくは HTMLElement
であるため、Any
にしている。
また、vm.message
を参照するために dynamic
にしている。
JavaScript の動的な側面を扱う方法については検討が必要である。