Created
March 25, 2020 16:50
-
-
Save okunokentaro/4ca6da8a3479a26a568decd1c93507c2 to your computer and use it in GitHub Desktop.
Scala.js学習としてscalajs-reactのサンプルを読んでみた
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2015/10/11 にQiitaに投稿した記事のアーカイブです | |
--- | |
東京に引っ越した@armorik83です。 | |
引っ越してから長らくデスクが整っていなかったので、こういった記事を書く気力が無かったんだが、やっと整ったのでその勢いで。 | |
今回のテーマは、Scala.js学習の一環として[scalajs-react](https://github.com/japgolly/scalajs-react)の[サンプル](https://github.com/tastejs/todomvc/blob/master/examples/scalajs-react/src/main/scala/todomvc/Main.scala)を読んでみたというもの。JavaScriptエンジニア目線でのScalaとして扱う。学習ノートで進めた順序通りに記事も執筆しているため多少構造が散漫になっていることは了承いただきたい。 | |
# どこから読めばいいか | |
ルートはここ。 | |
- https://github.com/tastejs/todomvc/tree/24b83cdbd7d888540640bf3b64cf0f0348aa50d3/examples/scalajs-react | |
## ./project/Build.scala | |
こういうのは勘で、とりあえずビルドツールである[sbt](http://www.scala-sbt.org/)周りから。 | |
- https://github.com/tastejs/todomvc/blob/master/examples/scalajs-react/project/Build.scala | |
一個一個が何やっているかは全く追ってないが、きっとJSコンテクストでの`package.json`なんだろうなと推測して終わり。 | |
```scala | |
.enablePlugins(ScalaJSPlugin) | |
``` | |
きっとこの一行が重要。[plugins.sbt](https://github.com/tastejs/todomvc/blob/24b83cdbd7d888540640bf3b64cf0f0348aa50d3/examples/scalajs-react/project/plugins.sbt)でプラグイン宣言。 | |
## ./src/main/scala/todomvc | |
Scalaではこういう深いところにソースを置くのが作法らしい。 | |
## Main.scala | |
> https://github.com/tastejs/todomvc/blob/24b83cdbd7d888540640bf3b64cf0f0348aa50d3/examples/scalajs-react/src/main/scala/todomvc/Main.scala | |
気になるところを見ていく。 | |
### package | |
> [Main.scala#L1](https://github.com/tastejs/todomvc/blob/24b83cdbd7d888540640bf3b64cf0f0348aa50d3/examples/scalajs-react/src/main/scala/todomvc/Main.scala#L1) | |
```scala | |
package todomvc | |
``` | |
- 名前空間宣言 | |
- Javaと同じくパッケージとしてまとめる | |
--- | |
```scala | |
package foo { | |
// | |
} | |
``` | |
```scala | |
package foo | |
// | |
``` | |
この2つは同じ。 | |
--- | |
```scala | |
package foo.bar { | |
// | |
} | |
``` | |
```scala | |
package foo.bar | |
// | |
``` | |
この2つは同じ。 | |
--- | |
```scala | |
package foo { | |
package bar { | |
// | |
} | |
} | |
``` | |
```scala | |
package foo | |
package bar | |
// | |
``` | |
この2つは同じ。こう書くものは次と同じ。(thx @xuwei_k) | |
```scala | |
package foo.bar | |
import foo._ | |
``` | |
たぶん`package foo.bar`が一番使われるケース。 | |
### import | |
> [Main.scala#L3](https://github.com/tastejs/todomvc/blob/24b83cdbd7d888540640bf3b64cf0f0348aa50d3/examples/scalajs-react/src/main/scala/todomvc/Main.scala#L3) | |
```scala | |
import japgolly.scalajs.react._ | |
``` | |
JavaScript (ES2015)のように`import 変数名 from 'モジュール名文字列'`とはしない。import文末尾のものが使えるようになる。ここが`_`のときはwildcard。今回の場合、[`japgolly/scalajs/react`](https://github.com/japgolly/scalajs-react/tree/master/core/src/main/scala/japgolly/scalajs/react)の全てを指すようだ。 | |
個人的な印象としては、明示できる量の時は安易にwildcardにせず、使うものだけをその都度列挙した方が親切と感じた。おそらくチーム内で合意を取ったほうがいい。 | |
- `import org.scalajs.dom`は、きっとここ | |
- https://github.com/scala-js/scala-js-dom/blob/master/src/main/scala/org/scalajs/dom/package.scala | |
`package.scala`から`index.js`みたいな雰囲気を感じる。 | |
> メソッドや定数を定義できる「パッケージオブジェクト」とは | |
http://www.atmarkit.co.jp/ait/articles/1205/22/news134_2.html | |
### object Main extends JSApp | |
> [Main.scala#L12](https://github.com/tastejs/todomvc/blob/24b83cdbd7d888540640bf3b64cf0f0348aa50d3/examples/scalajs-react/src/main/scala/todomvc/Main.scala#L12) | |
`extends JSApp`しておくと、その`object`に宣言された`main()`がJavaScript側のエントリポイントとして呼ばれる。[以前の記事](http://qiita.com/armorik83/items/124d5b59ec0c4ce7037e#q-jsapp%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%E3%81%AA%E3%82%89%E3%81%AA%E3%81%84)で少し触れた。 | |
### main() | |
> [Main.scala#L46-L48](https://github.com/tastejs/todomvc/blob/24b83cdbd7d888540640bf3b64cf0f0348aa50d3/examples/scalajs-react/src/main/scala/todomvc/Main.scala#L46-L48) | |
```scala | |
dom.document.getElementsByClassName("todoapp")(0) | |
``` | |
この辺はJavaScriptコンテクストを彷彿とさせるが、2点注目すべき箇所がある。 | |
__文字列が'todoapp'ではなく"todoapp"__ | |
JavaScriptでは`''`と`""`は等しく、文字列としてHTMLを書く際に属性をダブルクオートで記述するための利便から普段はシングルクオートで書くことが多かったが、Scalaでは明確に区別される。 | |
ScalaにおいてシングルクオートはChar型リテラルであり、ダブルクオートが文字列(String型リテラル)となる。 | |
__index添字が`[]`ではなく`()`__ | |
```java:java | |
a[0] = 123; | |
final int e = a[0]; | |
``` | |
```scala:scala | |
a(0) = 123 | |
val e = a(0) | |
``` | |
なるほど。なおセミコロンも不要。 | |
## React.scala | |
### object React extends React | |
> [React.scala#L8-L9](https://github.com/japgolly/scalajs-react/blob/master/core/src/main/scala/japgolly/scalajs/react/React.scala#L8-L9) | |
`main()`を読むと、ここで`React.render()`となったため実装側に移る。 | |
```scala | |
object React extends React | |
trait React extends Object { | |
// | |
} | |
``` | |
という奇妙な記述が出てきたが、きっと`trait React`を`object React`として扱うという意味なんだろう。(それ以外に`extends React`の出元になりそうなもの無いし) | |
この`React.scala`だが、実装が一切ないので(そりゃそうだ、JavaScriptのライブラリなんだから)じゃあこのソースは一体何をしているかというと、型定義のように見える。TypeScriptでいう`.d.ts`だ。ということは、`.d.ts`を`.scala`に変換するやつなど誰かもう作っているだろうと思うと、[やっぱりあった](https://github.com/sjrd/scala-js-ts-importer)。クオリティは不明。 | |
型定義なのは分かったが、これが実際のJSの何と紐付いているかはどこで取れるのだろうか。 | |
> Defining JavaScript interfaces with traits | |
> http://www.scala-js.org/doc/calling-javascript.html | |
この`js.native`のようだ。たぶん「同名前空間の同プロパティと紐付く」みたいな解釈でいいはず。 | |
#### 動かして確認 | |
動かして何が格納されているか調べてみるため、`fastOptJS`で読めるJSにコンパイルする。方法は[過去の記事](http://qiita.com/armorik83/items/124d5b59ec0c4ce7037e#q-scalajs%E3%81%8C%E5%87%BA%E5%8A%9B%E3%81%99%E3%82%8Bjs%E3%81%AF%E3%81%A8%E3%81%A6%E3%82%82%E8%AA%AD%E3%82%81%E3%81%AA%E3%81%84)でまとめた。 | |
``` | |
$ sbt | |
> fastOptJS | |
``` | |
およそ47,000行のJSが出力される。atomでは開いてすぐ固まり画面がまったくスクロールしなくなったので、TextEdit.appやパフォーマンスに取り組んでいるエディタで開くといい。 | |
```js:todomvc-opt.js | |
$c_Ltodomvc_Main$.prototype.main__V = (function() { | |
$g["React"]["render"](this.router$1, $g["document"]["getElementsByClassName"]("todoapp")[0]) | |
}); | |
``` | |
およそ12,500行目付近にこのような出力がある。`$g`は`window`、そしてそこに`window.React`となる形で`$g["React"]`となっている。Scala.jsコンパイル後のJSは大半の変数名が名前空間付きなので非常に長いが、長いだけでJSとしては読めるレベルである。 | |
このReactは格納済みだが、`Browserify`も`npm i`もしていないのにどこから?と調べてみたら、先の`Build.scala`に答えがあった。 | |
```scala:Build.scala | |
def useReactJs(scope: String = "compile"): PE = | |
_.settings( | |
jsDependencies += "org.webjars" % "react" % "0.12.1" % scope / "react-with-addons.js" commonJSName "React", | |
skip in packageJSDependencies := false) | |
``` | |
sbtで依存解決しているらしい。sbtログにも次のような表示がみられる。 | |
```Terminal.app | |
[info] downloading https://repo1.maven.org/maven2/org/webjars/react/0.12.1/react-0.12.1.jar | |
``` | |
この実体はどこにダウンロードされたのか調べると、`~/.ivy2/cache/org.webjars/react/jars/react-0.12.1.jar`に居た。[WebJarsとはMavenなどのJavaエコシステムにおいてJSやCSSを解決するためのリポジトリらしく](http://qiita.com/opengl-8080/items/c8c5f787613c230a9827)、有名どころはほとんどが保守されている。AngularJSもあった。 | |
ivyは[Apache Ivy](http://ant.apache.org/ivy/)といいsbtが利用している依存解決の実装のことだそうだ。正直MavenやIvyと言われてもさっぱりだが、npmやbowerのようなものと認識している。 | |
> 2014年4月 | |
exe/dmgしか知らない人のためのインストール/パッケージ管理/ビルドの基礎知識 (3/4) | |
http://www.atmarkit.co.jp/ait/articles/1403/31/news032_3.html | |
ということで、この`react-0.12.1.jar`内に生のJSソースが入ってたため、これを解決している。 | |
[JSExportをCommonJSモジュールとして扱う方法](http://qiita.com/armorik83/items/124d5b59ec0c4ce7037e#q-jsexport%E3%82%92commonjs%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%A8%E3%81%97%E3%81%A6%E6%89%B1%E3%81%86%E3%81%AB%E3%81%AF)もあるため、Browserify側に寄せることも可能だろうと見ている。アプリケーションや開発リソースによってはWebJarsに寄せきるのもアリかもしれない。(React実装を全てScalaに寄せるとJSXとして書けないという問題が起きるので、コストと相談) | |
## CTodoList.scala | |
Reactの挙動の詳細は本稿では割愛する。ざっくり言うと、HTML内の`<section class="todoapp"></section>`にて指定のReact Componentが展開される。その指定されたComponentがここでは`CTodoList`だ。 | |
> [CTodoList.scala](https://github.com/tastejs/todomvc/blob/24b83cdbd7d888540640bf3b64cf0f0348aa50d3/examples/scalajs-react/src/main/scala/todomvc/CTodoList.scala) | |
非常に奇妙なのが、このソースに現れる`render`だ。 | |
```scala:CTodoList.scala#L59-L80 | |
def render: ReactElement = { | |
val todos = $.state.todos | |
val filteredTodos = todos filter $.props.currentFilter.accepts | |
val activeCount = todos count TodoFilter.Active.accepts | |
val completedCount = todos.length - activeCount | |
<.div( | |
<.h1("todos"), | |
<.header( | |
^.className := "header", | |
<.input( | |
^.className := "new-todo", | |
^.placeholder := "What needs to be done?", | |
^.onKeyDown ~~>? handleNewTodoKeyDown _, | |
^.autoFocus := true | |
) | |
), | |
todos.nonEmpty ?= todoList(filteredTodos, activeCount), | |
todos.nonEmpty ?= footer(activeCount, completedCount) | |
) | |
} | |
``` | |
Reactを理解していると、これがJSXに相当することは察しがつくがそれにしても奇妙な面をしている。1つずつ見ていく。 | |
### `<.div` | |
JSXの`<div>`のように見えるが、そもそもここでいう`<`は何か。追ってみるとscalajs-react側に実装があった。 | |
> [package.scala#L33-L45](https://github.com/japgolly/scalajs-react/blob/master/core/src/main/scala/japgolly/scalajs/react/vdom/package.scala#L33-L45) | |
```scala:package.scala#L33-L45 | |
object prefix_<^ extends Base { | |
@inline def < = Tags | |
@inline def ^ = Attrs | |
} | |
object svg { | |
object all extends Base with SvgTags with SvgAttrs | |
object prefix_<^ extends Base { | |
@inline def < = SvgTags | |
@inline def ^ = SvgAttrs | |
} | |
} | |
``` | |
`<`はここで宣言されている。下には`^`もいた。想像通り、タグや属性のためのものだ。JavaScriptでは変数名に使用できる記号は`$_`に限られているが、Scalaはけっこうなんでもありだ。 | |
> Scala用メソッド名の変換 | |
http://www.ne.jp/asahi/hishidama/home/tech/scala/reflect.html#h_NameTransformer | |
### `~~>?` | |
これまた奇妙だ。これは[ScalazReact.scala#L25-L39](https://github.com/japgolly/scalajs-react/blob/master/scalaz-7.1/src/main/scala/japgolly/scalajs/react/ScalazReact.scala#L25-L39)にある。 | |
```scala:ScalazReact.scala#L25-L39 | |
@inline implicit final class SzRExt_Attr(private val _a: Attr) extends AnyVal { | |
@inline final def ~~>(io: => IO[Unit]): TagMod = | |
_a --> io.unsafePerformIO() | |
@inline final def ~~>[N <: dom.Node, E <: SyntheticEvent[N]](eventHandler: E => IO[Unit]): TagMod = | |
_a.==>[N, E](eventHandler(_).unsafePerformIO()) | |
@inline final def ~~>?[T[_]](t: => T[IO[Unit]])(implicit o: Optional[T]): TagMod = | |
_a --> o.foreach(t)(_.unsafePerformIO()) // This implementation keeps the argument lazy | |
//o.fold[IO[Unit], TagMod](t, ~~>(_), EmptyTag) | |
@inline final def ~~>?[T[_], N <: dom.Node, E <: SyntheticEvent[N]](eventHandler: E => T[IO[Unit]])(implicit o: Optional[T]): TagMod = | |
_a.==>[N, E](e => o.foreach(eventHandler(e))(_.unsafePerformIO())) | |
} | |
``` | |
ここでメソッドとして宣言されているが、使うときは`^.onKeyDown ~~>? handleNewTodoKeyDown _,`のように扱われる。これを中置記法という。 | |
### `@inline` | |
`inline`については、はっきりと分からなかったのだが、おそらくC言語などのインライン展開を意味するのではないだろうか。なお`@`はScalaでは[Annotations](http://docs.scala-lang.org/tutorials/tour/annotations.html)と呼ばれる。一方JavaScriptの次期仕様としては、[Decoratorsなるものが提案されている](http://qiita.com/armorik83/items/e3a0ce67f569ddc4b432)。 | |
### `:=` | |
これについては悩んだ。sbtにもScala.jsにも登場するためどちらを使っているか追いきれなかったが、おそらくScala.js側の`def`だろう。 | |
> [TreeDSL.scala#L48-L49](https://github.com/scala-js/scala-js/blob/master/tools/shared/src/main/scala/org/scalajs/core/tools/javascript/TreeDSL.scala#L48-L49) | |
# 今回はここまで | |
Scalaと出力JS読みながら、実際に動かしながら、それぞれ見ていった。気になったところは順次潰していったが、まだまだ興味は尽きない。`implicit`や`case class`などは今後に持ち越す。 | |
最後に実際に動いているものを。 | |
> http://todomvc.com/examples/scalajs-react/#/ | |
「ああ…」としか言えん。 | |
以上。 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment