- 投稿日:2020-02-12T21:25:56+09:00
JavaScriptしか知らない初心者のReact Native入門
プログラミング歴半年の素人が書いています。
間違いのないようご自身でも良く調べた上でお願いいたします。
対象読者
- モバイルアプリ開発をしたことがない人
- JavaScriptの基礎構文がわかる人
- Reactを知っている人
- Macの人
わたしです。 試行錯誤の記録となっておりますので、ベストプラクティスではない可能性が十分含まれておりますことご了承ください。
環境構築
Node.jsのインストール
https://nodejs.org/en/download/
上記リンクからダウンロードできます。
ReactNativeのインストール
公式チュートリアルでもおすすめされているように、「Expo CLI」という「ReactNative+便利機能いろいろ」のパッケージをインストールしてみます。
ついでに、お手持ちのスマホのアプリストアで「Expo」を検索し、アプリをスマホにダウンロードしておくとよいでしょう。自分のスマホで、開発中の画面を確認することができます。
$ npm install -g expo-cli上記のコマンドで「Expo CLI」をインストールできます。
新しいプロジェクトの作成
早速、アプリの開発にとりくみましょう。
$ expo init AwesomeProject
新しいプロジェクトを作成します。今回は「AwesomeProject」という名前をつけました。
途中、スターターテンプレートを何にするか聞かれます。
「Blank」とか「Blank with TypeScript」だとか、いろいろありますが、今回は初めてなので「Blank」を選択してみました。cd AwesomeProject npm start
プロジェクトの作成が済んだら、作成したディレクトリに移動して、
npm start
で起動します。
ブラウザにCLIが表示されたら成功です。Expoのダウンロードと登録
上記からExpoにアクセスし、SignUp します。
お使いのスマホがあれば、公式アプリをダウンロードします。Expoで開発画面を表示する
npm start
してブラウザに表示された画面に、QRコードがあればそれをスマホで読み取ることで開発中の画面を表示することができます。ここでエラー!
私の場合はここでエラーがでました。
error Unable to resolve "@react-navigation/native" from "App.js"うーん、初めてなのでなんのこっちゃモジュール。
ひとまず
App.js
を公式チュートリアルのとおり、シンプルに書き直すことで解決しました。App.jsimport React, { Component } from 'react'; import { Text, View } from 'react-native'; export default class HelloWorldApp extends Component { render() { return ( <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <Text>Hello, world!</Text> </View> ); } }ちなみに、Hooksで関数コンポーネントにしても動きました。(
react-native: 0.61.4
)App.jsimport React from 'react'; import { Text, View } from 'react-native'; const HelloWorldApp = () => { return( <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <Text>Hello, World!</Text> </View> ) } export default HelloWorldApp無事、Hello Worldまでたどりつきました。
React Native独自のコンポーネント
通常のJSXで使うことができる
<div>
や<h1>
などのコンポーネントの代わりに<View>
や<Text>
を使います。- <View> : <div>や<span> - <Text> : <p>など文字列の表示に使用画像を表示する
「Hello, World!」の代わりに画像を表示します。
React Nativeでは<img>
のかわりに<Image>
が用意されているので、インポートして使います。画像ソースを指定するプロパティは、
src
ではなくsource
です。App.jsimport React from 'react'; // Imageコンポーネントをインポート import { Text, View, Image } from 'react-native'; const HelloWorldApp = () => { // 画像へのパスを定義 let pic = { uri: 'https://i.picsum.photos/id/237/300/200.jpg' } return( <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <Image source={pic} style={{width: 300, height: 200}} /> </View> ) } export default HelloWorldAppPropsとStateは通常のReactと同じ
同じ...だと思います。
まとめ
...更新中
- 投稿日:2020-02-12T13:39:00+09:00
reactのコンポーネントの書き方メモ
reactのコンポーネントの書き方
app.js
import React from 'react'; import logo from './logo.svg'; import './App.css'; import TestComponent from './components/TestComponent.js'; import ButtonComponent from './components/ButtonComponent.js'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload!!!!!. </p> <TestComponent text="うふふふ" /> <TestComponent text="あははは" /> <ButtonComponent text="あははは" bgColor="red" /> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } export default App;TestComponent.js
import React from "react"; import styled from "styled-components"; const TestComponent = props => { return ( <Test> {props.text} </Test> ); }; const Test = styled.p` padding: 0 40px 40px 40px; font-size: 1.4rem; `; export default TestComponent;
- 投稿日:2020-02-12T13:01:51+09:00
Reactのルーティングライブラリの実装を読んだ&解説してみた
SPAアプリを開発する場合、どのpathにアクセスしてもサーバーからは同じHTML、jsファイルが返却されるので、クライアント側でルーティング機能を実装する必要があります。そしてReactなら、react-router-domといったライブラリを使って実装するのが一般的だと思います。
<BrowserRouter> <Switch> <Route exact path=""> <Component /> </Route> </Switch> </BrowserRouter>こんな感じで記述しておけば、書いたpathにアクセスすることで指定したコンポーネントが表示され、実質的にルーティングが可能になります。
では、ルーティングライブラリはどんな実装でこの機能を実現しているのか、というのが気になったので、ライブラリの中身をざっと読んでみました。
react-router-domの公式ドキュメントを読むと、上記の実装が最小限っぽかったので、この3つのコンポーネントの実装を追っていきます。
BrowserRouter.js
BrowserRouterの実装は非常にシンプルで、以下の数行だけです。
BrowserRouter.jsimport { createBrowserHistory as createHistory } from "history"; /* * The public API for a <Router> that uses HTML5 history. */ class BrowserRouter extends React.Component { history = createHistory(this.props); render() { return <Router history={this.history} children={this.props.children} />;historyを生成し、コンポーネントのpropsに乗せてreturnしているだけですね。createBrowserHistoryは何をしているかというと、ブラウザが提供するhistoryオブジェクトをラップし、APIとしてpushメソッドやreplaceメソッドを公開しています。Switchコンポーネント以下のコンポーネントで、propsをconsole.logなどしてみると、historyの中身(=createBrowserHistory)を確認できます。
Switch.js
次にSwitch.jsを見ていきます。ここで少しややこしいのは、react-routerはいくつかのパッケージに分かれていることです。react-router-config、react-router-dom、react-router-native、react-routerの4つがあり、Switch.jsの本体はreact-routerにあります。
react-router-domにもSwitch.jsファイルはありますが、react-router側のSwitch.jsを参照する内容が記述されているだけです。おそらく、コアの機能はreact-router側に集約されているのでしょう。BrowserRouter.jsはその名前の通り、Webブラウザ特有の実装だったため、react-router-domパッケージの中で実装されていました。
Switch.jsの処理も非常に簡潔で、コア部分は30行ほどからの実装になっています。(一部、コメントを削除して、解説コメントを追記しています)
Switch.js/** * The public API for rendering the first <Route> that matches. */ class Switch extends React.Component { render() { return ( <RouterContext.Consumer> {context => { invariant(context, "You should not use <Switch> outside a <Router>"); // 現在のpathを取得 const location = this.props.location || context.location; let element, match; // Switchコンポーネント以下に置かれているコンポーネントをループで処理 React.Children.forEach(this.props.children, child => { if (match == null && React.isValidElement(child)) { element = child; // Routeコンポーネントからpathを取得 const path = child.props.path || child.props.from; // 現在のpathと、Routeコンポーネントのpathが一致している場合、matchにそのpathを代入 match = path ? matchPath(location.pathname, { ...child.props, path }) : context.match; } }); // 現在のpathとmatchするRouteコンポーネントがあった場合は、propsを追加してreturn return match ? React.cloneElement(element, { location, computedMatch: match }) : null; }} </RouterContext.Consumer> ); } }Switchコンポーネントの役割は、自身の配下にあるRouteコンポーネント(=children)をループで処理し、現在のpathとマッチするRouteコンポーネントがあるかを判定することです。
Switchコンポーネントの配下にいくつRouteコンポーネントが配置されていても、ループ処理の中でふるいにかけられ、レンダリング時点ではpathにマッチするコンポーネントのみが残ることになります。
Route.js
Route.jsもSwitch.jsと同じくreact-routerパッケージにあります。上2つに比べて少し長いので、前半、後半に分けて見ていきます。
Route.js/** * The public API for matching a single path and rendering. */ class Route extends React.Component { render() { return ( <RouterContext.Consumer> {context => { invariant(context, "You should not use <Route> outside a <Router>"); // 現在のpathを取得 const location = this.props.location || context.location; // computedMatchはSwitch.jsで追加されたprops const match = this.props.computedMatch ? this.props.computedMatch : this.props.path ? matchPath(location.pathname, this.props) : context.match; const props = { ...context, location, match }; let { children, component, render } = this.props; // Preact uses an empty array as children by // default, so use null if that's the case. if (Array.isArray(children) && children.length === 0) { children = null; } {/* 後半部分は省略 */} }} </RouterContext.Consumer> ); } }前半部分は特別なことはしていないように見えます。Switch.jsで行なっていた、現在のpathとpropsのpathが一致するかどうかの判定をもう一度行なっているのがよくわかりませんが。。。
後半部分は以下です。
Route.js/** * The public API for matching a single path and rendering. */ class Route extends React.Component { render() { return ( <RouterContext.Consumer> {/* 前半部分は省略 */} return ( <RouterContext.Provider value={props}> {props.match ? children ? typeof children === "function" ? __DEV__ ? evalChildrenDev(children, props, this.props.path) : children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null : typeof children === "function" ? __DEV__ ? evalChildrenDev(children, props, this.props.path) : children(props) : null} </RouterContext.Provider> </RouterContext.Consumer> ); } }三項演算子の連打でちょっとイヤになりますが、上から順に見ていきます。
まず、props.matchを評価します。matchしている(=現在のpathとRouteコンポーネントで指定しているpathが同じ)場合、Routeコンポーネントが子コンポーネントを持っているかを見ます。持っている場合、そのchildrenは関数かどうかをチェックします。関数ならば実行する必要があるためです。
Routeコンポーネントが子コンポーネントを持っていなかった場合、propsにcomponentがあるかどうかを見ていきます。あるならば、React.createElementでそのcomponentを描画します。component propsが記述されていなかった場合、renderの行に進むのですが、このrenderがどういった評価になるのかがちょっとわかりません。。。ただ、trueの場合はpropsをrenderし、falseの場合はnull、つまり何も描画しないという挙動になるようです。
ここで三項演算子の頭に戻って(props.match)、pathにmatchしていなかった場合は、childrenの存在を見て、かつそれが関数であるかどうかを確認します。関数であれば実行し、そうでなければnullに到達して一連の三項演算子の評価はおしまいです。
Switch.jsで現在のpathを見て描画すべきRouteコンポーネントを絞り込み、Route.jsはどのようにレンダリングするかを担当する、みたいな感じかと思われます。
Link.js
ここまでの3つのコンポーネントを配置すればルーティングのロジックは稼働しますが、このLink.jsも大切なコンポーネントです。Linkコンポーネントはページ間遷移をサーバーへのリクエストなしで実現するコンポーネントです。
HTMLのaタグをクリックするとイベントが発行され、サーバーへのリクエストが発生します。すると、リクエスト → レスポンス → レンダリングのような流れが再度実行され、これらは基本的に同期的に実行されるので、ユーザーはその時間分、待たされることになります(いわゆるSPAではないWebサイト)。
なので、SPAの内部ページ遷移にはaタグはそのままでは使えません。Linkコンポーネントは、aタグをラップし、サーバーへのリクエストイベントをキャンセルするコンポーネントになっています。その実装は以下です。
Link.js/** * The public API for rendering a history-aware <a>. */ const Link = forwardRef( ( { component = LinkAnchor, replace, to, innerRef, // TODO: deprecate ...rest }, forwardedRef ) => { return ( <RouterContext.Consumer> {context => { invariant(context, "You should not use <Link> outside a <Router>"); const { history } = context; // 遷移先のpathを生成(/example) const location = normalizeToLocation( resolveToLocation(to, context.location), context.location ); // 完全なURLを生成(https://example.com/example) const href = location ? history.createHref(location) : ""; const props = { ...rest, href, navigate() { const location = resolveToLocation(to, context.location); const method = replace ? history.replace : history.push; // path遷移を実行 method(location); } }; // React 15 compat if (forwardRefShim !== forwardRef) { props.ref = forwardedRef || innerRef; } else { props.innerRef = innerRef; } return React.createElement(component, props); }} </RouterContext.Consumer> ); } );これもざっくり見ていきます。
まずforwardRefというのが出てきます。これは子コンポーネント内のDOM要素にアクセスするための機能で、useRefに近しい感じです。useRefの場合、子コンポーネント内の特定のDOM要素にアクセスすることはできませんが(親から見ると内部実装は隠蔽されているので)、forwardRefの場合はそれが可能になるようです。
LinkコンポーネントでforwardRefが使われているのは、この機能を実現するためだと思いますが、自身でこの機能を使ったことはないので、ちょっと具体的なイメージは湧きづらいです。
navigate()の中身を見ると、history.replace、history.pushの記述があります。これらは少し上で受け取ったhistoryオブジェクト内に実装されているメソッドで、その途中にwindow.locationにページ履歴を追加する処理があります。これにより、SPAにおいてもブラウザの「戻る」や「進む」の機能が正しく動作するようになります。
ただ、最後まで読み進めてもaタグのイベントをキャンセルする処理はおろか、aタグすら出てきません。それらはどこで実装されているかというと、引数で渡されているLinkAnchorです。
LinkAnchorの記述はLinkの直上にあり、以下のようになっています。
Link.jsconst LinkAnchor = forwardRef( ( { innerRef, // TODO: deprecate navigate, onClick, ...rest }, forwardedRef ) => { const { target } = rest; let props = { ...rest, onClick: event => { try { // LinkコンポーネントにonClick propsが記述されていた場合はそれを実行 if (onClick) onClick(event); } catch (ex) { event.preventDefault(); throw ex; } // aタグのイベント発行をキャンセルし、navigate()を実行 if ( !event.defaultPrevented && // onClick prevented default event.button === 0 && // ignore everything but left clicks (!target || target === "_self") && // let browser handle "target=_blank" etc. !isModifiedEvent(event) // ignore clicks with modifier keys ) { event.preventDefault(); navigate(); } } }; // React 15 compat if (forwardRefShim !== forwardRef) { props.ref = forwardedRef || innerRef; } else { props.ref = innerRef; } // aタグに上記のpropsを付与してreturn /* eslint-disable-next-line jsx-a11y/anchor-has-content */ return <a {...props} />; } );LinkAnchorには本命のaタグのイベント発行をキャンセルする処理が記述されています。
onClickで発行されるイベントをキャンセルすればOKなのですが、開発者が自身でLinkコンポーネントのonClickハンドラに何かしらの処理を追加しているかもしれません。それも全てキャンセルしてしまうのは使い勝手が悪いので、ユーザーが追加したonClickについては実行されるようになっています。
次のif文の中で4つの条件が評価されており、trueだった場合はイベント発行をキャンセル、つまりサーバーへのリクエストを送らないようにしています。代わりにnavigate()を実行していますが、この具体的な処理はLinkコンポーネント側に記述されていました。(
const method = replace ? history.replace : history.push;
の箇所)これらの実装により、Linkコンポーネントは「サーバーへのリクエストが起こらないaタグ」のような振る舞いになっています。
まとめ
react-routerのリポジトリには他にもたくさんのファイルがあり、非常に多機能なライブラリなので、全部を見ていくのはさすがに難しかったのですが、上記の4コンポーネントについて知るだけでもSPAにおけるルーティングのロジックが掴め、非常によかったです。
おそらくですが、他のルーティングライブラリも似たような実装になっていると思うので、自身が使っているライブラリの中身を見てみると面白いかもしれません。
- 投稿日:2020-02-12T11:41:40+09:00
TypeScript + React + Sass + Babelを利用したWebpack環境構築
TypeScript + React + Sass + Babel利用したWebpack環境構築
最近, TypeScriptを勉強し始めたので
今回はTypeScript + React + Sass + Babelを利用した
Webpackの環境構築を行っていきたいと思います。前提
- ターミナルが利用できる
- Node.jsを利用環境がある
- npm, yarnの利用環境がある
環境
2020/2/12
時点での最新モジュールNode v11.10.1
ES6+
TypeScript v3.7.4
React.js v16.12.0
Webpack v4.41.2
ファイル構成
前準備
以下の
package.json
を作成し,各npmモジュールを
npm install
もしくは,yarn add package.json
を利用してインストールしてください。package.json{ "name": "", "version": "1.0.0", "description": "", "main": "Main.tsx", "scripts": { "build": "webpack", "start": "webpack-dev-server" }, "author": "", "license": "ISC", "dependencies": { "webpack": "^4.41.2", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.9.0", "@babel/core": "^7.7.4", "@babel/preset-env": "^7.7.4", "@babel/preset-react": "^7.7.4", "@types/react": "^16.9.17", "@types/react-dom": "^16.9.4", "@types/react-router-dom": "^5.1.3", "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "babel-minify-webpack-plugin": "^0.3.1", "core-js": "3", "css-loader": "^3.2.0", "html-webpack-plugin": "^3.2.0", "mini-css-extract-plugin": "^0.9.0", "node-sass": "^4.13.0", "package.json": "^2.0.1", "postcss-loader": "^3.0.0", "react": "^16.12.0", "react-dom": "^16.12.0", "react-router-dom": "^5.1.2", "sass-loader": "^8.0.0", "ts-loader": "^6.2.1", "typescript": "^3.7.4" }, "devDependencies": { } }これで環境構築に必要なモジュールが全て
node_modules
配下に
インストールされました。今回はメイン環境構築ですので, ささっとしたい方については
srcディレクトリ配下のファイル
とMain.tsx
について,
こちらを参照して各ディレクトリ上に作成してください。
https://github.com/olt556/react_hooks_ts_tmpWebpackの設定
次にWebpackの設定ファイルとなる,
webpack.config.js
を作成していきます。webpack.config.jsconst path = require('path'); // Babelの機能のminifyを利用するため const BabelMinifyPlugin = require("babel-minify-webpack-plugin"); // ビルドする際にHTMLも同時に出力するため const HtmlWebpackPlugin = require('html-webpack-plugin'); // CSSをJSにバンドルせずに出力するため const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { mode: 'development', // pathの設定についてですがpathモジュールを使う必要は特にはありません。 entry: path.resolve(__dirname, 'src/Main.tsx'), output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, devServer: { contentBase: path.resolve(__dirname, 'dist'), port: 8080, historyApiFallback: true, // これがないとルーティングできない }, resolve: { modules: [path.resolve(__dirname, 'src'), path.resolve(__dirname, 'node_modules')], extensions: ['.ts', '.tsx', '.js'] }, module: { rules: [ // scssのローダ設定 { test: [/\.css$/, /\.scss$/], exclude: /node_modules/, loader: [MiniCssExtractPlugin.loader, 'css-loader?modules', 'postcss-loader', 'sass-loader'], }, // js,ts,tsxのローダ設定 { test: [/\.ts$/, /\.tsx$/, /\.js$/], loader: ['babel-loader', 'ts-loader'], }, ], }, plugins: [ new BabelMinifyPlugin(), new HtmlWebpackPlugin({ publicPath: 'dist', // ビルド後のHTMLの出力先 filename: 'index.html', //出力するHTMLのファイル名 template: 'src/html/index.html', //出力するためのHTMLのテンプレート }), new MiniCssExtractPlugin({ publicPath: 'dist', // ビルド後のCSSの出力先 filename: 'app.css', //出力するCSSのファイル名 }), ], }
webpack.config.js
は以上のようになります。
ちなみに, ローダの設定についてですが,
loader: ['babel-loader', 'ts-loader']
の場合,ts-loader
→babel-loader
の順に読み込まれます。Babelの設定
Babelの設定は
.babelrc
に記述していきます。{ "presets": [ ["@babel/preset-env", { //babelの設定 "useBuiltIns": "usage", "corejs": 3 // polyfill用の設定 }], "@babel/preset-react", // react用のbabelの設定 ["minify",{}] // minifyの設定 ] }TypeScriptの設定
続いてTypeScriptのトランスパイル(コンパイル)設定を
tsconfig.json
に
記述してきます。tsconfig.json{ "compilerOptions": { "sourceMap": true, "noImplicitAny": true, "allowJs": true, "strictNullChecks": true, "module": "ES6", "target": "es5", "jsx": "react" }, "include": [ "src" ], "exclude": [ "node_modules" ], }各々の
compilerOptions
のオプションについては,
こちらを参照するといいかもしれません。
https://qiita.com/ryokkkke/items/390647a7c26933940470AutopreFixerの設定
AutopreFixer
はPostCSSの機能ですので,
利用できるようPostCSSの設定をpostcss.config.js
に記述していきます。postcss.config.jsmodule.exports = { plugins: [ require("autoprefixer")({ grid: "autoplace", browsersList: ["ie >= 11"] }) ], };ビルドについて
以上の設定ファイルを作成したのち,
npm run build
を実行することで,
エントリーポイントであるMain.tsx
とsrc配下のファイル
を
読み込んでビルドを行い,dist
以下に出力することができます。また,
npm run start
でリアルタイムでビルドを行い,
出力されたファイルが読み込まれた, ローカルサーバが
htttp://localhost:8080
で起動します。おわりに
間違えや質問などありましたら,
お気軽にコメントしていただけると幸いです。
今回はTypeScriptのテンプレートについては端折っているため,
そちらの記述方法などは,
GitHubを参照していただけたらと思います。
- 投稿日:2020-02-12T07:55:14+09:00
ReactでTypeScriptを使い始めたときに知りたかった@types/reactの用途別使い方
はじめに
reactの型定義ファイルには普段の開発では見かけることの少ない型が多く定義されています。
これらを使いこなすことでany
を使わずにきれいにコンポーネントを定義できるケースがあります。今回は3つシチュエーションとそのときに使うと便利な型をサンプルコードと合わせて紹介します。
propsで要素を受け取って表示したいとき
- propsの型に
React.ReactNode
を使うExample1interface Props1 { leftElement: React.ReactNode; rightElement: React.ReactNode; } const Example1:React.FC<Props1> = ({leftElement, rightElement}) => ( <div> <div>{leftElement}</div> <div>{rightElement}</div> </div> ) const Main:React.FC = () => ( <Example1 leftElement={<p>left</p>} rightElement={'right'} /> )
React.ReactNode
はReactElement | number | string | boolean | undefined | null
と定義されています。1
React.ReactElement
はタグで囲んだ要素(例:<p>要素</p>)を示します。Example1のコードでは
leftElement
やrightElement
の型にReact.ReactNode
を指定することで、文字列や数値,<p>left</p>
のようなReactElementを受け取ることができます。タグ名をpropsで受け取って動的に変更したいとき
- propsの型に
React.ElementType
を使うExample2interface Props2 { tag?: React.ElementType; } const Example2: React.FC<Props2> = ({tag: Tag = 'div', children}) => ( <Tag>{children}</Tag> ) const Dummy: React.FC = () => ( <p>dummy</p> ) const Main2: React.FC = () => ( <> <Example2>div</Example2> {/* <div>div</div> */} <Example2 tag={'p'}>p</Example2> {/* <p>p</p> */} <Example2 tag={Dummy}></Example2> {/* <Dummy></Dummy> => <p>dummy</p> */} </> )
React.ElementType
は'span'や'div'などHTMLタグの文字列 | React.ComponentClass | React.FC
と定義されています。1
React.ComponentClass
はReact.Component
を継承したクラスのことで、クラスコンポーネント形式で書かれたコンポーネントの型はReact.ComponentClass
になります。Example2のコードではpropsのtagに
React.ElementType
を指定していおり、tagは'p'などのHTMLタグだけでなく独自に定義したコンポーネントを受け取ることもできます。HTML属性と同じpropsを持ったコンポーネントを作りたいとき
React.DetailedHTMLProps<React.HogeHTMLAttributes<HTMLHogeElement>,HTMLHogeElement>
でHTML属性の型を取得して、propsを定義するExample3type Props3 = { name: string } & React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> const Example3: React.FC<Props3> = ({name, href, ...props}) => { return <a href={href} {...props}>{name}</a> } const Main3: React.FC = () => ( <Example3 name={'main3'} href={'google.com'} /> )
React.DetailedHTMLProps
はジェネリックスに応じてHTML属性のそれぞれの型を示します。
見出し下に示した文のHoge
を型定義ファイルで定義された、それぞれのHTMLタグに応じた名前にすれば取得できます。
例えば、aタグであればAnchor
、formタグであればForm
になります。
説明しやすくするために型定義ファイルの定義とは違う型で説明しています ↩
- 投稿日:2020-02-12T02:56:38+09:00
CORS回避と、ReactからAPIへのPOSTリクエストが正常に処理されなかった話
目的
ReactからaxiosでPOSTのAPIを叩きたい。
環境
React:ローカルの開発環境 http://localhost:3000/、
API:PHPで書かれており、開発サーバにおいてある https://api.example.com/***
同じドメイン上に無い環境です。状況
ReactからaxiosでAPIにPOSTリクエストを投げているが、CORSエラーで弾かれます。
また、axiosからPOSTリクエストが正常に投げられていない、もしくはPHPが正常にPOSTリクエストを受け取れていないような挙動をします。
こんな状況です。
私は基本的にPHPの開発がメインのため、Reactは初心者です。
もちろんaxiosも初めて使います。javascriptでAjax処理は書いたことがあるものの、コピペで動けば良い程度の使い方しかしませんでした。
また、とりあえずの解決は出来たものの、各機能の仕様を理解した上での対策ではないため記事に誤りがある可能性しかありません。
ご理解ください。エラー1 CORSエラー
Access to XMLHttpRequest at 'https://api.example.com/****/****' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.対処
ググった結果
・fetchを使った{ mode : cors }で回避できるらしい
・axiosの設定を変えると回避できるらしい
・API側のheaderを弄れば回避できるらしい
・Chromeの拡張機能でCORS回避できるらしい
という記事を多数見つけ、拡張機能をなるべく使いたくなかったため、とりあえずReact側のコードをいじってみることに。
が、この対処法が悪手でした。この時はまだ、「CORSってなんぞや???」という状況です。
reactimport axios from 'axios' axios.defaults.baseURL = 'http://localhost:3000' axios.defaults.headers.post['Content-Type'] = 'application/json;charset=utf-8' axios.defaults.headers.post['Access-Control-Allow-Origin'] = 'https://api.example.com'エラーが変わら無いため、API側に下記コードを追加します。
phpheader("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Headers: X-Requested-With, Origin, X-Csrftoken, Content-Type, Accept"); header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH, HEAD");エラー2 別のCORSエラー
その後も コード変更 → エラー → コード変更 → エラーを何回か繰り返しました。
Access to XMLHttpRequest at 'https://api.example.com/****/****' from origin 'http://localhost:3000' has been blocked by CORS policy: Request header field access-control-allow-origin is not allowed by Access-Control-Allow-Headers in preflight response.対処
結局あきらめて以下の拡張機能を導入することに・・・。
ちなみにこの機能を有効化した状態でTwitterを開くと「Twitterおかしい?」というツイートができなくなるので、拡張機能の有効範囲を[ http://localhost:3000/ ]だけに限定する等の対策が必要です。
エラー3 明らかにおかしい分岐に入る
拡張機能入れたしこれで大丈夫!!
と思いつつ試してみたのですが、いまだエラー解消ならず・・・Access to XMLHttpRequest at 'https://api.example.com/****/****' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.ただ、エラー文に気になることが書かれています。
It does not have HTTP ok status.
APIがok status(200)を返していないということは、PHP側で弾いている・・・?
原因の切り分け
ステータスコードを調べるためにデベロッパーツールで見てみることに。
ただし、failedとしか書いておらず、詳細を見ても不明とのこと。
仕方ないのでAPI側で例外処理をスキップすることに。
phptry{ if(!is_post_request()){ throw new Exception("ERROR_INVALID_METHOD"); } ... ... }catch(Exception $e){ switch($e->getMessage()){ case "ERROR_INVALID_METHOD": ... // コメントアウト!! // set_header( HEADER_TYPE_400 ); break; case "ERROR_INVALID_PARAMETER": ... // コメントアウト!! // set_header( HEADER_TYPE_400 ); break; ... default: set_header( HEADER_TYPE_500 ); } }例外を投げている箇所を特定してみると・・・
phpif(!is_post_request()){ throw new Exception("ERROR_INVALID_METHOD"); }どうやらPOST以外のmethodでリクエストを受け取っており、そのせいで400 Bad Requestを返しているらしい。
・・・・!?!?!?
axiosはPOSTを投げていないのか!?!?それともPHPが正常にPOSTを受け取れていないのか!?!?
ちなみにis_post_request()はこんな感じ。
phpfunction is_post_request(): bool { if( $_SERVER['REQUEST_METHOD'] === "POST" ) { return true; } return false; }対処
さて困ったどうしたものか・・・
「axios初めて使うしなあ・・・しっかりPOST送れてるかな~・・・」
「まさかPHPがPOSTの判別を出来ないわけないよな~・・・」
「まあPHP側は大丈夫だと思うけどな~・・・」
「axiosとかcorsとかわかんねーよ・・・」
などと考えつつとりあえず原因がPHPに無いことを確定させたかったため、printデバッグしてみることに。phpif(!is_post_request()){ // 2行追加 var_dump($_SERVER); exit; throw new Exception("ERROR_INVALID_METHOD"); }とりあえずこれで$_SERVER['method']に"POST"が入ってることが確認できるだろうと思いながらReactのページを見てみると・・・
おお!正常にjsonが返ってきた!
良かったよかった、これで先に進める・・・ え????????「いやいやexitで強制終了してるんだからjsonが帰ってくるのはおかしいでしょ」
「というか$_SERVERの中身が表示されてないんですけど・・・?」
「どうなってんのこれ!?!?」
大パニックです。。。
ということでもう一箇所にprintデバッグの追加phpvar_dump($_SERVER); exit; if(!is_post_request()){ var_dump($_SERVER); exit; throw new Exception("ERROR_JSON_INVALID_METHOD"); }ようやく$_SERVERの情報が取得できました。
["REQUEST_METHOD"]=> string(4) "POST"ただ、正常にPOSTで受け取っているようです。
「もしやis_post_request()関数が正常に機能していない・・・?」
ということになり、php// もとに戻した if(!is_post_request()){ var_dump($_SERVER); exit; throw new Exception("ERROR_JSON_INVALID_METHOD"); }phpfunction is_post_request(): bool { // printデバッグ die($_SERVER['REQUEST_METHOD']); if( $_SERVER['REQUEST_METHOD'] === "POST" ) { return true; } return false; }そして出力された文字列は
"POST"ほう・・・
「"POST" === "POST"がfalseになるん・・・??」
「POSTの判別方法間違えてる・・・?」
「そもそも文字列の比較ってこれでできるっけ・・・?」
「"POST" == "POST"のほうが良いのかな・・・」
「いや変わんないわ・・・」
「PHPのコンパイル時にdie, exitの挙動が不安定になる・・・?」
「そもそもPHPでfunctionって使っていいの・・・?」
再び大パニックprintデバッグの出力先をエラーログに
なにか良いデバッグ方法は・・・と考えているときに
「これ本当にPOST投げてる?」
「というかPOST以外のリクエストも同時に投げてる?」
という疑問が湧き上がり、printデバッグの出力先を変えてみることに。phperror_log($_SERVER['REQUEST_METHOD'],0); if(!is_post_request()){ var_dump($_SERVER); exit; throw new Exception("ERROR_INVALID_METHOD"); }error_log[Tue Feb 11 17:34:14.278789 2020] [:error] [pid 15147] [client **.**.**.**:38309] OPTIONS, referer: http://localhost:3000/ [Tue Feb 11 17:34:14.664012 2020] [:error] [pid 15147] [client **.**.**.**:38309] POST, referer: http://localhost:3000/案の定1回のリクエストでOPTIONS、POSTの順番にリクエストが投げられてました。
ということで原因はエラー1の対処で追加したコードの部分でした。
詳しくは省きますが、preflight requestの仕組みが悪さをしていたようです。
貴重な休日を真夜中(2:30頃)まで潰したCORSを私は許さない。。。
あとがき
その後もCORS関連のコードいじってたら結局拡張機能も要らなくなったため消しちゃいました。
デバッグの手法を教えるのって難しいですよね。
echoやdieでのprintデバッグを知っていたとしても、error_logでOSのシスログにエラーを投げる方法は知らなかったりとか。
かといって便利だからとあれもこれも教えても(教えられても)覚えきれないから意味がないみたいな。
こればっかりは場数ですかね。あと、Twitterを開けない時ってめちゃくちゃ辛くないですか?
「Twitter開けない」ってつぶやきたいのにつぶやけないこの気持ち。
それはまるで好きな子に正直に好きと言えない初恋のような甘酸さ。最後まで見ていただきありがとうございました。
今恋してるよーって人はいいねお願いします。