TypeScriptおよびReact Webアプリケーションを開発するための環境:「hello world」から最新のSPAまで。 パート2

この記事の目的は、最新のWebアプリケーションを開発し、必要なツールとライブラリを順次追加およびカスタマイズするための環境を読者と一緒に書くことです。 多数のスターターキット/定型リポジトリとの類推によってですが、私たちのものです。



この記事は完全に改訂と修正のために公開されており、おそらく最終的な資料は、専門家と新しい技術を試してみたい人の両方にとって興味深い最新の便利な参考書になります。



画像



この記事では、詳細なTypeScript構文とReactの操作の基本については考慮していません。上記のテクノロジーを使用した経験がない場合は、それらの研究を分離することをお勧めします。



記事の最初の部分へのリンク



プロジェクトリポジトリには、各ステップの個別のブランチにコードが含まれています。



パート2の主なテーマは、 Redux状態マネージャーの接続と使用です。



Reduxを使用する理由と、Fluxパターンの他の実装との比較-個々の記事のトピック、この情報は簡単に見つけて調べることができます。



いくつかの利点-大規模なコミュニティとエコシステム、アプリケーションの動作を完全に制御できる可能性、テストの容易さ、関数型プログラミングの学習に向けた特定の手順に注目します。



React-reduxはいくつかのReactコンポーネントを提供する小さなライブラリです-ProviderはReduxストレージをコンテキストに転送し、 connectはポイント単位の転送とストレージからラップされたコンポーネントのプロパティへのデータの更新のための高次コンポーネントです。



コードを見てみましょう!



ステップ4-プロジェクトにReduxを追加します(ベースHello World)



結果のコードを表示するには:



git checkout step-4
      
      





srcフォルダーで、コンポーネントを削除します-ステップ3の例では、 index.htmlindex.tsxのみが残ります。



依存関係のインストール (reduxにはソースに宣言ファイルが含まれています):



 npm install redux react-redux -S npm install @types/react-redux -D
      
      





プロジェクト設定を変更します。



tsconfig.jsonにmoduleResolution:nodeプロパティを追加して、コンパイラーがpackage.jsonライブラリー(この場合はredux)で定義された宣言を見つけるようにします。



tsconfig.json
 { "compilerOptions": { "lib": [ "es5", "es6", "es7", "dom" ], "target": "es5", "module": "esnext", "jsx": "react", "moduleResolution": "node" } }
      
      







アヒルモジュールの方法論を使用して、将来のリポジトリの単純なアクションとレデューサーを作成しましょう。



ソースフォルダーに、アヒルモジュールを保存するreduxフォルダーを作成します。 内部で、 field.tsファイルを作成します。



field.ts
 /** * State * *       , *      . */ export interface FieldState { value: string; focus: boolean; } const initialState: FieldState = { value: '', focus: false } /** * Constants * *    ,   . *       ,    *        . */ const SET = 'field/SET'; type SET = typeof SET; const FOCUS = 'field/FOCUS'; type FOCUS = typeof FOCUS; const BLUR = 'field/BLUR'; type BLUR = typeof BLUR; /** * Actions * *  Redux  TypeScript,     * ,     (FieldAction)   */ export interface SetAction { type: SET; payload: string; } export interface FocusAction { type: FOCUS; } export interface BlurAction { type: BLUR; } type FieldAction = SetAction | FocusAction | BlurAction; /** * Reducer * *    ,     ,  *      . *  action     ,  *       (FieldAction),   *      ( case SET)   *    action. */ export default function reducer(state: FieldState = initialState, action: FieldAction): FieldState { switch (action.type) { case SET: return { ...state, value: action.payload } case FOCUS: return { ...state, focus: true } case BLUR: return { ...state, focus: false } default: return state; } } /** * Action Creators * *      , *        *  ,       * . */ export const set = (payload: string): SetAction => ({ type: SET, payload }); export const focus = (): FocusAction => ({ type: FOCUS }); export const blur = (): BlurAction => ({ type: BLUR });
      
      







index.tsファイルをreduxフォルダーに追加します。これをルートレデューサー(rootReducer)としてリポジトリにインポートします。



redux / index.ts
 import { combineReducers } from 'redux'; import fieldReducer from './field'; export default combineReducers({ field: fieldReducer })
      
      







さらに、Redux- Redux DevToolsで開発ツールを使用します。

ソースフォルダーで、 index.tsファイル内にストアフォルダーを作成します。



ストア/index.ts
 import { createStore } from 'redux'; import rootReducer from '../redux'; import { FieldState } from '../redux/field'; /** *       mapStateToProps, *    ,       * (,     redux-thunk) */ export interface IStore { field: FieldState } /** *          . */ const configureStore = (initialState?: IStore) => { return createStore( rootReducer, initialState, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ) }; export default configureStore;
      
      







TypeScriptコンパイラは、グローバルウィンドウオブジェクトの__REDUX_DEVTOOLS_EXTENSION__プロパティについて何も認識していないため、宣言を追加します。



さらにこれらの宣言では、__ DEV__や__PRODUCTION__など、Webpackを介して送信するグローバルフラグを追加します。



ルートフォルダーで、 windows.d.tsファイル内に、 typingsフォルダーを作成します。



window.d.ts
 interface Window { __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any; __REDUX_DEVTOOLS_EXTENSION__: any; }
      
      







次に、ストアからデータを受信して​​更新するコンポーネントを作成します。 簡素化するために、コンポーネントとコンテナーに分離することはありません。 ソースフォルダーで、 Field.tsxファイル内にcomponentsフォルダーを作成します。



Field.tsx
 import * as React from 'react'; import { connect, Dispatch, DispatchProp } from 'react-redux'; import { IStore } from '../store'; import { set, focus, blur } from '../redux/field'; /** *   DispatchProp ,   dispatch  *   .    connect,    *  ( mapDispatchToProps) */ interface FieldProps extends DispatchProp<IStore>, React.HTMLProps<HTMLInputElement> { value?: string; } class Field extends React.Component<FieldProps, {}> { handleChange = (event: React.FormEvent<HTMLInputElement>) => { const { dispatch } = this.props; const value = event.currentTarget.value; /** *     set  dispatch  *   . */ dispatch(set(value)); } handleFocus = () => { const { dispatch } = this.props; dispatch(focus()); } handleBlur = () => { const { dispatch } = this.props; dispatch(blur()); } render() { const { value, dispatch, ...inputProps } = this.props; return ( <input {...inputProps} type="text" value={value} onChange={this.handleChange} onFocus={this.handleFocus} onBlur={this.handleBlur} /> ); } } /** *   mapStateToProps,   (  ) *        */ const mapStateToProps = (state: IStore, ownProps: FieldProps) => ({ value: state.field.value }); /** *  mapDispatchToProps: * (dispatch: Dispatch<IStore>, ownProps: FieldProps) => ({ ... }) */ /** * connect   ,   10   *   . *  ,   3 : *  mapStateToProps,  mapDispatchToProps,  *   . *  ,       , *         . */ export default connect<{}, {}, FieldProps>(mapStateToProps)(Field);
      
      







そして最後に、エントリポイント-src / index.tsxでアプリケーションのすべてを収集します。



src / index.tsx
 import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import configureStore from './store'; import Field from './components/Field'; /** *   ,   initialState  * window.__INITIAL_STATE__,       *    . */ const store = configureStore(); /** *          *  DevTools */ const App = () => ( <Provider store={store}> <div> <h1>Hello, Redux!</h1> <Field placeholder='I like dev tools!' /> </div> </Provider> ); ReactDOM.render(<App />, document.getElementById('root'));
      
      







ステップ5-いくつかのRedux Typescriptレシピ



結果のコードを表示するには:



 git checkout step-5
      
      





最初のレシピはミドルウェアです。



ソースフォルダーで、 logger.tsファイル内にミドルウェアフォルダーを作成します(コードは公式ドキュメントから取得されます )。



ミドルウェア/ logger.ts
 import { Middleware, MiddlewareAPI, Dispatch, Action } from 'redux'; import { IStore } from '../store'; /** *    store - MiddlewareAPI<S & IStore> -   *   ,      *    .     , *    middleware  -. */ const logger: Middleware = <S>(store: MiddlewareAPI<S & IStore>) => (next: Dispatch<S>) => //   - <A extends Action>(action: A),   . (action: any) => { //     store.getState().field.value; console.log('dispatching', action); let result = next(action); console.log('next state', store.getState()); return result; } export default logger;
      
      







コードを更新してリポジトリを作成します。



ストア/index.ts
 import { createStore, compose, applyMiddleware } from 'redux'; import rootReducer from '../redux'; import { FieldState } from '../redux/field'; import logger from '../middlewares/logger'; export interface IStore { field: FieldState } let composeEnhancers = compose; //      middleware const middlewares = [ logger ]; if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; } const configureStore = (initialState?: IStore) => { return createStore( rootReducer, initialState, composeEnhancers( applyMiddleware(...middlewares) ) ) }; export default configureStore;
      
      







2番目のレシピは、高次のリデューサーです。



reduxフォルダーで、ファイルcreateNamedReducer.tsを作成します(コードは公式ドキュメントから取得されます )。



createNamedReducer.ts
 import { Reducer, Action } from 'redux'; import { IStore } from '../store'; /** *      ,  * ,   createNamedReducer */ export interface namedAction extends Action { name: string; } function createNamedReducer<S>(reducer: Reducer<S>, reducerName: string): Reducer<S> { return (state: S, action: namedAction) => { const { name } = action; const isInitializationCall = state === undefined; if (name !== reducerName && !isInitializationCall) { return state; } return reducer(state, action); } } export default createNamedReducer;
      
      







ステップ6-APIを使用する



結果のコードを表示するには:



 git checkout step-6
      
      





注意! APIを操作するためのメソッドを使用してサービスを分離し、サンクアクション内でこれらのメソッドを呼び出して、データをリポジトリにバインドすることを好みます。



しかし、redux-axios-middlewareやredux-apiなどのライブラリがあります。これらのライブラリは、ボイラープレートコードを減らし、http要求の作成に対するラッパーを作成するように設計されています。



したがって、ReduxとREST APIをリンクする際のヒントとコメントでこの記事を補足し、将来最も人気のあるテクニックを詳細に説明したいと思います。



APIモックの場合、 jsonplaceholderサービスを使用します。



依存関係のインストール (両方のライブラリに宣言が含まれています):



 npm install axios redux-thunk -S
      
      





client.tsファイルusers.tsファイル内のプロジェクトソースにservicesフォルダーを作成します



client.ts
 import axios from 'axios'; /** *        redux * ,       . */ const client = axios.create({ baseURL: 'https://jsonplaceholder.typicode.com' }); export default client;
      
      







users.ts
 import { AxiosPromise } from 'axios'; import client from './client'; //       export interface IUser { id: number; name: string; username: string; email: string; address: any; phone: string; website: string; company: any; } export function get(id: number): AxiosPromise<IUser> { return client.get(`/users/${id}`); } export function getList(): AxiosPromise<IUser[]> { return client.get('/users'); }
      
      







次に、新しいアヒルモジュールusers.tsを作成します。この段階で多くの質問が発生し、それらを解決するための多くのオプションがあります。



redux / users.ts
 import { Dispatch } from 'redux'; import { IStore } from '../store'; import * as client from '../services/users'; //     type Error = any; //       http  interface AsyncState<D> { isFetching: boolean; error: Error; data: D; } //    ,      interface AsyncAction<P> { status?: 'error' | 'success'; payload?: P | Error; } /** * State */ export interface UsersState { get: AsyncState<client.IUser>; getList: AsyncState<client.IUser[]>; } const initialState: UsersState = { get: { isFetching: false, error: null, data: null }, getList: { isFetching: false, error: null, data: [] } } /** * Constants */ const GET = 'users/GET'; type GET = typeof GET; const GET_LIST = 'users/GET_LIST'; type GET_LIST = typeof GET_LIST; /** * Actions */ export interface GetAction extends AsyncAction<client.IUser> { type: GET; } export interface GetListAction extends AsyncAction<client.IUser[]> { type: GET_LIST; } type UsersAction = GetAction | GetListAction; /** * Reducer * * ,  ! *   ,       *    ,    *   . *     ,     * ,  -   . */ export default function reducer(state: UsersState = initialState, action: UsersAction): UsersState { switch (action.type) { case GET: if (!action.status) { return { ...state, get: { ...state.get, isFetching: true, error: null } } } if (action.status === 'error') { return { ...state, get: { isFetching: false, error: action.payload, data: null } } } return { ...state, get: { isFetching: false, error: null, data: action.payload } } case GET_LIST: if (!action.status) { return { ...state, getList: { ...state.getList, isFetching: true, error: null } } } if (action.status === 'error') { return { ...state, getList: { isFetching: false, error: action.payload, data: [] } } } return { ...state, getList: { isFetching: false, error: null, data: action.payload } } default: return state; } } /** * Action Creators */ export const getActionCreator = ( status?: 'error' | 'success', payload?: client.IUser | Error ): GetAction => ({ type: GET, status, payload, }); export const getListActionCreator = ( status?: 'error' | 'success', payload?: client.IUser[] | Error ): GetListAction => ({ type: GET_LIST, status, payload, }); /** * Thunk Actions */ export function get(id: number) { return async (dispatch: Dispatch<IStore>, getState: () => IStore) => { dispatch(getActionCreator()); try { const response = await client.get(id); dispatch(getActionCreator('success', response.data)); } catch (e) { dispatch(getActionCreator('error', e)); throw new Error(e); } } } export function getList() { return async (dispatch: Dispatch<IStore>, getState: () => IStore) => { dispatch(getListActionCreator()); try { const response = await client.getList(); dispatch(getListActionCreator('success', response.data)); } catch (e) { dispatch(getListActionCreator('error', e)); throw new Error(e); } } }
      
      







rootReducerとストレージインターフェイスを更新し、サンクミドルウェアを追加します。



redux / index.ts
 import { combineReducers } from 'redux'; import fieldReducer from './field'; import usersReducer from './users'; export default combineReducers({ field: fieldReducer, users: usersReducer });
      
      







ストア/index.ts
 import { createStore, compose, applyMiddleware } from 'redux'; import ReduxThunk from 'redux-thunk' import rootReducer from '../redux'; import { FieldState } from '../redux/field'; import { UsersState } from '../redux/users'; import logger from '../middlewares/logger'; export interface IStore { field: FieldState, users: UsersState } let composeEnhancers = compose; const middlewares = [ logger, ReduxThunk ]; if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; } const configureStore = (initialState?: IStore) => { return createStore( rootReducer, initialState, composeEnhancers( applyMiddleware(...middlewares) ) ) }; export default configureStore;
      
      







次に、ユーザーのリスト、エラーメッセージ、または条件付きプリローダーを表示するコンポーネントを作成します。



Users.tsx
 import * as React from 'react'; import { connect, Dispatch, DispatchProp } from 'react-redux'; import { IStore } from '../store'; import { getList, Error } from '../redux/users'; import { IUser } from '../services/users'; interface UsersProps extends DispatchProp<IStore> { isFetching?: boolean; error?: Error; users?: IUser[]; } class Users extends React.Component<UsersProps, {}> { componentDidMount() { const { dispatch } = this.props; dispatch(getList()); } render() { const { isFetching, error, users } = this.props; if (error) { return <b> !</b> } if (isFetching) { return '...'; } return users.map((user) => <div>{user.name}</div>); } } const mapStateToProps = (state: IStore, ownProps: UsersProps) => ({ isFetching: state.users.getList.isFetching, error: state.users.getList.error, users: state.users.getList.data }); export default connect<{}, {}, UsersProps>(mapStateToProps)(Users);
      
      







次に、アプリケーションのルートコンポーネントで<Users />コンポーネントを呼び出します。



明確な答えのない質問:



リクエストオブジェクトをリポジトリに保存する必要がありますか?また、どのような利点がありますか? おそらくこれにより、リクエストのキャンセルが簡単になります。



URLのidがdynamicである1つのGETリクエストで、1つの画面で多くのコンポーネントを使用する場合の対処方法



異なるパラメータが同じリクエストに送られる場合の非同期オートコンプリートに関する同様の問題。 応答をキャッシュできますが、そのような場合、各リクエストのステータスを個別に監視する必要があります。これには、個別のリデューサーが必要です。



1つの特定の要求に対してレデューサーを動的に追加するコンポーネント、またはローカルでのみ使用される非同期データの一部をReduxに保存する必要がないコンポーネントを使用することは意味がありますか?



React + ReduxアプリケーションでAPIを使用する方法についての記事に詳細な解説を書き、次のステップに進みます。



第7ステップ-生産および開発アセンブリ



結果のコードを表示するには:



 git checkout step-7
      
      





1) クロスブラウザーの互換性

依存関係のインストール:



 npm install core-js -S npm install @types/core-js -D
      
      





Core-jsは、現代のJSコンストラクトのポリフィルライブラリです。 core-js / shimモジュールのインポートは、 babel-polyfillプラグインの使用ほぼ同じです。



いくつかの必要なポリフィルのみを使用し、それらをアプリケーションのエントリポイントの先頭に追加します。



src / index.ts
 import 'core-js/es6/promise'; import 'core-js/es6/map'; import 'core-js/es6/set'; if (typeof window.requestAnimationFrame !== 'function') { window.requestAnimationFrame = (callback: FrameRequestCallback) => window.setTimeout(callback, 0); } ...
      
      







tsconfig.jsonファイルでは、「target」プロパティはすでに「es5」として指定されているため、ほとんどのポリフィルは必要ありません。 現在のビルドはIE9 +をサポートしています。



1) 生産組立



この段階で、アセンブリパラメーターを追加し、webpack設定自体を変更し、値process.env.NODE_ENVをグローバルパラメーターとして送信する必要があります。一部のライブラリ(Reactなど)は、このパラメーターに応じてprodまたはdevソースを使用します。



依存関係のインストール:



 npm install better-npm-run -D
      
      





better-npm-run -npmスクリプトをダウンロードします。



package.jsonの npmスクリプトを編集してみましょう。環境変数は「betterScripts」ブロックで非常に便利に定義されています。



package.json
 { ... "scripts": { "start": "better-npm-run dev", "build": "better-npm-run build" }, "betterScripts": { "dev": { "command": "webpack-dev-server", "env": { "NODE_ENV": "development" } }, "build": { "command": "webpack", "env": { "NODE_ENV": "production" } } }, ... }
      
      







webpackの設定が複雑な場合、 webpack-mergeプラグインが助けになります-現時点では、コードを複雑にしないために、それを使用しません。



webpack.config.jsの変更:



webpack.config.js
 const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); //    const env = process.env.NODE_ENV; const __DEV__ = env === 'development'; const __PRODUCTION__ = env === 'production'; const paths = { src: path.resolve(__dirname, 'src'), dist: path.resolve(__dirname, 'dist') }; const config = { context: paths.src, entry: { app: './index' }, //     ,   output: { path: paths.dist, filename: __PRODUCTION__ ? '[name].bundle.[chunkhash].js' : '[name].bundle.js', chunkFilename: __PRODUCTION__ ? '[name].bundle.[chunkhash].js' : '[name].bundle.js' }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] }, module: { rules: [ { test: /\.tsx?$/, loader: 'awesome-typescript-loader' } ] }, plugins: [ //   NODE_ENV     new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(env) }), new HtmlWebpackPlugin({ template: './index.html' }), //      new webpack.optimize.ModuleConcatenationPlugin() ] }; if (__DEV__) { //  source map  development  config.devtool = 'inline-source-map'; } if (__PRODUCTION__) { config.plugins.push(new CleanWebpackPlugin(['dist'])); //   config.plugins.push(new webpack.optimize.UglifyJsPlugin()); } module.exports = config;
      
      







実動アセンブリには次のコマンドを使用します。



 npm run build
      
      





アセンブリが完了すると、サイズが約180kb、gzipで圧縮された約55kbの合計バンドルが得られます。 さらに、node_modulesのライブラリは、CommonsChunkPluginを使用して別のバンドルに移動できます。



次の記事のトピック:ルーティング、プログレッシブWebアプリケーション(PWA)の作成、サーバーレンダリング、Jestでのテスト。



ご清聴ありがとうございました!



All Articles