本文的最終目的是編寫出能調用 Webpack 來施行構建流程的 Kotlin Build Script。
前言
因為惱於 JavaScript 的無類型,又不喜歡 TypeScript 的類型系統,於是就有了嘗試用 Kotlin 開發前端的嘗試。
不過一番搜尋和嘗試後,我放棄了。兩個類型系統相差懸殊,Kotlin 依舊保留了大量的 JVM 平台行為,各種容器和類型的包裝並不能輕易地和前端開發的習慣匹配。
但我都研究了這麼多了,不拿來玩一下豈不就浪費掉了嘛。於是就把目光投向了每次都惹我惱的 Webpack 配置。(我真的討厭配 Webpack)
環境說明
本文涉及 NPM、Javascript、Kotlin 以及 Bash 終端的運用。所有需要的指令都在專案目錄下局部安裝。
運行環境為 macOS,Linux 應該沒問題。Kotlin 的編寫使用的是 IDEA Ultimate。Webpack 版本 5。
使用 npm init
創建一個專案目錄後進入,然後開始操作。
如何編譯 Kotlin 程式碼?
編譯 Kotlin 程式碼需要使用 kotlinc-js
指令。
1 | npm i --save-dev kotlin |
kotlin-js
的使用很簡單:
1 | kotlin-js [程式碼目錄] -libraries [程式庫目录] -output [最终 JavaScript 输出檔] |
程式庫目錄是可選的,默認會包含 Kotlin stdlib 本身。如果僅使用 stdlib 則無須使用。
最終輸出是一個單一 JavaScript 檔案。直接執行即可。
kotlin-js
還有一些值得注意的選項:
-module-kind
:JavaScript 模塊類型使用,支援plain | amd | commonjs | umd
四個選項。我個人建議只使用commonjs
選項。其他選項構建出來的結果不適合單獨執行。-main
:是否調用main()
函數。選項為call | noCall
,即調用或不調用,默認為調用。在寫程式庫時可能需要用到noCall
。-meta-info
:輸出 Kotlin 元數據。方便其他 Kotlin JS 程序引入使用。
更多選項可執行 kotlinc-js -help
查看。
Kotlin 程式庫依賴
Kotlin stdlib 是自帶的。Webpack 運行於 Node.js,需要 Node JS 的 Kotlin API。雖然可以按需使用 extrenal
關鍵詞聲明原生 JavaScript 實現,但更方便的是使用 Kotlin 官方做好的聲明。
嘗試在 IDEA 上新建一個 Kotlin JS 專案後,找到了對應的依賴 org.jetbrains.kotlinx:kotlinx-nodejs:0.0.7
,但可惜 JCenter 已經 sunset 了,我沒找到 Gradle 到底是從哪裡下載這個包的,在其 GitHub 專案首頁也沒有對應的下載連結,我暫時把它放在我們的倉庫中,如有需要可以臨時使用:https://nexus.shinonometn.com/repository/maven-public/
(本人不對服務質量作保證)
雖說 stdlib 是自帶的,但我還是建議下載 org.jetbrains.kotlin:kotlin-stdlib-js:1.6.21
並將其加入 Project Library。
配置關鍵詞提醒
使用 IDEA 打開專案目錄,把下載好的 kotlinx-nodejs
jar 包增加至 Project Library 即可獲得關鍵詞提醒功能。
為了目錄的乾淨整潔,我新建了 buildSrc
目錄放置所有 Webpack 構建用程式。將其添加至專案的 Source Root 後,Kotlin 關鍵詞提醒將正常工作。
Javascript 程式庫依賴
1 | npm i --save-dev webpack webpack-merge webpack-dev-server html-webpack-plugin copy-webpack-plugin mini-css-extract-plugin chalk@4 ora@1.2.0 rimraf |
Webpack 是必須的了。chalk、ora 和 rimraf 用於展示如何從 Kotlin 調用 JavaScript 功能,也為了順便製造點 eye candy(XD
chalk 和 ora 必須使用 commonjs module 的版本,否則不能從 kotlin 調用。
開始寫代碼
由於程式碼數量不少,這裡僅節選關鍵點,完整專案可訪問 GitHub 鏈接。
main 入口
在 buildSrc 內的 package level main 將會成為整個程式的入口,且其只能聲明一次。可以是 suspend function
1 | import process |
注意,這裡的 main 沒有 args。是可以加進去的,但只會得到一個空數組。像普通的 Node 程式,命令行参表需要從 process.argv
獲取。在引入 kotlinx-nodejs
后,所有的 Node API 都可以像 JVM 上的包那样被引入。
访问原始 JavaScript 内容
1 | fun jsObject(): dynamic = js("({})") |
在 Kotlin 程式碼中調用 JavaScript 函數,需使用 js(String)
函數。此函數會把內容內聯進當前位置,返回 dynamic
類型。
dynamic
類型是一個比任意類型還任意類型的類型,代表著一個原始 JavaScript 存在。可以對此類型變數作任何操作:
- 賦予變數:例如 a 為 dynamic 類型,
a.b = "1"; a.b = 2; a.b = suspend { }; a.b = Unit
… 都是合法的 - 取變數
- 調用:例如 a 為 dynamic 類型,
a(); a(string1, value2, option3); a(...arrayOf('a', 'b', 'c'))
… 都是合法的,返回值會是dynamic
類型。
dynamic
類型也是危險的,因為等同於臨時關閉幾乎所有類型檢查,而且 undefined | null
也是以 dynamic
的形式返回,我們不能過度依賴它,只能在與 JavaScript 交互時使用。
同時需要注意的是: 輸入的字符串不能是變數,必須是編譯時靜態的,任何動態的字符串拼接與變數的使用會觸發編譯時錯誤。 但我們可以這樣:
1 | val a = "Hello World" |
還需要注意的是,此用法需在同一作用域內使用,否則 a 在編譯後的 JavaScript 裏可能會被帶上作用域後綴(最常見的是作用域深度後綴,例如_0
)引起運行時錯誤:
1 | val webpack = js("require('webpack')") |
使用這種方法,我們可以創建已有 JavaScript 庫的包裝(wrapper):
1 | class OraSpinner(text : String) { |
聲明原始 JavaScript API
除了以上直接調用 JavaScript 的方法,還能夠使用 @JsModule
注解配合 external
关键词
1 |
|
以 chalk
为例,若已知其方法聲明,則可以直接原樣翻譯進 Kotlin 中,然後在外部調用這個聲明。
suspend function 與 Promise 的互相轉換
調用 webpack 或 webpack-dev-server 的 api 是會遇到 Promise 與 suspend function 的互相轉換問題。Kotlin JS 中自帶 coroutine,但其實現依舊是原汁原味的 Kotlin Coroutine。
關於這部分內容可參考我的另一篇文章:Kotlin/JS Promise 與 Coroutine 的互相轉換
JavaScript 類型實現與 Kotlin 類型實現的轉換
在 Webpack 配置中,最常用的除了標量類型(String、Number、Boolean 等)就是集合類型(Array 和 Map)。但 Kotlin 中的 List 和 Map 的實現都不是使用 JavaScript 的 Array 和 Object,Regex 是個 wrapper,在使用的時候就需要編寫點轉換函數了。
1 |
|
調用 Webpack 和 Webpack Dev Server
webpack 函數吃一個 webpack 配置和一個回調
1 | private val webpack = js("require('webpack')") |
webpack-dev-server 的本體在 webpack-dev-server/lib/Server.js
。它吃一個 compiler (Webpack)和 options(配置),返回一個 Promise<void>
,把它引進來就可以了:
1 | val webpack = js("require('webpack')") |
package.json 的配置
我們已經使用代碼來調用 webpack 和 webpack dev server 了,package.json
的入口就也得改成 Kotlin JS 構建後的輸出。我這裡使用 kotlin_build/buildscript/buildscript.js
作為輸出,那麼 package.json
就得這麼改了:
1 | { |
編譯 Kotlin 程式碼,包含它的依賴。
編譯之前還需要做一件事情。
kotlinc-js
並不能讀取 jar ,我們需要把它們解壓出來。需要用到的只有 kotlinx-nodejs
,那麼就把它解壓到一個地方去,例如 kotlin_build/bulidscript/lib
1 | unzip ./lib/kotlinx-nodejs-0.7.0.jar -d kotlin_build/bulidscript/lib |
這時候就可以調用 kotlinc-js
編譯我們的 buildscript 了:
1 | kotlinc-js ./buildSrc -module-kind commonjs -main call -source-map -libraries ./kotlin_build/bulidscript/lib -output ./kotlin_build/buildscript/buildscript.js |
每次都手动调用构建是一件很麻烦的事情,我们可以写个脚本来自动化这些事情:
1 |
|
寫好之後執行這個腳本,serve 或者 build 就自動啦~
後記
實際做這個東西花了好幾天,更多的還是卡在理解 Webpack 那神奇的配置上。快做好的時候才發現原來我參考的 webpack 配置已經是很老的版本了,於是對著新版本重新修整了一番。現在的 Webpack 配置比以前舊版本的要好,做完這個 DSL 之後其實效率一般般,編譯 buildscrip 也要花一定的時間,而且強弱類型系統之間的差距導致給 Webpack 寫 Kotlin DSL 是一件很燒事件燒腦袋的事情。
我嘗試過用 dukat
工具來生成 Webpack API。結果是失敗了,dukat
要不 property not found 要不 stack overflow 讓我失望得很,所以只好乖乖手寫。
在 JavaScript 的代碼中引入 Kotlin 包內聲明,需要按照像 Java 那樣的包結構定位聲明位置。Kotlin 的代碼編譯後都被閉包起來封在局部,除非主動修改外圍環境,否則聲明內容不會洩漏。
完整產物要比文章內的功能多,shell 檔中包含了從 maven 倉庫下載依賴的過程,所以會更複雜,可以在 GitHub 上查看原始碼。
只是個能用的玩具,沒有打算深入開發,所以 DSL 不完整也不夠友好,如果繼續有想法的話或許會改進它。