TypescriptでReactのハンズオンプロジェクトを作ってみた
以前ES6でReactのハンズオンを作成し、今回はTypescriptが触ってみたかったのでTypescriptでReactのハンズオンプロジェクトを作ってみました。
まだまだTypescriptに慣れていないので型を指定するところではanyでやり過ごす部分が多々あったので、この辺りは触りながら都度修正していけたらと思います。
TypescriptでReactのプロジェクトを作ってみた
公式の手順に従いプロジェクト作成
公式の手順に従ってTypescriptでReactのプロジェクトを作成してみたいと思います。
まずプロジェクトルートのディレクトリをさくせしnpm initを実行します。
それからwebpackをグローバルインストールします。
npm install -g webpack
次に必要になるモジュールをインストールします。
npm install --save react react-dom @types/react @types/react-dom npm install --save-dev typescript awesome-typescript-loader source-map-loader
次にプロジェクトルートに以下の"tsconfig.json"を作成します。
{ "compilerOptions": { "outDir": "./dist/", "sourceMap": true, "noImplicitAny": true, "module": "commonjs", "target": "es5", "jsx": "react" }, "include": [ "./src/**/*" ] }
それから"src/components/Hello.tsx"を作成します。
import * as React from "react"; export interface HelloProps { compiler: string; framework: string; } export const Hello = (props: HelloProps) => <h1>Hello from {props.compiler} and {props.framework}!</h1>;
次に"src/index.tsx"を作成します。
import * as React from "react"; import * as ReactDOM from "react-dom"; import { Hello } from "./components/Hello"; ReactDOM.render( <Hello compiler="TypeScript" framework="React" />, document.getElementById("example") );
次に"index.html"を作成します。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Hello React!</title> </head> <body> <div id="example"></div> <!-- Dependencies --> <script src="./node_modules/react/dist/react.js"></script> <script src="./node_modules/react-dom/dist/react-dom.js"></script> <!-- Main --> <script src="./dist/bundle.js"></script> </body> </html>
ビルドの設定に必要になる"webpack.config.js"を作成します。
module.exports = { entry: "./src/index.tsx", output: { filename: "bundle.js", path: __dirname + "/dist" }, // Enable sourcemaps for debugging webpack's output. devtool: "source-map", resolve: { // Add '.ts' and '.tsx' as resolvable extensions. extensions: [".ts", ".tsx", ".js", ".json"] }, module: { rules: [ // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. { test: /\.tsx?$/, loader: "awesome-typescript-loader" }, // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. { enforce: "pre", test: /\.js$/, loader: "source-map-loader" } ] }, // When importing a module whose path matches one of the following, just // assume a corresponding global variable exists and use that instead. // This is important because it allows us to avoid bundling all of our // dependencies, which allows browsers to cache those libraries between builds. externals: { "react": "React", "react-dom": "ReactDOM", } };
ここまで出来たらプロジェクトルートで以下のwebpackコマンドを実行したらdistにビルドしたものが出力されます。
webpack
動きを確認する場合はプロジェクトルートで以下のコマンドを実行しプロジェクトのルートをドキュメントルートとした上でサーバを起動した上で"http://8000/index.html"にアクセスすることで確認できます。
python -m SimpleHTTPServer
vue-cliからプロジェクト作成
vue-cliのインストール
npm install -g vue-cli
プロジェクト作成
vue init webpack react_typescript_starter_project cd react_typescript_starter_project npm install
一旦起動してみる
プロジェクト作成直後はvueのプロジェクトになっておりますが、とりあえず以下のコマンドでwebpackを起動して動作を確認してみたいと思います。
npm run dev
上記コマンド実行後にブラウザが開いてvue-jsのアイコンが表示された画面が表示されたかと思います。
vue-cliで作成したプロジェクトをreact化する
それではreactで開発できるようにしていきたいと思います。まずは必要なモジュールをインストールします
npm install --save react react-dom @types/react @types/react-dom npm install --save react-redux redux @types/react-redux @types/redux npm install --save-dev typescript awesome-typescript-loader source-map-loader
tsconfig.jsonを作ります。
{ "compilerOptions": { "outDir": "./dist/", "sourceMap": true, "noImplicitAny": true, "module": "commonjs", "target": "es5", "jsx": "react" }, "include": [ "./src/**/*" ] }
“build/webpack.base.conf.js"にTypescriptの情報も追加します。
var path = require('path') var utils = require('./utils') var config = require('../config') var vueLoaderConfig = require('./vue-loader.conf') function resolve (dir) { return path.join(__dirname, '..', dir) } module.exports = { entry: { //app: './src/main.js' app: './src/index.tsx' }, output: { path: config.build.assetsRoot, filename: '[name].js', publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath }, resolve: { extensions: ['.ts', '.tsx', '.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src') } }, module: { rules: [ { test: /\.(js|vue)$/, loader: 'eslint-loader', enforce: 'pre', include: [resolve('src'), resolve('test')], options: { formatter: require('eslint-friendly-formatter') } }, // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. { test: /\.tsx?$/, loader: "awesome-typescript-loader" }, { test: /\.vue$/, loader: 'vue-loader', options: vueLoaderConfig }, // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. { enforce: "pre", test: /\.js$/, loader: "source-map-loader" },/* { test: /\.js$/, loader: 'babel-loader', include: [resolve('src'), resolve('test')] },*/ { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('fonts/[name].[hash:7].[ext]') } } ] } }
それから"src/components/Hello.tsx"を以下の内容で作成します。
import * as React from "react"; export interface HelloProps { compiler: string; framework: string; } export const Hello = (props: HelloProps) => <h1>Hello from {props.compiler} and {props.framework}!!</h1>;
次に"src/index.tsx"を作成します。
import * as React from "react"; import * as ReactDOM from "react-dom"; import { Hello } from "./components/Hello"; ReactDOM.render( <Hello compiler="TypeScript" framework="React" />, document.getElementById("example") );
最後に"index.html"を以下の内容で作成したら完成です。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Hello React!</title> </head> <body> <div id="example"></div> </body> </html>
これで"npm run dev"を実行すると実際に動いているのが確認できると思います。それから"npm run build"を行った後にdistディレクトリで"python -m SimpleHTTPServer"を実行してみるとビルドがうまくいくことも確認できます。webpackの設定周り詳しくなくてもこれならなんとかなりそうな気がします。
今回vue-cliで作成したプロジェクトは以下にあげておきました。 https://github.com/teruuuuuu/react_typescript_starter_project
React事始め
以前vue-cliで作成したプロジェクトをreact化してみたので、それを使って基本的なことはできるようにしておきたいと思います。
最初のコンポーネントを追加する
まずは表示だけしてみる
vue-cliで作成したプロジェクトをreact化した続きから進めていきたいと思います。まず最初のコンポーネントを追加してみたいと思います。 ‘src/components/first-component.js'を追加して以下の内容にしてください。
import React, { Component } from 'react'; export default class FirstComponent extends Component { constructor(props) { super(props) } render() { return ( <div> <h1>Hello, React!</h1> </div> ); } }
特に値を受け取って表示するわけでもない上記コンポーネントをまずは表示できるようにしてみたいと思います。'main.js'を以下の内容にして'npm run dev'で動作が確認できます。
import React from 'react' import { render } from 'react-dom' import FirstComponent from './components/first-component' render( <FirstComponent /> , document.getElementById('app') );
stateの値を表示してみる
今度はstateの値を表示できるようにしてみたいと思います。Reactではstateに似た概念としてpropsがありますがこれは親から受け取るプロパティはpropsにセットされ自分自身のコンポーネントで使う値はstateにセットするというもので、コンポーネント初期化時に呼び出されるconstructor(props)で初期化を行います。
import React, { Component } from 'react'; export default class FirstComponent extends Component { constructor(props) { super(props); this.state = { copyText: '', }; } textFromInput(e) { this.setState({ copyText: e.target.value }); } render() { return ( <div> <h1>Hello, React!</h1> <input name="a" type="text" placeholder="from text" onChange = { this.textFromInput.bind(this) } /><br /> <input name="a" type="text" hintText="to text" value = { this.state.copyText } readOnly="readonly" /><br /> </div> ); } }
propsで引き継いだ値を子コンポーネントで利用する
まずpropTypesの宣言周りで必要になるのでbabelにpresetプラグインを有効化します。現在0から1,2,3と機能が異なるpresetが用意されていて具体的にどれを使えばよくわかっていないのですが、vue-cliでプロジェクトを作成した時に入っていたpreset-2を指定しておきたいと思います。.babelrcを以下のように修正します。
{ "presets": [ "es2015", "react", "stage-2"] }
それから、親からの値を受け取る側のコンポーネントを準備します。以下の"src/components/child-component.js"を作成します。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class ChildComponent extends Component { static propTypes = { copyText: PropTypes.string.isRequired } static defaultProps = { copyText: 'init val' } constructor(props) { super(props); } render() { const { copyText } = this.props; return ( <div> <label>{ copyText }</label> </div> ); } }
次に値を受け渡す側"src/components/first-componet.js"を以下のように修正します。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ChildComponent from './child-component'; export default class FirstComponent extends Component { static propTypes = { copyText: PropTypes.string.isRequired } static defaultProps = { copyText: 'copy text' } constructor(props) { super(props); this.state = { copyText: 'copy text', }; } textFromInput(e) { this.setState({ copyText: e.target.value }); } render() { const { copyText } = this.state; return ( <div> <h1>Hello, React!</h1> <input name="a" type="text" placeholder="from text" onChange = { this.textFromInput.bind(this) } /><br /> <input name="a" type="text" placeholder="to text" value = { this.state.copyText } readOnly="readonly" /><br /> <ChildComponent copyText={ copyText }/> </div> ); } }
renderの呼び出しで新しく作成したChildComponentコンポーネントのプロパティ'copyText'にテキスト入力した値が入るようになっています。ChildComponenは受け取ったプロパティをラベルでそのまま出すようにしておりまして動かしてみるとそれが確認できるかと思います。
ちなみに今回新しく追加したChildComponentは親コンポーネントのrenderメソッドで直接タグを記入していましたが、以下のようなコンポーネントを返すメソッドを用意しておいてrender内で呼び出すようにすることでも同様の動きになります。
renderItem(copyText){ return ( <ChildComponent copyText={ copyText }/> ) }
呼び出しサンプル render() { const { copyText } = this.state; return ( <div> <h1>Hello, React!</h1> <input name="a" type="text" placeholder="from text" onChange = { this.textFromInput.bind(this) } /><br /> <input name="a" type="text" placeholder="to text" value = { this.state.copyText } readOnly="readonly" /><br /> {this.renderItem(copyText)} </div> ); }
表示する内容を条件によって切り替える必要があるけどコンポーネントを分けるまでもない場合で、renderメソッド内をごちゃごちゃさせたくないという場合があるのでしたらこの方が良い場合もあるかもしれませんが、ルールを決めておかないと逆にわかりづらくなりそうなので注意が必要です。この辺りが柔軟そうなのは助かりそうではあります。
react-eduxを利用する
Reduxでコンポーネンの値を変更してみる
先ほどは親コンポーネントのプロパティを直接子コンポーネントに渡して連携していました。今度はReduxのフレームワークを使用してコンポーネント間の連携を行っていきたいと思いまして、reactであればreact-reduxというモジュールが公式から出ているのでこちらを利用したいと思います。
まずReduxについての簡単な概要ですが、single-page-applicationの誕生により以前よりも多くの状態を管理する必要が出てきていましてFluxというフレームワーク
ではデータの流れを一方通行にしてしまうことで、例えばコンポーネント間でのデータのやりとりでそれぞれのコンポーネントが実データを更新する処理を行っているのであれば管理しづらくなるので
それを打開するため状態を更新する場合は共通のアクションを呼び出すなどしてデータを一方通行にするといった方法が出てきています。ReduxというのはFluexの実装の一つという位置づけのようなもので
厳密なFluxよりかはReactから扱いやすいように変更が加えられたものとなっています。
npm install –save react-redux npm install –save redux-thunk npm install babel-plugin-transform-decorators-legacy –save-dev
react-redux, redux-thunkについてはreact-redux関連のモジュールと想像がつくと思います。babel-plugin-transform-decorators-legacyについてはreduxが生成するstateをコンポーネントに関連付けるのに使用する@connetアノテーションで必要になります。
まず.babelrcを修正してbabel-plugin-transform-decorators-legacyを有効にするようにしたいと思います。
{ "presets": [ "es2015", "react", "stage-2"], "plugins": ["transform-decorators-legacy"] }
次に"src/define/action/sample-action-define.js"に今回追加するアクションの定数定義を追加します。
export const CHANGE_TEXT = 'CHANGE_TEXT'
“src/reducers/sample-reducer.js"にstate更新に使用するreducerを追加します。これはコンポーネント側がアクションを呼び出して来た場合にここでstateの更新を行います。
import { CHANGE_TEXT } from '../define/action/sample-action-define' const initialState = { text : 'init text' }; export default function sampleReducer(state = initialState, action) { switch (action.type) { case CHANGE_TEXT: return Object.assign({}, state, { text: action.text}) default: return state } }
それから"src/reducers/index.js"にreducerをマージするためのメソッドを追加します。今回使用するReducerは一つだけなのであまり恩恵を感じないですが、複数のReducerが必要になる場合はこういったようにマージするメソッドがあった方が良さそうです。
import { combineReducers } from 'redux' import sampleReducer from './sample-reducer' const rootReducer = combineReducers({ sampleReducer }) export default rootReducer
Reducerを呼び出すアクションを"src/actions/sample-action.js"に追加します。アクション自体はコンポーネントがディスパッチという関数を使うことで呼び出すことができます。
import * as types from '../define/action/sample-action-define' export function change_text(text) { return { type: types.CHANGE_TEXT, text: text } }
stateを管理する大元であるstoreを"src/store/store-config.js"に作成します。applyMiddleware後にfinalCreateStoreやっているところでstoreを生成しています。applyMiddlewareについてはreactにおけるミドルウェアの機能を使う時に必要になるもので例えばログ出力や他サーバにリクエストを投げる場合などに利用されます。今回はミドルウェアを使用することもないのでapplyMiddlewareを使わずにcreateStoreだけでも良いはずですが、後からミドルウェアを使うことを想定し先にこの書き方にしておきます。module.hotの判定式の内部ではreduxを使う場合にホットリロードを行うための設定となっておりまして、これがない場合は開発車モードで起動中にファイルを保存するたびにstateが初期化されるのを防ぐためにあった方が良さそうなものです。
import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' import rootReducer from '../reducers' export default function StoreConfig(preloadedState) { const finalCreateStore = applyMiddleware(thunk)(createStore); const store = finalCreateStore(rootReducer, preloadedState); if (module.hot) { // Enable Webpack hot module replacement for reducers module.hot.accept('../reducers', () => { const nextReducer = require('../reducers').default store.replaceReducer(nextReducer) }) } return store }
Reactにreduxのstateを私て扱えるようにする。"src/main.js"が以下になるようにします。ここではreact-reduxモジュールのProviderコンポーネントにstoreを渡しています。
import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import FirstComponent from './components/first-component' import StoreConfig from './store/store-config' const store = StoreConfig() render( <Provider store={store}> <FirstComponent /> </Provider > , document.getElementById('app') );
それではReduxのstateを利用する側である"src/components/first-component.js"を以下のように修正します。mapStateToPropsにはstateの値が、mapDispatchToPropsにはディスパッチするために必要となるメソッドが格納されています。@connect(mapStateToProps, mapDispatchToProps)のアノテーションでreduxのstoreを渡しています。propTypesのプロパティと、textに変更があった時に呼び出すメソッドの情報が入っていてRenderメソッドではこれを使用して描画を行っています。またテキスト入力を行った際に呼び出されるtextFromInput内で"this.props.change_text(e.target.value)“でアクションを呼び出した上でディスパッチしてReducerにより新しいstateが発行されます。
import React, { Component } from 'react'; import { bindActionCreators } from 'redux'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import ChildComponent from './child-component'; import * as SampleAction from '../actions/sample-action'; function mapStateToProps(state) { const { text } = state.sampleReducer return { text: text }; } function mapDispatchToProps(dispatch) { return bindActionCreators( Object.assign({}, SampleAction), dispatch); } @connect(mapStateToProps, mapDispatchToProps) export default class FirstComponent extends Component { static propTypes = { change_text: PropTypes.func.isRequired, text: PropTypes.string } constructor(props) { super(props); } textFromInput(e) { this.props.change_text(e.target.value) } renderItem(text){ return ( <ChildComponent copyText={ text }/> ) } render() { const { text } = this.props; return ( <div> <h1>Hello, React!</h1> <input name="a" type="text" placeholder="from text" onChange = { this.textFromInput.bind(this) } /><br /> <input name="a" type="text" placeholder="to text" value = { text } readOnly="readonly" /><br /> <ChildComponent copyText={ text }/> </div> ); } }
これで動かしてみるとeslintにno-undefとか怒られるはずなので、.eslintrc.jsを以下のように修正し再度"npm run dev"で動画確認できるはずです。
'rules': { // allow paren-less arrow functions 'arrow-parens': 0, // allow async-await 'generator-star-spacing': 0, 'react/jsx-uses-vars': 1, // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 'no-undef': 0, 'no-console': 0, }
子のコンンポーネントからアクションを呼び出してみる
子のコンポーネントにも直接storeで管理しているstateを関連付けて利用することができる。以下の修正を加えることでテキストのstateを空白にするアクションを呼び出すようにすることができる。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import * as SampleAction from '../actions/sample-action'; function mapStateToProps() { return {} } function mapDispatchToProps(dispatch) { return bindActionCreators( Object.assign({}, SampleAction), dispatch); } @connect(mapStateToProps, mapDispatchToProps) export default class ChildComponent extends Component { static propTypes = { change_text: PropTypes.func.isRequired, copyText: PropTypes.string.isRequired } static defaultProps = { copyText: 'init val' } constructor(props) { super(props); } clearText() { this.props.change_text("") } render() { const { copyText } = this.props; return ( <div> <label>{ copyText }</label><br /> <button onClick = { this.clearText.bind(this) }>クリア</button> </div> ); } }
今回は直接storeの値を関連付けるようにしているが、親コンポーネント側で呼び出すアクションなりを変更できるようにしたいのであればpropsとして親のコンポーネントから子のコンポーネントに直接渡せるようにしたら良さそうに思います。
表示について
リストを表示してみる
リストのデータを表示する場合は以下のようになります。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class ListComponent extends Component { static propTypes = { listData: PropTypes.array } static defaultProps = { listData: [{} ] } constructor(props) { super(props); this.state = { listData: [ {id: 1, name: "山田一郎"}, {id: 2, name: "田中二郎"}, {id: 3, name: "佐藤三郎"} ] } } render() { const listData = this.state.listData return ( <ul className="user-list"> {listData.map((user, i) => <div key={i}><li>{ user.name } </li></div> )} </ul> ); } }
renderの部分は以下のように書き換えることもできる
userRender(userList){ return ( <ul className="user-list"> {userList.map((user, i) => <div key={i}><li>{ user.name } </li></div> )} </ul> ) } render() { const listData = this.state.listData return ( <div> { this.userRender(listData) } </div> );
または以下のような書き方もできる
userRender(userList){ const userListView = [] userList.map((user, i) => userListView.push(<div key={i}><li>{ user.name } </li></div>) ) return ( <ul className="user-list"> { userListView } </ul> ) } render() { const listData = this.state.listData return ( <div> { this.userRender(listData) } </div> ); }
webpackのcss-loaderを使ってみる
react,webpackでの環境でスタイルを適用する方法は複数あるのですが、まずはhtmlのheadタグの中にstyleを書き込んですべてのコンポーネントが適用対象にするのがなじみ深いと思いますのでそれから試してみたいと思います。webpackのcss-loaderを使ってbootstrapを読み込むようにしたいと思います。適用するのは簡単でbootstrapからファイル一式をダウンロードしてきて'src/assets/bootstrap'にダウンロードしたすべてのファイルを写した上で"src/main.js"に以下のimportを追加するだけになっております。
import './assets/bootstrap/css/bootstrap.min.css'
これで動きを見てみるとheadタグの中にstyleが書き込まれているのが分かります。今回はcssで試しましたがsassやstylusもwebpack側で読み込んで使うことができます(別途loader用のプラグインインストールが必要になるかもしれないです)。
CSS-in-JSを試してみる
Reactではcssをstyle属性として扱っていましてCSS-in-JSはCSSの記法で書いたスクリプトをを直接style属性として扱えるようにするものとなっております。例えば"src/style/sample.css.js"が以下の内容だったとする場合
export default { ul: { listStyle: 'none', marginTop: '20px', padding: '0px', fontSize: '18px', }, span: { paddingLeft: '20px', } }
コンポーネント側では以下のようにインポートしてstyle属性を設定することができます。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import styles from '../style/sample.css.js'; export default class ListComponent extends Component { static propTypes = { listData: PropTypes.array } static defaultProps = { listData: [{} ] } constructor(props) { super(props); this.state = { listData: [ {id: 1, name: "山田一郎"}, {id: 2, name: "田中二郎"}, {id: 3, name: "佐藤三郎"} ] } } render() { const listData = this.state.listData return ( <ul style={styles.ul}> {listData.map((user, i) => <li key={i}><span>{ user.id }</span><span style={styles.span}>{ user.name } </span></li> )} </ul> ); } }
またjsのプロパティとして扱っているだけなのでcss用にファイルを分ける必要もなく、以下のようにスタイルを設定することもできます。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class ListComponent extends Component { static propTypes = { listData: PropTypes.array } static defaultProps = { listData: [{} ] } constructor(props) { super(props); this.state = { listData: [ {id: 1, name: "山田一郎"}, {id: 2, name: "田中二郎"}, {id: 3, name: "佐藤三郎"} ], style: { ul: { listStyle: 'none', marginTop: '20px', padding: '0px', fontSize: '18px', }, span: { paddingLeft: '20px', } } } } render() { const listData = this.state.listData const style = this.state.style return ( <ul style={style.ul}> {listData.map((user, i) => <li key={i}><span>{ user.id }</span><span style={style.span}>{ user.name } </span></li> )} </ul> ); } }
デザイナーではないので良くわからないのですが、基本的にはcss-loaderの機能だけでスタイルを調整して動的に変更したい場合とかがあったらCSS-in-JSを使うとかの方がシンプルで良さそうな気がしました。
routerを使ってみる
react-routerのバージョン変更による影響が大きいので実施しるタイミングによって設定が結構変わってきそうです。自分が試した時はv4がリリースされていたので以下のコマンドでモジュールをインストールしたところv4.1.1が入りました。
npm install –save react-router-dom
“src/main.js"を以下のように修正することで利用できます。
import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import StoreConfig from './store/store-config' import './assets/bootstrap/css/bootstrap.min.css' import './assets/css/main.css' import { BrowserRouter, Route, Switch } from 'react-router-dom'; import FirstConponent from './components/first-component'; import ListComponent from './components/list-component'; const store = StoreConfig() render( <Provider store={store}> <BrowserRouter> <Switch> <Route exact path="/" component={FirstConponent} /> <Route exact path="/first" component={FirstConponent} /> <Route exact path="/list" component={ListComponent} /> </Switch> </BrowserRouter> </Provider > , document.getElementById('app') );
ここでは"http://localhost:8888/“及び"http://localhost:8888/first"でアクセスした時にFirstConmponentを表示し"http://localhost:8888/list"でアクセスした時にListConponentを表示する動きになります。urlからパラメータを受け取ったりテスト方法であったりは公式の方から確認いただければと思います。
ミドルウェアを使ってみる
それではReactを使う上で結構肝になりそうなミドルウェアを試してみたいと思います。まず簡単なログ出力を行ってみます。
ログを出力する
アクションが実行されるタイミングでミドルウェア側でconsole出力できるようにしたいと思います。まず"src/midleware/logger.js"を以下の内容で作成します。
const logger = function actionDebugMiddleware() { return next => action => { console.info(action.type, action); next(action); }; }; export default logger
次に"src/store/store-config.js"側でミドルウェアを使うように修正を加えます。
import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' import logger from '../midleware/logger' import rootReducer from '../reducers' export default function StoreConfig(preloadedState) { const finalCreateStore = applyMiddleware(thunk, logger )(createStore); const store = finalCreateStore(rootReducer, preloadedState); if (module.hot) { // Enable Webpack hot module replacement for reducers module.hot.accept('../reducers', () => { const nextReducer = require('../reducers').default store.replaceReducer(nextReducer) }) } return store }
これで動きを確認してみるとアクションがディスパッチして新しいstateを出力する直前でログ出力のミドルウェアがコンソール出力を行っているのが確認できます。ミドルウェア側ではnext(action)で次のアクションを呼び出すようにchainしているのですがここでactionの内容を修正したりすることで限定的ではありますがaopのように横断的な処理が行えるようです。
apiを呼び出してみる
次にミドルウェアを利用してapiを呼び出せるようにしてみましょう。今回はwebpackの静的コンテンツとしてjsonを作成しそれに対してgetのリクエストを投げて表示に反映したいと思います。まず以下のダミーデータを"static/dummy.json"のファイル名で追加します。
{"user_list":[ { "id":1, "name":"藤岡弘" }, { "id":2, "name":"佐々木剛" }, { "id":3, "name":"宮内洋" }, { "id":4, "name":"速水亮" }, { "id":5, "name":"岡崎徹" } ]}
次にapiを呼び出すミドルウェアを"src/midleware/api_caller.js"に追加します。今回はjqueryを使ってリクエストを投げます。
import {CALL_API} from '../define/action/sample-action-define' import $ from 'jquery' const api_caller = function actionApiCall() { function remoteService(next, remote){ $.ajax({ url: remote.url, dataType: remote.dataType, type: remote.type, data: remote.data, cache: false, scriptCharset: 'utf-8', success: data => { const new_action = remote.response_action(data) if(new_action.type == CALL_API){ remoteService(next, new_action.remote) }else{ next(new_action) } }, error: data => { console.info(data) } }); } return next => action => { if(action.type == CALL_API){ remoteService(next, action.remote) }else{ next(action) } }; }; export default api_caller
jqueryを使えるようにするため以下のコマンドを実行しておいてください。
npm install –save jquery
ここではアクションをディスパッチしてきた時のtypeがCALL_APIであったらajaxでリクエストを投げるようにしています。レスポンスが帰ってきた時に何をするかはディスパッチに受け取ったactionのremote属性に設定されているremote_responseを呼び出すようにしています。ここで使っている定数は"src/define/action/sample-action-define"で以下のように定義しています。
export const CHANGE_TEXT = 'CHANGE_TEXT' export const CALL_API = 'CALL_API' export const INIT_USER_LIST = 'INIT_USER_LIST'
次にミドルウェアとして呼び出せるように"src/store/store-config.js"を以下のように修正します。
const finalCreateStore = applyMiddleware(thunk, logger )(createStore); ↓ const finalCreateStore = applyMiddleware(thunk, logger, api_caller )(createStore);
また、今回api呼び出しで取得するユーザリストの情報はreduxのstateで管理するので以下のリデューサーを"src/reducers/userlist-reducer.js"に追加します。
import { INIT_USER_LIST } from '../define/action/sample-action-define'; const initialState = { user_list :[{id: '0', name: 'john doh'}] }; export default function userListReducer(state = initialState, action) { switch (action.type) { case INIT_USER_LIST: return Object.assign({}, state, action.data) default: return state } }
それから作成したreducerをcombineReducersで既存のものとマージして使えるようにします。
import { combineReducers } from 'redux' import sampleReducer from './sample-reducer' import userListReducer from './userlist-reducer' const rootReducer = combineReducers({ sampleReducer, userListReducer }) export default rootReducer
api呼び出しとapi受け取り後に新しいstateをディスパッチするためのアクションを"src/action/api/sample-api-action.js"に追加します。
import * as types from '../../define/action/sample-action-define' export function callApi(remote) { return { type: types.CALL_API, remote:remote} } export function user_list_init() { const response_action = function(data){ return {type: types.INIT_USER_LIST, data: data} } const data = {} return createRequestData( process.env.REQUEST_URL.USER_LIST_INIT, 'JSON', 'GET', data, response_action); } function createRequestData(url, dataType, type, data, response_action){ return { url: url, dataType:dataType, type:type, data: data, response_action: response_action, contentType: 'application/x-www-form-urlencoded; charset=UTF-8' } }
ここではリクエストを呼び出すのに使用するURLをwebpackの環境変数から取得しているのですが、apiのURLを環境変数から取得できるようにするため、"config/request.url.dev.json"を以下の内容で作成します。
{ "USER_LIST_INIT": "/static/dummy.json" }
これは開発時に使うapiのURLになりますので、実運用用のものは"config/request.url.dev.json"に記入しておいてください。それから"config/dev.env.js"でprodEnvにマージさせておいてください。prod.env.jsonも同様です。
var merge = require('webpack-merge') var prodEnv = require('./prod.env') var devUrl = require('./request.url.dev.json') module.exports = merge(prodEnv, { NODE_ENV: '"development"', REQUEST_URL: JSON.stringify(devUrl) })
ここのprodEnvは"build/webpack.dev.conf.js"で以下のように環境変数に追加しているので確認できます。
new webpack.DefinePlugin({ 'process.env': config.dev.env }),
これでURLを利用するときはprocess.env.REQUEST_URL.xxxxといった感じになるのがわかります。
あとapiを呼び出して利用する側のコンポーネントにも修正が必要なので"src/components/list-component.js"を以下のように修正します。apiの呼び出しはcomponentWillMountのタイミングで行っています。
import React, { Component } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import styles from '../style/sample.css.js'; import * as SampleApiAction from '../actions/api/sample-api-action' function mapStateToProps(state) { const { user_list } = state.userListReducer return { user_list: user_list } } function mapDispatchToProps(dispatch) { return bindActionCreators( Object.assign({}, SampleApiAction), dispatch); } @connect(mapStateToProps, mapDispatchToProps) export default class ListComponent extends Component { static propTypes = { user_list: PropTypes.array, callApi: PropTypes.func.isRequired, } static defaultProps = { user_list: [{}] } constructor(props) { super(props); this.state = {} } componentWillMount() { this.props.callApi(SampleApiAction.user_list_init()); } render() { const user_list = this.props.user_list console.info(user_list) return ( <ul style={styles.ul}> {user_list.map((user, i) => <li key={i}><span>{ user.id }</span><span style={styles.span}>{ user.name } </span></li> )} </ul> ); } }
これで"npm run dev"で起動してみるとapi呼び出しによる初期化が確認できるかと思います。
最後にこれまでの作業を行ってpackage.jsonは最終的に以下のようになりましたので、参考のため載せときます。
{ "name": "react_hands-on_project", "version": "1.0.0", "description": "A Vue.js project", "author": "arimuraterutoshiMac <arimuraterutoshi@192.168.11.6>", "private": true, "scripts": { "dev": "node build/dev-server.js", "start": "node build/dev-server.js", "build": "node build/build.js", "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", "e2e": "node test/e2e/runner.js", "test": "npm run unit && npm run e2e", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { "jquery": "^3.2.1", "react": "^15.5.4", "react-dom": "^15.5.4", "react-redux": "^5.0.4", "react-router-dom": "^4.1.1", "redux": "^3.6.0", "redux-thunk": "^2.2.0", "vue": "^2.2.6", "vue-router": "^2.3.1", "webpack": "^2.4.1", "webpack-dev-server": "^2.4.5" }, "devDependencies": { "autoprefixer": "^6.7.2", "babel-core": "^6.24.1", "babel-eslint": "^6.1.2", "babel-loader": "^6.4.1", "babel-plugin-istanbul": "^4.1.1", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-runtime": "^6.22.0", "babel-preset-env": "^1.3.2", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", "babel-preset-stage-2": "^6.24.1", "babel-register": "^6.22.0", "chai": "^3.5.0", "chalk": "^1.1.3", "chromedriver": "^2.27.2", "connect-history-api-fallback": "^1.3.0", "copy-webpack-plugin": "^4.0.1", "cross-env": "^4.0.0", "cross-spawn": "^5.0.1", "css-loader": "^0.28.0", "eslint": "^3.19.0", "eslint-config-standard": "^6.2.1", "eslint-friendly-formatter": "^2.0.7", "eslint-loader": "^1.7.1", "eslint-plugin-html": "^2.0.0", "eslint-plugin-promise": "^3.4.0", "eslint-plugin-react": "^6.10.3", "eslint-plugin-standard": "^2.0.1", "eventsource-polyfill": "^0.9.6", "express": "^4.14.1", "extract-text-webpack-plugin": "^2.0.0", "file-loader": "^0.11.1", "friendly-errors-webpack-plugin": "^1.1.3", "glob-loader": "^0.3.0", "html-webpack-plugin": "^2.28.0", "http-proxy-middleware": "^0.17.3", "inject-loader": "^3.0.0", "karma": "^1.4.1", "karma-coverage": "^1.1.1", "karma-mocha": "^1.3.0", "karma-phantomjs-launcher": "^1.0.2", "karma-phantomjs-shim": "^1.4.0", "karma-sinon-chai": "^1.3.1", "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "0.0.30", "karma-webpack": "^2.0.2", "lolex": "^1.5.2", "mocha": "^3.2.0", "nightwatch": "^0.9.12", "opn": "^4.0.2", "optimize-css-assets-webpack-plugin": "^1.3.0", "ora": "^1.2.0", "phantomjs-prebuilt": "^2.1.14", "react-hot-loader": "^1.3.1", "react-redux": "^5.0.4", "rimraf": "^2.6.0", "selenium-server": "^3.0.1", "semver": "^5.3.0", "shelljs": "^0.7.6", "sinon": "^2.1.0", "sinon-chai": "^2.8.0", "url-loader": "^0.5.8", "vue-loader": "^11.3.4", "vue-style-loader": "^2.0.5", "vue-template-compiler": "^2.2.6", "webpack": "^2.3.3", "webpack-bundle-analyzer": "^2.2.1", "webpack-dev-middleware": "^1.10.0", "webpack-hot-middleware": "^2.18.0", "webpack-merge": "^4.1.0" }, "engines": { "node": ">= 4.0.0", "npm": ">= 3.0.0" }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ] }
github
検証を行ったプログラムはGitHub - teruuuuuu/react_hands-on_projectにあげておきましたので、こちらで確認できるようになっています。
PostgreSQLで階層データ取得
PostgreSQL8.4からの新機能であるWITH RECURSIVEが便利であったのでメモ
WITH RECURSIVEは階層構造のデータを再起的に取得するのに利用でき、例えば以下のようなテーブルとデータがあったとして
CREATE TABLE user_mst ( user_id bigserial NOT NULL PRIMARY KEY, family_name character(32), first_name character(32), boss_id bigint ); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 1, '田中', '一郎', -1); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 2, '田中', '二郎', 1); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 3, '田中', '三郎', 1); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 4, '田中', '四郎', 1); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 5, '田中', '五郎', 2); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 6, '山田', '六郎', -1); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 7, '山田', '七郎', 6); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 8, '山田', '八郎', 7); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 9, '山田', '九郎', 8); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 10, '山田', '十郎', 9); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 11, '山田', '十一郎', 10); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 12, '佐藤', '十二郎', -1);
これに対して以下のSQLを実行すると、子供のデータを再起的に取得することができる。
WITH RECURSIVE user_hierarchy AS ( SELECT user_id FROM user_mst WHERE user_id = 1 UNION ALL SELECT user_mst.user_id FROM user_mst, user_hierarchy WHERE user_mst.boss_id = user_hierarchy.user_id ) SELECT * FROM user_mst WHERE user_id IN ( SELECT user_id FROM user_hierarchy ORDER BY user_hierarchy.user_id );
ここでのuser_idを変更して確認してみると確かにそうなっていることが確認できる。
それでは逆に親のデータを再起的に取得したい場合は、UNIONしているSELECTのWHERE条件で使っているuser_idとboss_idを入れ替えて以下のようにするだけである。
WITH RECURSIVE user_hierarchy AS ( SELECT user_id, boss_id FROM user_mst WHERE user_id = 10 UNION ALL SELECT user_mst.user_id, user_mst.boss_id FROM user_mst, user_hierarchy WHERE user_mst.user_id = user_hierarchy.boss_id ) SELECT * FROM user_mst WHERE user_id IN ( SELECT user_id FROM user_hierarchy ORDER BY user_hierarchy.user_id );
なかなか便利そう
Reactのプロジェクト作成
vue-cliによるプロジェクト作成
reactを始める時は最初のプロジェクト作成が障壁になりそうでreact-cliという公式が出しているツールを使えば開発環境を素早く構築することができますが、内部で利用しているwebpackの設定がどうなっているかなど良く分からなさそうで、使ってみると不便そうな気がしたので自分は試したことがありません。
そこで今回はvueでの開発に使用しているvue-cliを使用してプロジェクトを作成後、reactを利用できるようにしたいと思います。vue-cliの方が開発環境や、ビルドの設定の確認、編集が行いやすくなっておりますので、個人的にはこちらの方が扱いやすいのでこっちで試してみたいと思います。
vue-cliのインストール
npm install -g vue-cli
プロジェクト作成
vue init webpack my_project
cd my-project
npm install
一旦起動してみる
プロジェクト作成直後はvueのプロジェクトになっておりますが、とりあえず以下のコマンドでwebpackを起動して動作を確認してみたいと思います。
npm run dev
上記コマンド実行後にブラウザが開いてvue-jsのアイコンが表示された画面が表示されたかと思います。
vue-cliで作成したプロジェクトをreact化する
それではreactで開発できるようにしていきたいと思います。まずは必要なモジュールをインストールします
npm install –save react react-dom redux react-redux npm install –save-dev babel-loader babel-core babel-preset-react babel-preset-es2015
npm install –save-dev eslint@3.x babel-eslint@6
npm install –save-dev file-loader
npm install –save-dev glob-loader
npm install –save-dev eslint-plugin-react
それから以下の".babelrc"をプロジェクトのルートディレクトリに作成します。
{ "presets": [ "es2015", "react"] }
次にeslintでreactのプラグインを使うように以下の内容にします。
// http://eslint.org/docs/user-guide/configuring module.exports = { root: true, parser: 'babel-eslint', parserOptions: { sourceType: 'module' }, env: { browser: true }, // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style extends: [ "eslint:recommended", "plugin:react/recommended" ], // required to lint *.vue files plugins: [ 'html', 'react' ], // add your custom rules here 'rules': { // allow paren-less arrow functions 'arrow-parens': 0, // allow async-await 'generator-star-spacing': 0, 'react/jsx-uses-vars': 1, // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 } }
あとは"src/main.js"を以下のないように変更したらReactでのコンポーネント初期化が確認できると思います。コンポーネントの初期化についてはes2015の機能であるconstで値の再代入をできないようにしております。es2015を使う場合は変数を定義する場合できるだけlet, constでスコープを制限したり再代入を禁止することで想定外の副作用を防げるようにしておいた方がよさそうです。
import React from 'React' import { render } from 'react-dom' const element = ( <div> <h1>Hello, React!</h1> </div> ) render( element , document.getElementById('app') );
それから"npm run dev"で起動すると思うのですがたくさん警告が表示されると思います。これについてインポートするフモジュール名が実際はreactなのにReactでしているのが原因で、モジュール名はケバブケースを用いるのが標準のようですhttps://github.com/AngularClass/angular2-webpack-starter/issues/926。 それでは以下のようにimport先のモジュール名を変更して再度"npm run dev"を実行してみると警告もなく動くのが確認できると思います。
import React from 'React' ↓ import React from 'react'
ビルドをする場合は"npm run build"を実行すればよく、設定などを変更したい場合はbuildディレクトリの中身を確認して変更していけばどうにかなりそうです。
ScalaでfoldLeftを使ってみる
foldLeftを使ってみる
foldLeft使用の覚書
list内の単語の集計を行って降順にする
これだけでリスト内の単語の集計とソートが行える
val countWordsList = List("a", "b", "c", "a", "d", "b", "e", "b") val wordAggregate = countWordsList.foldLeft(Map[String, Int]().withDefaultValue(0)){ (map, key) => map + (key -> (map(key) + 1)) }.toSeq.sortBy(_._2).reverse println(wordAggregate)
文字列の置換を行う
val replaceMap = Map("{$1}" -> "health", "{$2}" -> "wealth") val beforeText = "{$1} is better than {$2}" val afterText = replaceMap.foldLeft(beforeText){ (tex, map) => tex.replace(map._1, map._2) } println(afterText)