- 投稿日:2019-06-22T13:31:32+09:00
Reactの基本まとめ(Hooks含む)
最終的にReactでTodoリストを構築します。
Reduxなどの状態管理ライブラリは含みません。Reactとは
Reactは、クライアントサイドでWebページのレンダリングを行うサイトSPA(Single Page Application)を構築する際に便利なライブラリーです。
SPAでは、Webページ表示後も引き続きクライアント側でコンテンツの表示非表示等の状態管理を、継続的に行う必要があります。
Reactでは、予めJSによるロジックとHTMLがセットになった部品(コンポーネント)を用意しておき、それらを組み合わせることでWebページを構成する、コンポーネントベースと呼ばれる考え方を採用しています。
Reactは内部でJSのロジックと対応するDOM要素をマッピングしており、JS側の処理に応じて自動でDOM要素を更新してくれます。
このあたりの仕組みは仮想DOMと呼ばれています。静的ページやサーバーサイドレンダリングのページでは、引き続きjQueryも便利かなと思っています。
Reactの本家サイトは、最近日本語にも対応しておりとても充実しています。
ここのチュートリアルを一通り実践することもお勧めいたします。※Node.jsとnpmの利用環境が既に整っていることを前提としています。
Node.js / npmをインストールする(for Windows)
過去に投稿したこちらの記事でも触れています。
Node.jsとExpressでローカルサーバーを構築する(1) ―Node.jsとnpmの導入―環境構築
React公式では、Create React Appというスターターキットを用意しています。
今回は、簡単にはじめられるParcelを利用して環境を構築したいと思います。インストール
React本体と、ReactとDOM要素をつなげるためのReactDOMをインストールします。
$ npm install --save react react-dom引き続き、ParcelとBabelのReact用プリセットをインストールします。
$ npm install --save-dev parcel-bundler @babel/preset-reactトランスパイラ
Reactは、通常のJavaScript構文だけで構築することも可能ですが、基本的にJSXと呼ばれる構文を利用するのが一般的です。
ブラウザに導入されているわけではないので、トランスパイラと呼ばれるツールを用いてJavaScripの構文に変換(トランスパイル)する必要があります。
このトランスパイルには一般的にBabelというツールが用いられます。公式提供スターターキットのCreateReactAppでも内部でBabelが利用されているようです。
Babel自体は、自分で色々設定が出来る自由度の高いツールであると同時に、設定が面倒なツールでもあります。
ParcelはこのBabelを内包した上で、細かな設定をせずともすぐに使えるように作られています。
プリセットは、Babelのオプション用パッケージのことを指します。
ReactのJSXのトランスパイルはオプション機能として提供されている為、別途インストールが必要です。
Parcelの場合、インストールするだけで設定は特に不要です。バンドラ
es2015にて、JavaScripにモジュール機能(ESModules)が導入されました。
用途ごとに分割された外部JSファイルを、import・export文を通して利用する仕組みです。
HTMLファイル上での読み込み順序を気にすることなく、JS側でファイルの依存関係の管理が完結します。
また、各モジュール毎にスコープが閉じているので、ファイル間での名前の衝突の心配がなくなります。既にブラウザに機能導入が進んでいるようです。
対象のJSファイルを読み込む際に<script type="module">
とすると、ESModulesの対象となります。
ただ、読み込むファイル数が増えればリクエストの回数も増えてしまうので、今のところモジュールバンドラーというツールを用いて、ある程度まとめてしまう(バンドルする)のが一般的です。モジュールバンドラーは色々ありますが、特に人気なのはwebpackでしょうか。
webpackもBabelと同様、自由度が高い分、設定の手間が多いツールです。
実は、Parcelはバンドルもしてくれます。
こちらも細かな設定は不要なので、インストールした時点でほぼほぼ環境構築が完了しています。JavaScript modules | MDN
webpackとBabelについては、過去に投稿した記事もございます。
webpackとBabelの基本を理解する(1) ―webpack編―Reactコンポーネントの基本
JSXをHTMLで出力する
ひとまず、Reactを使って何かしらのHTMLを出力してみましょう。
下記のフォルダ構成でファイルを用意します。
ファイルの拡張子はjs
以外にjsx
も使えます。root └ src ├ index.html └ index.jsx<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>React Todo</title> </head> <body> <div id="root"></div> <script src="index.jsx"></script> </body> </html>// Reactパッケージの読み込み import React from 'react'; import ReactDOM from 'react-dom'; // Reactコンポーネント class App extends React.Component { render() { return ( <h1>Hello React!</h1> ); } } // HTMLタグにReactコンポーネントを紐付ける ReactDOM.render( <App />, document.getElementById('root') );rootフォルダで下記コマンドを実行してみます。
npx parcel src/index.htmlParcelによるバンドル
初回は
./dist
フォルダが作成され、html,js,mapファイルが出力されています。
コマンドで指定したhtmlをスタート地点として、<script>
で読み込んでいるJSファイルからimport
文を頼りに芋づる式に辿って、一つのファイルにまとめていきます。
出力されたJSファイルには暫定的な名前がつけられ、HTMLファイルの方もこれを参照するように書き換わっています。
mapファイルは、元ファイルと出力後ファイルのコードの対応を示す情報です。
ブラウザのDevToolなどでデバッグする際に、元ファイルの方を参照することが出来ます。実は、Parcelはテストサーバも立ち上げてくれます。
コンソールに出力されたローカルサーバのURLにアクセスすると、出力を確認できます。
また、特に指定をしなければwatchモードで起動する為、関連ファイルを編集すると勝手にコンパイル(トランスパイルとバンドル)を実行してくれます。カスタムコンポーネント
React.Component
クラスを継承したサブクラスApp
が、ユーザ定義のReactコンポーネント(カスタムコンポーネント)です。
名前は大文字で始める必要があります。
App
クラスのrender
というメソッドにて、戻り値を定義している部分に書かれているHTMLのようなものがJSXです。
正確にはXMLに近く、空要素には必ずスラッシュ/
を入れる必要があります。ReactDOM
ReactコンポーネントとDOM要素をつなぐ役割を果たします。
上記の例では、静的メソッドrender
を利用して、<div id="root">
配下にAPPで定義したHTMLが展開されるようにしています。対話による状態変更
ボタンのクリックに応じて、文字が切り替わるようにしてみます。
class App extends React.Component { constructor(props) { super(props); this.state = { isMorning: true }; } render() { return ( <div> <button onClick={e => { this.setState( { isMorning: !this.state.isMorning } ); }} > Click </button> <h1> {this.state.isMorning ? 'Good Morning' : 'Hello'} React! </h1> </div> ); } }
button
タグをクリックする度に文字が切り替わるようになりました。
このコンポーネントは、内部で状態isMorning
を保持・管理しています。
クリックするとisMorning
の状態が切り替わり、それに応じてHTMLも再レンダリングされます。
render
メソッド内のJSXの部分が、通常のHTMLとは様子が異なってきました。
onClick
はHTMLタグのonclick
属性とは異なる、JSX側の構文です。
用途はonclick
属性と同様、HTML要素にイベントハンドラを設置する為のものです。
h1
のテキストコンテンツ部分には、JSの三項演算子が追加されました。
isMorning
の値に応じて異なる文字列を返すようにしています。つまりHTMLにJSのコードを埋め込んだものがJSXです。
埋め込み部分は波括弧{ }
で囲みます。
PHPなどでテンプレートページを作る時と要領は似ており、JSの実行結果がHTML上に描画されます。
コンテンツのデータ、そのデータの処理ロジック、描画が一通りセットになったオブジェクトがReactコンポーネントです。
このコンポーネントを適宜組み合わせることで、状況に応じたWebページを表現します。renderメソッド
サブクラスで必ず定義しなくてはならないのが、
render
メソッドです。
render
メソッドでは、コンテンツをどう描画するのかをJSXなどで定義した情報を返す必要があります。
コンテンツに変更があれば、Reactはrender
メソッドを呼び出し、定義に基づいてコンテンツに紐づくHTML要素を更新します。
この際、全体を丸ごと更新するのではなく、差分を確認して必要な部分のみ更新してくれます。※差分の確認およびDOM要素の更新は、ReactDOMが担っています。
stateとsetState
前項で「コンテンツに変更があれば」と表しましたが、
render
メソッドにの実行に関わる情報は、厳密にはコンポーネントの「状態」を表す情報です。
このコンポーネントの「状態」を表す情報は、クラスのstate
プロパティとして、コンストラクタ内で定義します。
そして、このstate
プロパティを更新する専用メソッドsetState
を利用して、コンポーネントの状態を更新します。
するとrender
メソッドが呼び出され、state
に紐づくHTML要素が再レンダリングされます。
プロパティをstate
以外の名前で定義したり、直接更新するなどのsetState
メソッド以外の手段でstate
を更新した場合、render
は呼ばれません。上記サンプルコードにてボタンがクリックされた時に行っていることは、状態の変更だけです。
this.state.isMorning
の値を変更しているだけです。
isMorning
の値に紐づいているh1
タグは、Reactが勝手に更新してくれます。
HTMLのDOM要素を直接管理する手間から開放され、JavaScript上でのデータ管理を気にかけるだけで済みます。setStateを利用したstateの更新例
setStateで渡されたstateの断片は、最終的に
this.state
にマージされます。
配列を利用する場合は一旦複製する必要があります。// stateを複数定義する this.state = { stateA: 'hoge', stateB: [ 'cat', 'dog' ] }; // 特定のstateを更新する this.setState({ stateA: 'moimoi' }); // 配列の場合は、一旦複製 const stateB = this.state.stateB.slice(); stateB.push('rabbit'); this.setState({ stateB }); // 下記では再描画が発生しない this.state.stateB.push('rabbit'); this.setState({ stateB: this.state.stateB });Reactは、更新内容を最小限にとどめる為に差分チェックをします。
setState
を実行しても、前回と値に変わりが無ければ再レンダリングされません。
その際はObject.isのアルゴリズムに基づいて比較を行います。
配列は、保存されたメモリの位置でざっくりと比較されるので、新規配列を渡す必要があります。const stateA = ['cat', 'dog']; const stateB = stateA; //stateAもstateBも同じ配列を参照している const stateC = stateA.slice(); stateB.push('rabbit'); stateC.push('rabbit'); console.log(Object.is(stateA, stateB)); // true console.log(Object.is(stateA, stateC)); // falsesetState() | React
state とライフサイクル | Reactイベント処理
イベントハンドラに無名関数を渡すのではなく、クラスのメソッドを設定する場合は以下のようになります。
Reactに限った話ではありませんが、イベントリスナーを登録する際にelement.addEventListener('click', this.someMethod)
の要領でメソッドをそのまま渡しても、コールバックで実行された関数内でのthis
の参照はそのメソッドが属するオブジェクトにはなりません。
素のJSでは、イベントを登録したDOM要素がthisになります。Reactの場合はundefined
でした。
その為、コンストラクタ内にてbindメソッドで所定のオブジェクトをthis
として参照する関数を生成して、上書きしています。class App extends React.Component { constructor(props) { super(props); this.state = { isMorning: true }; // コールバック関数内で、thisの参照がこのクラスを指すための設定 this.handleClick = this.handleClick.bind(this); } // クリック時のハンドラ handleClick(e) { this.setState( { isMorning: !this.state.isMorning } ); } render() { return ( <div> <button onClick={this.handleClick} > Click </button> <h1> {this.state.isMorning ? 'Good Morning' : 'Hello'} React! </h1> </div> ); } }無名関数を直接渡す場合は、アロー関数を利用します。
propsによるデータの受け渡し
複数のコンポーネントを組み合わせてこそのReactなので、ボタンを別のコンポーネントとして切り出してみます。
// 子コンポーネント // ボタンコンポーネント class MyButton extends React.Component { // state等の宣言をしない場合は省略可 constructor(props) { super(props); } render() { return ( <button onClick={this.props.onClick} > {this.props.text} </button> ); } } // 親コンポーネント class App extends React.Component { constructor(props) { super(props); this.state = { isMorning: true }; this.handleClick = this.handleClick.bind(this); } handleClick(e) { this.setState( { isMorning: !this.state.isMorning } ); } // <button>の代わりに<MyButton /> render() { return ( <div> {/* 文字列とイベントハンドラを渡す */} <MyButton text="Click" onClick={this.handleClick} /> <h1> {this.state.isMorning ? 'Good Morning' : 'Hello'} React! </h1> </div> ); } }JSX上で、一つのタグとしてコンポーネント名を記述します。
この際、子コンポーネントとなるコンポーネントに値を渡すことが出来ます。
受け取った側のコンポーネントは、this.props
からこの値にアクセスできます。
コンストラクタ内で受け取っているprops
がそうです。
this.props
は読み取り専用です。this.state
の様に更新することは出来ません。
this.props
もまた、更新されるとrender
メソッドが呼び出され、紐づくDOM要素が再レンダリングされます。外部から渡される読み取り専用データ
props
と、内部で管理する制御データstate
、この二つがコンポーネントのレンダリング実行に関係します。尚、Reactコンポーネントが大文字で始める必要があるのは、通常のHTMLタグと区別する為です。
<div>
はhtmlのdivタグとして認識されますが、<Div>
はReactコンポーネントと見なされます。
スコープ内に該当コンポーネントが無い場合はエラーになります。関数コンポーネント
上記ボタンコンポーネントは、内部で状態
state
を持ちません。
受け取ったprops
をJSXで展開するのみです。
この場合、クラス構文ではなく関数の構文で定義する方が好ましいです。// 一つの連想配列でpropsを受け取る function MyButton(props) { return ( <button onClick={props.onClick} > {props.text} </button> ); } // 分割代入が便利 function MyButton({ onClick, text }) { return ( <button onClick={onClick} > {text} </button> ); }
render
メソッドと同じく、JSXを返します。
Reactがインポートされているモジュール内であれば、そのままReactコンポーネントとして認識されます。Hooks
ReactのVer.16.8から、新機能Hooksが導入されました。
Hooksは、関数コンポーネントに追加できる様々な機能群です。
クラスコンポーネントでしか出来なかったことが、関数コンポーネントでも出来るようになります。
公式によると、クラスコンポーネントはトランスパイル後のソースにて、関数コンポーネントほど最適化されていないようです。
今のところクラスコンポーネントを将来的に廃止するとかそういう話にはなっていませんが、これからはシンプルな関数コンポーネントの組み合わせによる構成が推奨されているようです。ステートフック
ステートフックは関数コンポーネントにも内部状態を持たせる仕組みです。
これまで、stateを保持する場合はクラスコンポーネント、保持しない場合は関数コンポーネントと使い分けていたのが、Hooksを利用すると関数コンポーネントでもstateを持つことが出来ます。上記のクラスコンポーネント
App
をHookを利用した関数コンポーネントに書き換えてみます。import React, { useState } from 'react'; function App() { // useStateの引数はstateの初期値 const [isMorning, toggleFlag] = useState(false); return ( <div> <MyButton onClick={e => toggleFlag(!isMorning)} text="Click" /> <h1> {isMorning ? 'Good Morning' : 'Hello'} React! </h1> </div> ); }
useState
関数をインポートして利用します。
必ず、関数コンポーネント直下のスコープで実行する必要があります。
引数はstateの初期値です。
戻り値は配列です。0番目が現在のstate、1番目がstateを更新する為の関数です。分割代入で受け取っています。
それぞれ、クラスコンポーネントのthis.state
とthis.setState
と同じ役割を果たします。
初回実行時はuseState
の引数で渡した初期値をstateとして受け取ります。
2回目以降は現在のstateの値(更新用関数で更新した場合は更新後の値)を受け取ります。
必要に応じて複数のstateを用意することも出来ますし、クラスコンポーネントの様に一つのオブジェクトで管理する事もできます。function Example() { // 別々にstateを用意する const [count, setCount] = useState(0); const [animal, setAnimal] = useState(['cat']); // もしくは一つのオブジェクトで定義 // 但し、this.setStateとは異なり、古いものにマージされるのではなく置換されるので注意 const [state, setState] = useState({ count: 0, animal: ['cat'] });自己定義関数
useState関数はレンダリングの度に実行されますが、引数が有効なのは初回レンダリング時のみです。
実際に中のソースがどうなっているのかは分かりませんが、挙動としては自己定義関数が近いでしょうか。let animal = (first) => { // 初回しか実行されない処理 console.log(first); // 自らを上書き animal = () => { // 2回目以降に実行される処理 console.log('cat!'); }; }; animal('dog!'); // dog! animal('dog!'); // cat! animal('dog!'); // cat!Todoアプリ
シンプルなTodoアプリを作成します。
Todoリストを表示する
ひとまず、予め用意したTodoリストを表示するコンポーネントを作成します。
リストは配列で管理します。import React, { useState } from 'react'; import ReactDOM from 'react-dom'; // Todo項目 function Todo({ text }) { const [completed, setState] = useState(false); return ( <li onClick={e => setState(!completed)} style={{ textDecoration: completed ? 'line-through' : 'none' }} > {text} </li> ); } // Todoリスト function TodoList() { const [todos, setTodo] = useState( [ '牛乳 2本', '卵 10ヶ入 1パック', '食パン 5枚切り 1袋' ] ); // todosを基に<Todo />の配列を作成 return ( <ul> {todos.map((todo, index) => ( <Todo key={index} text={todo} /> ))} </ul> ); } ReactDOM.render( <TodoList />, document.getElementById('root') );コンポーネント
Todo
は、一つのtodo項目を表します。
完了の状態を保持し、クリックすると完了を示す打ち消し線が表示され、再度クリックすると未完了の状態になります。
JSXにてstyle属性を適用したい場合は、オブジェクト{style名:値}
を渡します。
text-decoration
の様なハイフン-
を含む名前は、ローワーキャメルケースで指定します。コンポーネント
TodoList
は、todoリストを保持します。
配列をJSX内で展開する場合はmapメソッドを利用します。
その際、key
値も指定する必要があります。
keyはReactが内部でリストの各要素を識別する為に用いる為、リスト内で一意の値でなくてはなりません。
固有のID等の値が無い場合、配列のインデックス値を適用することも可能ですが、インデックスは随時振りなおされるものなので、あまりお勧めは出来ません。Todoを追加する
Todoを自分で入力するためのコンポーネントを追加します。
function Todo({ text }) { /* 変更なし */ } // Todoを追加する function AddTodo({ addTodo }) { const [inputText, setInputText] = useState(''); return ( <form onSubmit={e => { e.preventDefault(); // 素のJSのEvent.preventDefault()と同じ if (!inputText.trim()) { return; } addTodo(inputText); setInputText(''); }} > <input type="text" value={inputText} onChange={e => setInputText(e.target.value)} /> <button type="submit">Add Todo</button> </form> ); } // Todoリスト function TodoList() { const [todos, setTodo] = useState([]); return ( <div> <AddTodo addTodo={newTodo => setTodo(todos.concat(newTodo))} /> <ul> {todos.map((todo, index) => ( <Todo key={index} text={todo} /> ))} </ul> </div> ); }ReactにてInputフォームを利用するにあたり、Input要素の状態(value属性の値)を管理する為のstateを用意しています。
入力値を受け取りtodos
を更新する関数を、propsを通してTodoList
からAddTodo
に渡しています。Ref
DOM要素に直接アクセスする方法として、Refがあります。
React.createRef()
メソッドを利用してRef
を作成します。
そのRef
を対象のReact要素のref
属性に設定すると、current
プロパティを通してDOM要素に直接アクセスすることが可能になります。function AddTodo({ addTodo }) { let textInput = React.createRef(); return ( <form onSubmit={e => { e.preventDefault(); const elInput = textInput.current; if (!elInput.value.trim()) { return; } addTodo(elInput.value); elInput.value = ''; }} > <input type="text" ref={textInput} /> <button type="submit">Add Todo</button> </form> ); }HTML要素ではなく、カスタムクラスコンポーネントの
ref
属性にRef
を適用した場合、curret
プロパティでアクセスできるのは、そのクラスのインスタンスです。
関数コンポーネントはインスタンスが存在しない為、ref
属性は利用できません。
あまり多用するとReactを利用するメリットが薄くなるので、基本的にはstateとpropsを通して管理するのが望ましいです。Todoに表示フィルタを追加する
Todoリストを「全て・完了・未完了」で切り替えられるよう、フィルター機能を追加します。
// 表示切替用フィルタ const VisibilityFilters = { SHOW_ALL: 'All', SHOW_COMPLETED: 'Completed', SHOW_ACTIVE: 'Active' }; function Todo({ text, filter }) { const [completed, setState] = useState(false); if ( (filter === VisibilityFilters.SHOW_ACTIVE && completed) || (filter === VisibilityFilters.SHOW_COMPLETED && !completed) ) { // 表示対象外の場合はnullを返す(非表示) return null; } return ( <li onClick={e => setState(!completed)} style={{ textDecoration: completed ? 'line-through' : 'none' }} > {text} </li> ); } function AddTodo({ addTodo }) { /* 変更なし */ } // フィルターボタン function Link({ active, onClick, children }) { // 予めオブジェクトで定義して展開するのもあり const params = { onClick: onClick, disabled: active, style: { marginLeft: '4px' } }; return ( <button {...params}> {children} </button> ); } // 表示の切替 function Footer({ filter, setFilter }) { const linkList = Object.values(VisibilityFilters); return ( <div> {linkList.map(myFilter => ( <Link key={myFilter} active={myFilter === filter} onClick={e => setFilter(myFilter)} > {myFilter} </Link> ))} </div> ); } // Todoリスト function TodoList() { const [todos, setTodo] = useState([]); const [filter, setFilter] = useState(VisibilityFilters.SHOW_ALL); return ( <div> <AddTodo addTodo={newTodo => setTodo(todos.concat(newTodo))} /> <ul> {todos.map((todo, index) => ( <Todo key={index} text={todo} filter={filter} /> ))} </ul> <Footer filter={filter} setFilter={setFilter} /> </div> ); }
TodoList
に、現在のフィルターを表す新たなstatefilter
を追加しました。
Todo
コンポーネント内では、フィルターと項目の完了状態が一致しない場合にnull
を返すようにしています。
null
を返すと、そのコンポーネントはレンダリングの対象から外れます。props.children
Reactコンポーネントは、空要素以外にコンテンツを閉じタグで囲って記述することも可能です。
この場合、子要素はprops.children
で取得できます。
他のReactコンポーネントを渡すことも可能で、コンポーネントのネストが深くなることを防ぐことが出来ます。function Child(props) { return <p>{props.children}</p> } function OtherChild(props) { return <span>{props.text}</span> } function Parent() { return ( <Child> <OtherChild text="HOGE" /> </Child> ); } //<p> // <span>HOGE</span> //</p>Flux
上記例では、Todoのテキストのリストは
TodoList
コンポーネントが、項目ごとの完了未完了の状態はTodo
コンポーネントが保持しています。
大きなアプリケーションでは、このstateが各コンポーネントに散らばっていることで、管理が煩雑になってしまうかもしれません。
Reactを提供するFaceBookでは、アプリケーションの状態を一元的に管理する設計手法として、Fluxという考え方を提示しています。Fluxに関しては、以前に投稿したこちらの記事がございます。
- FluxによるReactアプリの状態管理 Flux・FluxUtils編
- FluxによるReactアプリの状態管理 Redux・React-Redux編
- FluxによるReactアプリの状態管理 ReactHooks編
デベロッパーツール
公式からデバッグ用ツールが提供されています。
- 投稿日:2019-06-22T08:22:01+09:00
React + TypeScript + Webpack を既存プロジェクト(ASP.NET MVC5)に導入してみた
はじめに
「JQueryもいいけれど、そろそろReactを使ってみたいな」
そう思って挑戦してみたところ、なかなかに難儀でしたので、それをまとめてみます。「React + TypeScript + Webpack 」をASP.NET MVC5に適用する内容になっています。次にような方に役立つのではないでしょうか。
- JQueryは使ったことあるけれど、Reactを始めてみたい方
- 1から作るのはサンプルやcreate-react-app をやってはみたものの、既存プロジェクトへの適用の仕方がよく分からない方
各パッケージの役割を整理
正直なところフロントエンドがここまで複雑化しているのとは思いませんでした。
たくさん登場人物がいて、さらに誰が何を担当しているのが分かりにくいです…。そもそもなんでフロントエンドの話に、サーバーサイドである、Node.js が出てくんねん!って感じですよ。過渡期なのでしょうけど、ちょっと魔境すぎません?色々な登場人物が出てくる流れ
大まかにこういう流れだと理解しています。
1. React を使いたい
- やることは React.jsをインポートする。JQuery導入と同じ。怖くない。
2. JSXが登場
- これまでのJavaScriptはHTMLに紐づくものだったけど、WEBサイト機能が複雑になってきたし機能単位でまとめた方がいいよね。JavaScript主体のJSXで書く方が便利。
- JSXは Babel でコンパイルしないと使えない。Babel 登場。
- Babelを使うのに Node.jsが必要。Node.jsも登場。
3. TypeScriptが登場
- JavaScriptを直接書くより、最近のプログラミング言語っぽく書けるTypeScriptを使った方がいいよね。JSXじゃなくてTSXにしよう。
- TSXの導入に伴い Babel は必要なくなった。でも TSXファイルをコンパイルするのに Node.js が必要なので、こいつは残ります。
4. webpackが登場
- 機能の拡大と共にJavaScriptファイルが沢山増えてしまった。沢山あると読み込みが遅くなる。そこでwebpackです。こいつは一括で取り込めるようにまとめたJavaScriptを作成できます。ついでに圧縮(miniファイル作成)もできるよ。
- TypeScriptのコンパイルを実行したり(コンパイルそのものは ts-loaderが行う)、どこにまとめたJavaScriptを置くかも担当します。
- Webpackを使うのにもやっぱりNode.jsが必要です。
5. Reactを使う
- 4で出力した JavaScriptを読み込めば使用できます。
要はブラウザのスクリプト言語としてはJavaScriptがデファクトスタンダード。でもJavaScriptは使いづらいから、それを何とかしようと色々している結果、今現在のような複雑なことになっているのだと思います。
前提
既存プロジェクトとして、ここでは、ASP.NET MVC5とします。.NET Coreじゃなく.NET Framework 4.7.2 の方にしてみます。(その方が既存プロジェクトっぽいし、.NET Coreの方はテンプレートあるしね……)
IDEは Visual Studio 2019 Community で確認していますが、Visual Studio 2017でも大丈夫でしょう。テンプレート通りに作成(既存プロジェクトのつもり)
npmを導入
npmとは
各種パッケージを管理するものです。Visual Studioのデフォルトとしてある Nugetと同じような機能です。
同じような機能を二つ使うのはややこしいですが、Nugetに無いので仕方がありません。フロントエンドのパッケージを管理するのが npm、サーバーサイドのパッケージを管理するのが、Nugetと分けるのがよさそうです。
(※デフォルトでは NugetでJQueryやBootstrapを管理しているので、後々削除する必要があります)拡張機能のインストール
Visual Studioではデフォルトでは npm を使えないので、拡張機能の管理を呼び出します。
ここでは、NPM Task Runner と Package Installerを入れておきます。
WebPack Task Runner もあとで使うので入れておきます。
一度 Visual Studioを閉じてインストールを実行します。
npmでReactをインストール
ソリューション エクスプローラー で右クリックをして、Quick Install Packagesを選択します。
npm を選び、react のバージョンを16.8.6(現時点の最新版)を選択し、Installを実行します。同じように react-domもインストールします。
すると、プロジェクトの直下に packeage.json が作られているのが確認できます。これがパッケージを管理するファイルになります。プロジェクト管理にはなっていませんが、package-lock.jsonファイルと node_modulesフォルダも作成されていることが確認できます。node_modulesにパッケージの実体が存在します。
(参考)
package-lock.jsonについて知りたくても聞けなかったことpackage.jsonの変更
このまま必要なパッケージをnpmコマンドでインストールしていってもいいのですが、直接package.jsonを書き換えた方が早いのでそうします。
修正後のpackage.json{ "name": "myproject", "version": "1.0.0", "scripts": { "build": "webpack", "watch": "webpack -w" }, "devDependencies": { "ts-loader": "^5.4.3", "typescript": "^3.4.4", "webpack": "^4.30.0", "webpack-cli": "^3.3.1" }, "dependencies": { "@types/react": "^16.8.14", "@types/react-dom": "^16.8.4", "react": "^16.8.6", "react-dom": "^16.8.6" }, "private": true }package.jsonの中身の詳細については下記ページが勉強になります。
変更後のpackage.jsonのパッケージをインストール
Visual Studio のメニューの 表示 -> その他のウィンドウ -> タスクランナー エクスプローラーを表示します。
Defaultsでインストールを実行します。
インストール終了。
インストールが成功すると、node_modulesフォルダ内のパッケージが増えていることが確認できます。
(注釈)ここまで「あれ?」と思った方もいると思います。package.jsonを作成して、直接書き換えることが出来るならば「Package Installer」は必要なさそうと思うかもしれません。確かにそうなのですが、今後追加でインストールしたいものが出来た時に便利です。
TypeScriptの設定
新しい項目の追加で、tsconfig.json を追加します。
そして、tsconfig.json を次のように変更します。
{ "compilerOptions": { "sourceMap": true, "target": "es5", "module": "es2015", "jsx": "react", "moduleResolution": "node", "allowSyntheticDefaultImports": true, "lib": [ "es2019", "dom" ] } }tsconfig.json の中身の詳細については package.json と同じく下記ページを参考にしています。
webpackの設定
新しい項目の追加で、webpack.config.js を追加します。
そして、webpack.config.js を次のように変更します。
ここでは「entry」に設定されている "./src/main.tsx"のファイルを ./dist/main.js として変換すると設定しています。module.exports = { // モード値を production に設定すると最適化された状態で、 // development に設定するとソースマップ有効でJSファイルが出力される mode: "development", entry: "./src/main.tsx", // ファイルの出力設定 output: { // 出力ファイルのディレクトリ名 path: `${__dirname}/dist`, // 出力ファイル名 filename: "main.js" }, module: { rules: [ { // 拡張子 .ts もしくは .tsx の場合 test: /\.tsx?$/, // TypeScript をコンパイルする use: "ts-loader" } ] }, // import 文で .ts ファイルを解決するため resolve: { extensions: [".ts", ".tsx", ".js", ".json"] } };webpack.config.js の中身についてもこちらを参考に(というかコピー)しています。
TSXファイルの追加と変更
新しい項目の追加で、TypeScript JSXファイルを追加します。場所は上記 webpack.config.js で指定した./src/main.tsxです。
内容は create-react-app のHello Worldみたくしてみます。
main.tsximport React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render(<App />, document.getElementById('root'));同じくTypeScript JSXファイルを./src/App.tsx に追加します。
App.tsximport React from 'react'; const App: React.FC = () => { return ( <div className="App"> <header className="App-header"> <p> Hello, World!! </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } export default App;WebPack Task Runnerでコンパイル
WebPack Task Runnerを使って、TypeScriptのコンパイルを実行します。
コンパイルが無事に実行されると ./dist/main.js が作成されます。
以後、TSX(TypeSciprt) が変更されるたびにコンパイルするのは面倒なので、ビルドする前にWebPack Task Runnerでコンパイルが走るようにバインドしておきます。
main.jsをHTML側で呼び出す
main.jsを使うために、適当な index.cshtml を変更します。
index.cshtmlを変更@{ ViewBag.Title = "Home Page"; } <div id="root"></div> <script src="~/dist/main.js"></script>ビルドして動作確認
ビルドして、Hello World! が確認できれば無事に React が使えていることになります。
おわりに
登場人物がたくさん出てきて混乱してしまいますが、一つ一つ誰が何をやっているかを理解していければ大丈夫だと思います。色々やってますけど、最終的にはJavaScriptになるというだけです。
逆に理解しないまま「とりあえずサンプルあるし~」と進めていくと、上手く行っている時はいいのですが、トラブル発生時にどこに問題が発生しているのか分からないことになります。というか、なった。参考資料