create-react-app(version=react-scripts-ts) で生成されたコードを読む
追記: 2019-10-20
なんかGoogle検索からこのエントリにきてくれる人がそこそこいるっぽいんですが、かなり前のVersionの時に書いたエントリなのでそこまで参考にしないでください。
現在はcreate-react-app 自体でTypeScript対応がされていてそちらが一般的なため、この内容とはおそらく大幅に内容が違います。
npx create-react-app my-app --scripts-version=react-scripts-ts
— はぜ (@haze_it_ac) October 27, 2018
Microsoftが create-react-app のTypeScriptバージョンを提供しているらしく、それを使って React + TypeScript でアプリケーションを作る練習をしようとした。
んだけど、どういう手順で作って行くのかがいまいちよくわからなかったので、とりあえず生成された中身を読んでみることした。
create-react-app の本体リポジトリにもTypeScriptのサポートが入るみたいで試してみたけど、上手く動かなかったのでやめた。
(型の確認まではできるようになったけど、 ソースコードは .js のままで、全部書き換えないといけなかった。それはそれでTypeScriptの勉強になりそうだからそのうちやりたい。)
レベル感
自分は、Node.js を使った外のAPIを叩いたりJSONを投げるアプリケーションは作ったことはあるけど、フロントエンドに関しての実装はしたことがない状態。
jQueryも知らないし、画面描画に関わるJavaScriptのコードはほぼ全く読めないと言ってもいい感じ。
Reactは2回本の写経をしたけど、あんまりちゃんと理解していない。コンポーネントのpropsとstateの使い分けは本に書いてあったから雰囲気でわかる、と思っていたけど完全に忘れた。
TypeScriptは名前しか知らなくて、型付きのJavaScriptにコンパイルしてくれる便利なやつ という認識。
本の写経したとき雰囲気だけはわかったけど、細かい仕様はわかってない。
ソースコードを読む前にやったこと
前提となるES2015〜のJavaScriptの文法を確認する
初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発 を軽く読む。
jQueryの話、基本的なデータ構造周りの話は飛ばして、ちゃんと理解しているか怪しい部分だけさらった。
具体的には、 ジェネレータ( function * )、class、prototype周り。
TypeScriptの基本的な部分を理解する
Qiitaを読んだりした。
Typeのことがよくわからなかったが、Qiita: TypeScriptのInterfaceとType aliasの比較 で雰囲気は理解したと思う。
init-appをつくる
$ npx create-react-app init-app --scripts-version=react-scripts-ts $ cd init-app $ npm run eject
これができる。
npm run eject は、create-react-app側で良い感じにまとめられていたパッケージを分解する処理を走らせる。
eject前は、 package.jsonのscripts欄は
"scripts": {
"start": "react-scripts-ts start",
"build": "react-scripts-ts build",
"test": "react-scripts-ts test --env=jsdom",
"eject": "react-scripts-ts eject"
},
こうなっていて、ejectをすると
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js --env=jsdom"
},
こうなる。
react-scripts-ts という便利コマンドが作られていて、それを解除するという感じ。
ちなみに、 start を実行をしたときに走る scripts/start.js は以下。
create-react-app-ts-init/start.js at master · haze-it/create-react-app-ts-init · GitHub
初期状態では混乱しないようにパッケージングされていて見えなくなっている、ということ。
(そもそもejectをする前は /scripts ディレクトリが存在しない)
中身
読んでいきましょう。
起動 - scripts/start.js
起動コマンドは npm run start か、 yarn start 。

さっきも書いた通り、 start を実行すると、 scripts/start.js が実行される。
create-react-app-ts-init/start.js at master · haze-it/create-react-app-ts-init · GitHub
まず、14行目で config/env.js が読み込まれている。

config/env.js はこれ。
create-react-app-ts-init/env.js at master · haze-it/create-react-app-ts-init · GitHub
env.js の中では、キャッシュを消したり、 NODE_ENV が存在するか確認したり、 .dotenv ファイルを読み込んだり、 NODE_PATH を設定したりしている。
その後は、色んなモジュールを読み込んで

起動するためのホスト(通常は 0.0.0.0 )とポート(通常は 3000 )を指定している。
(この下に書かれているものは Cloud9内で起動した場合に実行される内容なので飛ばす)

そして、63行目の choosePort でポートを取得する。

この choosePort メソッドは react-dev-utils/WebpackDevServerUtils から持ってきているもので、「指定のポートが空いていたら獲得し、空いていなかったらユーザに次のポートを取りに行くかを確認する」機能を持つ。
試しに3000番ポートを埋めた(別Appを起動させた)状態で npm run start をすると、以下のようなエラーメッセージが出て、 y とかエンターを押すと 3001番ポートを取りに行ってくれる。

npm - react-dev-utils: WebpackDevServerUtils
便利。
choosePort が成功したら、HTTP or HTTPS、Appの名前を確認して、URLを設定して、

webpackで各モジュールをバンドル(複数のファイルをまとめる)して、

proxyの設定を package.json から取ってきて

起動!!+ブラウザを開きます。

その下には Ctrl+C 押したりした時に終了する処理が書かれている。

というのが start.js の処理。この辺を全部勝手に用意してくれる create-react-app便利だなーという感じ。
src/index.tsx
ここからReact。
最初に開かれるアプリケーションは src/index.tsx 。
これは paths.js の appIndexJs という定数部分に記述されていて、 webpack.config.[env].js の entry でそれを最初に実行するように設定されている。
(webpackの仕様)
path.js

webpack.config.dev.js

src/index.tsx
create-react-app-ts-init/index.tsx at master · haze-it/create-react-app-ts-init · GitHub
短め。
react , react-dom モジュールをimportして、
./App.tsx , ./index.css , ./registerServiceWorker.tsx を取ってくる。
App が実際のアプリケーションで、 index.css は普通のライブラリで、 registerServiceWorker.tsx は名の通りServiceWorkerの部分。
行なっているメインの処理はこれだけ。
ReactのDOMに App.tsx の中身 と index.html 内にある <div id="root"></div> を投げている。

div root が HTMLElement にキャストされている。 HTMLElement は任意のHTML要素を表すものらしい。
document.getElementById() は普通のJSのやつ。
Document.getElementById() - Web API | MDN
そのあとにServiceWorkerを走らせているけど、これは後述。
App.tsx
create-react-app-ts-init/App.tsx at master · haze-it/create-react-app-ts-init · GitHub
メインの処理部分。
react モジュールをimportして、cssとlogoを取る。

Reactはコンポーネントという単位でUIを分けていて、その定義は、 React.Component を継承することで実現する。( extends React.Component )
初期状態だとコンポーネントは一つしかないので、この App コンポーネントが画面全てを描画している。

一般的なReactアプリケーションだと、entryにルーティング用のtsxをかませて、それでコンポーネントを出し分けたりするっぽい。
training-react-app/webpack.config.js at master · haze-it/training-react-app · GitHub
training-react-app/Routes.tsx at master · haze-it/training-react-app · GitHub
くるくる回るロゴは普通にCSS animationだった。(なんかのReactの機能を使ってると思ったら全然そんなことなかった)

メイン処理は以上。
registerServiceWorker.ts
Service Workerってなんだ。雰囲気しか知らない。
ブラウザ上の描画とは別に、スクリプトが実行できる環境が用意されていて、そこでプッシュ通知の処理をしたり、バックで処理走らせたりができるもの。
Webページ側の処理とは全く別のものなので、バックグラウンドで同期処理を走らせたりしても描画に影響はないみたい。便利そう。
registerServiceWorker.ts を読んでいく。
前半の事前処理で多用されている window.location について 。表示しているURLの情報とかを取ってこれるっぽい。
Window.location - Web API | MDN
envがproductionじゃないと登録処理がそもそも行われないようになっている。

後ろの 'serviceWorker' in navigator は ブラウザが対応しているかを確認する処理らしい。
Navigator.serviceWorker - Web API | MDN
これは、動くworkerが別ドメインのものでないかを確認している。セキュリティ対策かな?
CDNを使ったりする場合はそのURLを入れたりする処理も必要になるっぽい。

で、メインの登録処理と実行処理。

swURL は、 実行するスクリプトのURLを入れている。
初期状態だとローカルホストでしか動かないようになっていて、 checkValidServiceWorker(swUrl: string) で、別のアプリのservice workerが登録されていないかを確認している。
( checkValidServiceWorker は下の方で定義されている)
navigator.serviceWorker.ready.then(() => { .... } でserviceworkerを登録する。
直接DOMを編集できないので、 postMessage という機能を使って送り込む処理が必要らしい。その辺は別の記事を読んでください。
App.test.tsx
create-react-app-ts-init/App.test.tsx at master · haze-it/create-react-app-ts-init · GitHub
create-react-app-ts-init/README.md at master · haze-it/create-react-app-ts-init · GitHub
Testing-Components というものがあるらしい。
テストは追々で。
というわけで、一部雑に飛ばしたけど、メインの機能周りはこれでおしまい。
package.jsonとかtsconfigとか、configのpolyfill(ブラウザで対応していない機能を独自で実装するやつ)とかjest(テスト)辺りは後々深く見ていこうと思う。
とりあえず、次はイメージする画面を作る練習をいくつかする予定。