XXとしてる
2019年4月10日水曜日
How to Build a Simple Web App with React, Graphql, and Goを読んだ
GolangとReactで作ったサイトを作り直すのでこちらのサイトを読んでみた。 [How to Build a Simple Web App with React, Graphql, and Go](https://medium.com/@chrischuck35/how-to-build-a-simple-web-app-in-react-graphql-go-e71c79beb1d) 以下、サイトの内容の流れとメモ書き ## はじめに Golangのバージョンは1.11に上げたので書き直したよ。 Gloangはサイコーでパワフルな言語だから、次のプロダクトではNode.jsではなくてGoを使って作ってみるよ。 ここではReactとGraphql、Golangを使ってnot-todoリストサイトを作ってみる。 Githubにリポジトリにあげているので何かあったら参考にしてね! [React-Go-Graphql-Example](https://github.com/Chrischuck/React-Go-Graphql-Example)
※訳注 MongoDBも使ってます。
## 前準備 nodejsとGolang、depをインストールしておくこと。 データーベースにMongoDBクラウドサービスのMLabを使っているけど、好きなものを使ってね。 一応、このサイトの内容は無料プラン内で収まるから安心だね! プロジェクトのディレクトリ構造はこんな感じにするよ。 ```js |--client |--server | |--src | | |--app | | | |--main.go ``` GOPATHもプロジェクトに合わせておくことを忘れないように。 ## サーバーサイド ### 初期化 次のコマンドでサーバーサイドの初期化を行う。 `dep`はgolangのプロジェクト単位のパッケージ管理ツールになる。 `go get`とは違って他のプロジェクトに影響を与えないから便利。 ```bash cd server/src/app dep init ``` ### シンプルなRESTサーバー まずは簡単なサーバーを作ってみよう! Golangには標準ライブラリにHTTPサーバーを建てるパッケージがあるのでそれを使おう! ```js // main.go package main import ( "log" "net/http" ) func main() { http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("Hello Mars!") })) log.Println("Now server is running on port 3000") http.ListenAndServe(":3000", nil) } ``` `go run main.go`でサーバーを起動しよう。 `localhost:3000`にリクエストを送るとサーバーの出力に'Hello Mars!'が書き込まれるよ。 ## データベース 次にデータベースを建てよう! GolangのMongoDBドライバーがあるからそれをプロジェクトにインストールしよう。
※記事が書かれてからMongoDBで更新があったみたいなのでMongoDB周りは次のパッケージを使用しています
[MongoDB Go Driver](https://github.com/mongodb/mongo-go-driver) ```bash dep ensure -add "go.mongodb.org/mongo-driver/mongo@~1.0.0" ``` 追加したら、`server/src/app/data/mongo.go`を新しく追加しよう。 ```js //server/src/app/data/mongo.go package mongo import ( "context" "time" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) var Client, err = mongo.NewClient(options.Client().ApplyURI("Insert your MongoDB URI here!")) var ctx, _ = context.WithTimeout(context.Background(), 10*time.Second) var errConnect = Client.Connect(ctx) ``` Client変数は頭文字が大文字になっているからエクスポートされる。 ## Graphql お次はGraphqlの準備だ! こちらは`main.go`を修正していく。 ```js package main import ( "log" "net/http" "github.com/graphql-go/graphql" "github.com/graphql-go/handler" "app/queries" "app/mutations" ) var schema, _ = graphql.NewSchema(graphql.SchemaConfig{ Query: queries.RootQuery, Mutation: mutations.RootMutation, }) func main() { h := handler.New(&handler.Config{ Schema: &schema, Pretty: true, }) http.Handle("/graphql", disableCors(h)) log.Println("Now server is running on port 3000") http.ListenAndServe(":3000", nil) } func disableCors(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, Content-Length, Accept-Encoding") if r.Method == "OPTIONS" { w.Header().Set("Access-Control-Max-Age", "86400") w.WriteHeader(http.StatusOK) return } h.ServeHTTP(w, r) }) } ``` 修正したので、`/graphql`にリクエストを送るとなにか反応を返してくれるようになった。 `/graphql`で行う処理は`disableCors`関数が返すハンドルに書かれている。 そのハンドルの内容はCross-Origin Resource Sharing(CORs, オリジン間リソース共有)を無効にしている。 [こちらのIssueを参考にしている](https://github.com/graphql-go/graphql/issues/290) `graphql-go/graphql`と`graphql-go/handler`パッケージが必要に成るの`dep`に登録しよう。 `app/queries`と`app/mutations`は後で作るので覚えておいてね。 #### 訳注 CORsについて追記 CORsについては以下のサイトを参考になる。 Web開発者はみんな知っておくべき内容であるそうだ。 ブラウザ側が良きに働いてくれているが、新しい標準仕様はサーバー側で管理しないといけないので必須知識である。 [オリジン間リソース共有 (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS) [サーバーサイドアクセス制御 (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/Server-Side_Access_Control) [Access-Control-Allow-Origin](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) [Access-Control-Allow-Methods](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) [Access-Control-Allow-Headers](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) [Access-Control-Max-Age](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Max-Age) `disableCors`関数を見たとき一見無効にしてなさそうに見える。 #### Access-Control-Allow-Origin が、`Access-Control-Allow-Origin`を`*`に指定することで、資格情報があるときエラーを返すようになる。 (CORSリクエストの認証フラグをオフにすればエラーは起きない) `null`も設定できるがこちらは使用しないよう推奨されている。 詳しい説明が上のAccess-Control-Allow-Originリンクから見ることができる。 #### Access-Control-Allow-MethodsとAccess-Control-Allow-Headers `Access-Control-Allow-Methods`に指定したHTTPメソッド(POST, GET, OPTIONS, PUT, DELETE)はCORsを禁止される そして、`Access-Control-Allow-Headers`で指定したHTTPヘッダーもCORsで使うことができなくなる。 #### Access-Control-Max-Age `Access-Control-Max-Age`はプリフライトリクエストの結果をキャッシュできる時間を示す。 プリフライトリクエストとは実際にリクエストを送る前にOPTIONSメソッドによるリクエストを他のドメインに送り、 リクエストを送っても大丈夫か確認するリクエストのことである。 ブラウザごとにキャッシュ可能な時間の上限が異なるみたいなので、あくまで最長時間と見るべき。 #### CORsの通信の流れ 以上レスポンス側の設定項目について書いてきたが、実際にはリクエスト側にもCORs関係のヘッダー項目もあるので、上だけをみて理解は出来ないと思う。 CORsには以下の3つのシナリオが存在している。 - 単純リクエスト: プリフライトリクエストを引き起こさないリクエスト - プリフライトリクエスト: プリフライトリクエストを始めに行うリクエスト - 資格情報を含むリクエスト: 認証情報付きのリクエスト どれになるかはリクエストの内容次第で[オリジン間リソース共有 (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)に書かれている。
CORsについて追記 終わり
## depを使ったパッケージ管理 `graphql-go/graphql`と`graphql-go/handler`のインストールは下のコマンドを実行するだけでいい。 ```bash dep ensure ``` 簡単でしょう! インストールするパッケージ名を何も書いてないじゃないかと思うかもしれないが、`dep`はソースコードにあるのを見てうまいことしてくれる。 便利! ## main.goの分割 さて、main.goもごちゃごちゃしてきたから、機能ごとに分割したい。 なので次のようなディレクトリ構造にしてほしい。 ``` |--client |--server | |--src | | |--app | | | |--data | | | |--mutations | | | |--queries | | | |--types | | | |--main.go ``` `server/src/app`の`mutations`と`queries`、`types`にGraphqlのコンポーネントを分けていきたい。 ### Types Graphqlのための型定義は`types`ディレクトリに入れていく。 それじゃ、さっそく`types/notTodo.go`を作っていこう。 `types/notTodo.go`にはnot-todoオブジェクトをモデリングしていく。 ```js //types/notTodo.go package types import ( "github.com/graphql-go/graphql" ) var NotTodo = graphql.NewObject(graphql.ObjectConfig { Name: "NotTodo", Fields: graphql.Fields{ "name": &graphql.Field{ Type: graphql.String, }, "description": &graphql.Field{ Type: graphql.String, }, }, }) ``` ### Queries 次にクエリを用意する。これは`Graphql-go`で要求されているから作る。 Graphqlではデータを受け取ることにクエリを使っている。 クエリには使いたいパラメータをはっきり書く必要があって、Graphqlはクエリにあるものだけを送る。 ```js //queries/queries.go package queries import ( "github.com/graphql-go/graphql" fields "app/queries/fields" ) var RootQuery = graphql.NewObject(graphql.ObjectConfig{ Name: "RootQuery", Fields: graphql.Fields{ "getNotTodos": fields.GetNotTodos, }, }) ``` `queries/queries.go`は`queries/fields`にあるパッケージを使っている。 それじゃ、実際に使っている`fields.GetNotTodos`を作ろう! その内容は`queries/fields/getNotTodos.go`に書いていく ```js //queries/fields/getNotTodos.go package queries import ( "context" "github.com/graphql-go/graphql" "github.com/mongodb/mongo-go-driver/bson" "app/data" types "app/types" ) type todoStruct struct { NAME string `json:"name"` DESCRIPTION string `json:"description"` } var GetNotTodos = &graphql.Field { Type: graphql.NewList(types.NotTodo), Description: "Get all not todos", Resolve: func(params graphql.ResolveParams) (interface{}, error) { notTodoCollection := mongo.Client.Database("medium-app").Collection("Not_Todos") ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) todos, err := notTodoCollection.Find(ctx, bson.D{}) if err != nil { fmt.Println(err) panic(err) } defer todos.Close(ctx) var todosList []todoStruct for todos.Next(ctx) { doc := todoStruct{} err := todos.Decode(&doc) if err != nil { fmt.Println(err) panic(err) } todosList = append(todosList, doc) } return todosList, nil }, } ``` MongoDBからデータを全てのドキュメントを取ってきて、BSON形式からGolangで使うデータに変換している。 ### Mutations 最後に新しいnot-todoを作るためにミューテーションを作るよ。 ミューテーションは変化するデータに使われる。 今回はクエリを使ってデータを変更しているけど、いい設計とは言えないことを覚えておいてね。 ```js //mutations/mutations.go package mutations import ( "github.com/graphql-go/graphql" fields "app/mutations/fields" ) var RootMutation = graphql.NewObject(graphql.ObjectConfig{ Name: "RootMutation", Fields: graphql.Fields{ "createNotTodo": fields.CreateNotTodo, }, }) ``` MutationsもQueriesと同じく`mutations/fields`ディレクトリにあるパッケージを使っている。 なので、早速`createNotTodo.go`を作ろう。 ```js //mutations/fields/createNotTodo.go package mutations import ( "github.com/graphql-go/graphql" "context" "app/data" types "app/types" ) type todoStruct struct { NAME string `json:"name"` DESCRIPTION string `json:"description"` } var CreateNotTodo = &graphql.Field { Type: types.NotTodo, Description: "Create a not Todo", Args: graphql.FieldConfigArgument { "name": &graphql.ArgumentConfig { Type: graphql.String, }, "description": &graphql.ArgumentConfig { Type: graphql.String, }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { // get our params name, _ := params.Args["name"].(string) description, _ := params.Args["description"].(string) notTodoCollection := mongo.Client.Database("medium-app").Collection("Not_Todos") ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) _, err := notTodoCollection.InsertOne(ctx, map[string]string{"name": name, "description": description }) if err != nil { panic(err) } return todoStruct{name, description}, nil }, } ``` これにてGolangによるGraphqlのバックエンドを作ることが出来た! さぁ、`go run main.go`を実行してサーバーが動くかどうか確認しよう! ## React 次はフロントエンドを作っていこう。 ### 初期化 次のコマンドでフロントエンド側の開発環境を構築する。 ```bash cd ../../../client npm init -f ``` 開発時に使う依存パッケージは次のものだ。 BabelとWebpack関係のものをインストールする。 ```bash npm install --save-dev @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-syntax-dynamic-import @babel/preset-env @babel/preset-react @babel/polyfill "babel-loader@^8.0.0-beta" html-webpack-plugin webpack webpack-cli webpack-dev-server ``` リリース時にも使う依存パッケージは次になる。 ReactとGraphqlクライアントのようにApolloを使う。(Asyncルーティングには後で入れる) ```bash npm install --save apollo-boost graphql react react-apollo react-dom react-loadable react-router-dom ``` 必要なパッケージをインストールしたら、`package.json`にスクリプトを定義しよう。 ```json "scripts": { "dev": "webpack-dev-server --mode development", "build": "webpack --mode production" } ``` ファイル階層は次のようになる。 ``` |--client | |--node_modules | |--src | | |--components | | |--routes | | |--index.html | | |--index.js | |--.babelrc | |--.package.json | |--.webpack.config.js |--server ``` 確認できたら次に進もう! `.babelrc`は次の内容にする。 ``` { "presets": ["@babel/preset-env", "@babel/preset-react"], "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-class-properties", {"loose": true}], "@babel/plugin-syntax-dynamic-import" ] } ``` `webpack.config.js`は次のように。 ```js const HtmlWebPackPlugin = require('html-webpack-plugin'); const htmlWebpackPlugin = new HtmlWebPackPlugin({ template: './src/index.html', filename: './index.html' }); module.exports = { entry: [ '@babel/polyfill', './src/index.js', ], output: { filename: 'app.js', path: __dirname + '/dist' }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader" } }, { test: /\.css$/, use: [ { loader: "style-loader" }, { loader: "css-loader", options: { modules: true, importLoaders: 1, localIdentName: "[name]_[local]_[hash:base64]", sourceMap: true, minimize: true } } ] } ] }, devtool: 'eval-source-map', // ← 注釈参考 devServer: { historyApiFallback: true, }, plugins: [htmlWebpackPlugin] }; ``` ※注釈 webpack.config.js絡みの警告 ブラウザで表示すると`TypeError: ’xxx’ is not a valid URL. ...`と大量の警告が出てきたので以下のページを参考に修正しています。 [How to avoid “TypeError: popper.js.map is not a valid URL” message](https://stackoverflow.com/questions/54774650/how-to-avoid-typeerror-popper-js-map-is-not-a-valid-url-message)
注釈終わり
さぁ、Reactを使ってフロントエンド側を作っていこう! ### React `src/index.html`は次のコードをペーストしてね。 ```html
``` そして、`src/index.js`は次の内容でしよう。 ```js import React from "react"; import ReactDOM from "react-dom"; ReactDOM.render(
Hello World!
, document.getElementById('app') ); ``` これで基本的なReactアプリが完成! `npm run dev`を実行して、`localhost:8080`にアクセスしてみよう。 "Hello World!"って画面に出るはずだ。 ### Routing お次はReact Routingを使ってアプリのルーティングを追加しよう。 React Routetを使っているからすぐにでも非同期ルーティングを実装することが簡単にできるんだ。 React RouterはReact Loadableを作り直したものなんだって。 それじゃまず先に`loading.js`を`src/components`ディレクトリに作ろ! このReactコンポーネントはページ切り替えのとき、まだ読み込み待ちのときに表示されるものになるよ。 ```js //src/components/loading.js import React from 'react' const Loading = () =>
loading
export default Loading ``` よし、実際にルーティングを作っていこう。 `src/routes/home`ディレクトリを作って、その中に`index.js`と`home/home.js`を追加しよう。 ```js // src/routes/home/home.js import React from 'react' const Home = () => (
Home!
) export default Home ``` ```js //src/routes/home/index.js import React from 'react' import Loadable from 'react-loadable' import Loading from '../../components/loading' const LoadableComponent = Loadable({ loader: () => import('./home'), loading: Loading, }) const LoadableHome = () =>
export default LoadableHome ``` `index.js`で`react-loadable`パッケージの`Loadable`を使って`loading.js`と`home.js`を組み合わせているよ。 これで読み込み中に"loading"って画面に表示されるんだ。 #### 404ルーティング よし、次は404ルーティングを作ろか。 作り方は同じだよ。 `src/routes/notFound`ディレクトリを作って、その中に`index.js`と`notFound.js`を作るだけ。 ```js //src/routes/notFound/notFound.js import React from 'react' const NotFound = () =>
404 Not Found :(
export default NotFound ``` ```js //src/routes/notFound/indexjs import React from 'react' import Loadable from 'react-loadable' import Loading from '../../components/loading' const LoadableComponent = Loadable({ loader: () => import('./notFound'), loading: Loading, }) const LoadableNotFound = () =>
export default LoadableNotFound ``` #### 仕上げ よし、これで必要なルーティングは揃ったから仕上げとして、`src/routes/index.js`を作ろう。 ```js //src/routes/index.js import React from 'react' import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' import Home from './home' import NotFound from './notFound' class AppRouter extends React.Component { render() { return (
) } } export default AppRouter; ``` React Routerの`Route`コンポーネントのプロパティに私達が作ったルートを渡しているのがわかるかな? Homeルートは`localhost:8080/`でアクセスすることができるようにしているよ。 簡単だね! けどNotFoundルートはどうやってアクセスしたいいんだろ? じつはNotFoundルートはワイルドカードルートを使っていて、 例えば`localhost:8080/not-home`みたいに何も設定されていないルートにアクセスすることで出来るんだ。 よーし、最後の作業だ!`src/index.js`を次のように編集しよう! ```js //src/index.js import React from "react"; import ReactDOM from "react-dom"; import AppRouter from './routes' ReactDOM.render(
, document.getElementById('app') ); ``` 素晴らしい!!これでルーティングは完成だね。 早速`localhost:8080/`にアクセスしてみて出来ているか確認しよう! 404ページも忘れずにね。 ### Apollo ApolloはReactアプリでGraphqlを使うためのGraphqlクライアントだ。 FacebookからRelayという他のGraphqlクライアントもあるけど、僕はApolloが簡単で柔軟に使えると思っているよ。 それじゃ、Apolloを使うように`src/index.js`を修正しよう。 ```js //src/index.js import React from "react"; import ReactDOM from "react-dom"; import ApolloClient from 'apollo-boost'; import { ApolloProvider } from 'react-apollo'; import AppRouter from './routes' const client = new ApolloClient({ uri: 'http://localhost:3000/graphql' }); ReactDOM.render(
, document.getElementById('app') ); ``` 簡単でしょ?これでApolloの追加は終わったよ。 それじゃ、前に作ったサーバーがとコミュニケーションを取ろうか。 `src/routes/home/home.js`を次のように変更してね。 ```js //src/routes/home/home.js import React from 'react' import { gql } from 'apollo-boost'; import { graphql, compose } from 'react-apollo'; // example of a graphql query const query = gql` query GetNotTodos{ getNotTodos { name description } } ` // example of a graphql mutation const mutation = gql` mutation CreateNotTodo($name: String, $description: String) { createNotTodo(name: $name, description: $description) { name description } } ` @compose( graphql(query), graphql(mutation) ) class Home extends React.Component { render() { console.log(this.props) return (
Home still!
) } } export default Home ``` 上のコードは何してるんだろうね? まずHomeコンポーネントを関数形式から標準的なコンポーネントに変えたよ。 次に、これが魔法みたいなんだけど、`graphql`ラッパーを使ってHomeコンポーネントをラップしているんだ。 これで自動的にgraphqlのクエリを実行するようになるんだ。 (嘘はついていないよ、あまりに一瞬なことだからわからないだけで、ログを確認してみてね) 確認したら、Homeコンポーネントを次のように変更してね。 サーバーにあるnot-todoのリストを取得して表示するようにしているよ。 ```js class Home extends React.Component { render() { const { getNotTodos = [] } = this.props.data return (
name
description
{ getNotTodos.map(notTodo => (
{notTodo.name}
{notTodo.description}
)) }
) } } ``` OK!これで簡単なテーブルで名前と説明を表示できるようになったね。 ### ※注釈 Reactでリストの子要素にはKeyプロパティを与えないと警告がでる。 その際は添字ではなくユニークなキーに成るようにしたほうがアニメーションとかで不具合がでないので推奨される。 参考サイト [Why using an index as Key in React is probably a bad idea?](https://medium.com/@vraa/why-using-an-index-as-key-in-react-is-probably-a-bad-idea-7543de68b17c)
注釈終わり
次はnot-todoを追加できるようにしよう。 ```js class Home extends React.Component { constructor(props) { super(props) this.state = { name: '', description: '' } } onChange = event => { this.setState({ [event.target.name]: event.target.value}) } save = () => { this.props.mutate({ variables: { name: this.state.name, description: this.state.description }, refetchQueries: ['GetNotTodos'] }) } render() { const { getNotTodos = [] } = this.props.data return (
Add something to not do!
Save
name
description
{ getNotTodos.map(notTodo => (
{notTodo.name}
{notTodo.description}
)) }
) } } ``` 上のコードのコンストラクタでアプリに状態を持ち込んで、その状態を保存するためのボタンと機能を追加したよ。 保存する中でgraphqlのミューテーションを利用しているのがわかるかな? とても簡単に作れるでしょ? ## 終わり (実際に手を動かしながらいくつかのバグや警告を発見したので元の記事からコードを修正しています。)
0 件のコメント:
コメントを投稿
次の投稿
前の投稿
ホーム
登録:
コメントの投稿 (Atom)
0 件のコメント:
コメントを投稿