eyecatch
Fri, Mar 8, 2019

ズンドコキヨシ with Kubernetes Operator - KubebuilderでKubernetes Operatorを作ってみた

Javaの講義、試験が「自作関数を作り記述しなさい」って問題だったから 「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ!」って出力した後終了って関数作ったら満点で単位貰ってた — てくも (@kumiromilk) 2016年3月9日 久しぶりにズンドコしたくなったので、Kubebuilderを使って、KubernetesのOperatorとして動くZundoko Operatorを作ってみた。 (adsbygoogle = window.adsbygoogle || []).push({}); Kubernetes Operatorとは KubernetesのOperatorというのはCoreOS社(現Red Hat)によって提唱された概念(実装パターン)で、KubernetesのAPIで登録されるKubernetesオブジェクトの内容に従って何らかの処理をするController (e.g. Deployment Controller)の一種。 Controllerが汎用的なのに対して、特定のアプリケーションに特化しているのが特徴。 アプリケーションごとの細かな設定をKubernetesオブジェクトで表現するために、KubernetesのAPIを拡張する。 APIを拡張するにはAPI Aggregationを使う方法とCustom Resource Definition (CRD)を使う方法がある。 API Aggregationは、Kubernetesオブジェクトをetcd以外で管理したり、WebSocketを使ったり、Kubernetesクラスタ外のAPIサーバを使う場合など、特殊な場合にも対応できる高度なやりかたで、大抵のユースケースではCRDで事足りる。 Operatorも普通はCRDを使う。(というかCRDを使うのがOperatorという人もいる。) CRDとは KubernetesのAPIを簡単に拡張できる仕組みで、Kubernetesオブジェクト(リソース)を定義するKubernetesオブジェクト。 YAMLで、定義したいリソースの名前や型やバリデーションなんかを書いてkubectl applyすれば、そのリソースをKubernetesのREST APIとかkubectlで作成したり取得したりできるようになる。 Operatorの仕組み Operatorは、CRDで定義されたリソース(など)の作成、更新、削除を監視(watch)して、リソースの内容に応じた何らかの処理をするReconciliationループを回すPod。 普通、リソースはOperatorの管理対象のアプリケーションの状態を表す。 で、Operatorはリソースの内容とアプリケーションの状態が同じになるように、Reconciliationループ内でDeploymentを作ったりアプリケーションのAPIを叩いたりする。 ユーザとしては、アプリケーションの構成や設定をKubernetesのAPIで宣言的に統一的に管理できるようになって幸せになれる。 Operator作成ツール Operatorを作るツールとして以下がある。 ツール Operator SDK Kubebuilder Metacontroller 開発元 Kubernetesコミュニティ製 CoreOS社製 GKEチーム製 GitHubスター数 1459 1009 506 開発言語 Go、Ansible、Helm Go 任意 特徴 プロジェクトテンプレート生成、ビルド、デプロイをするCLIツール。AnsibleでもOperatorを書けるのが面白い。Operator FrameworkとしてLifecycle Managerなどが提供されていたり、OperatorHub.ioというコミュニティサイトがあったり、エコシステムが充実している。 プロジェクトテンプレート生成、ビルド、デプロイをするCLIツール。3つの中で一番シンプル。Goでしか開発できない。 他の2つと毛色が違って、Metacontroller自体が汎用的にOperatorを管理するKubernetesアプリ。Operatorの定義をJSONを投げて登録すると、Reconciliationループを回してその中でWebフックを実行してくるので、それを受けて任意の処理をするサーバを任意の言語で書ける。 この中では、Operator SDKが数歩リードしている感じ。 (CoreOS社を買収した)Red Hatが後ろ盾ているし、OperatorHub.ioはGCPとAWSとAzureが協力している。 けど、この記事のネタを書き始めたときにはまだOperatorHub.ioが発表されていなくて、単純にKubebuilderがシンプルでいいと思って採用してしまった。 まあOperator SDKもKubebuilderも下回りのライブラリは同じものを使っているので、だいたい同じだろうし、Operator Frameworkへの移行も難しくなかろう。 Zundoko Operator Kubebuilderで今回作ったのはZundoko Operator。 CRDで定義したリソースは以下。 Hikawa: 作るとズンドコきよしを開始する。 Zundoko: 「ズン」と「ドコ」を表す。Hikawaに管理される。 Kiyoshi: 「キ・ヨ・シ!」を表す。Hikawaに管理される。 Zundoko Operatorは、HikawaとZundokoをwatchする。 Hikawaが作成されると、一定間隔で、ランダムに「ズン」か「ドコ」をセットしたZundokoを作成する。 「ズン」を4つ作ったあとに「ドコ」を作ったら、Kiyoshiを作成して、Zundokoの作成を止める。 Kubebuilderの使い方 Quick Startを参考に。 Kuberbuilderを使うにはGo、depとkustomizeとDockerが必要で、Linuxしかサポートしていない。 自分のPCがWindows 10なので、WSL (Ubuntu 18.04)で環境を作ったんだけど、結局Dockerビルドとかテストとかが上手く動かなかったので、VMとかのLinuxで動かしたほうがよさそう。 Kubebuilderセットアップ Goインストール Goは公式サイトからLinux用アーカイブをダウンロードして展開して、そのbinディレクトリにPATH通すだけでインストールできる。 $ go version go version go1.11.4 linux/amd64 あと、作業ディレクトリを作ってGOPATHを設定しておく。 ~/go/を作業ディレクトリとする。 $ export GOPATH=$HOME/go $ echo 'export GOPATH=$HOME/go' >> ~/.profile $ mkdir $GOPATH/bin $ mkdir $GOPATH/src で、$GOPATH/binにもPATH通しておく。 depインストール Go公式の依存ライブラリ管理ツール。 コマンド一発でインストールできる。 $ curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh kustomizeインストール バイナリをPATHの通ったところにダウンロードするだけ。 $ curl -L https://github.com/kubernetes-sigs/kustomize/releases/download/v2.0.3/kustomize_2.0.3_linux_amd64 -o /usr/local/bin/kustomize $ chmod +x /usr/local/bin/kustomize kubebuilderインストール GitHubのReleasesからアーカイブをダウンロードして展開してPATH通すだけ。 $ version=1.0.6 $ arch=amd64 $ curl -LO https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${version}/kubebuilder_${version}_linux_${arch}.tar.gz $ tar -zxvf kubebuilder_${version}_linux_${arch}.tar.gz $ sudo mv kubebuilder_${version}_linux_${arch} /usr/local/kubebuilder $ export PATH=$PATH:/usr/local/kubebuilder/bin $ echo 'export PATH=$PATH:/usr/local/kubebuilder/bin' >> ~/.profile Dockerインストール は適当に… Kubebuilderプロジェクト生成 Zundoko Operatorのプロジェクトを生成する。 $ mkdir -p $GOPATH/src/github.com/kaitoy/zundoko-operator $ cd $GOPATH/src/github.com/kaitoy/zundoko-operator $ kubebuilder init --owner kaitoy dep ensureを実行するかを聞かれるのでyesで回答すると、依存ライブラリがダウンロードされ、プロジェクトのビルドが走る。 デフォルトではCRDなどの名前空間がk8s.ioになっているので、kaitoy.github.comに変えるべく、zundoko-operator/PROJECTを編集する。 zundoko-operator/PROJECT: version: "1" -domain: k8s.io +domain: kaitoy.github.com repo: github.com/kaitoy/zundoko-operator CRDとController生成 HikawaとZundokoとKiyoshiのCRDを生成する。 $ kubebuilder create api --group zundokokiyoshi --version v1beta1 --kind Hikawa $ kubebuilder create api --group zundokokiyoshi --version v1beta1 --kind Zundoko $ kubebuilder create api --group zundokokiyoshi --version v1beta1 --kind Kiyoshi それぞれ、リソースを作成するか (Create Resource under pkg/apis [y/n]?) と、Controllerを作成するか (Create Controller under pkg/controller [y/n]?) を聞かれる。 リソースはそれぞれ作成して、ControllerはHikawaにだけ作成した。 生成されたのは以下のファイル。 API定義とそのテスト: zundoko-operator/pkg/apis/zundokokiyoshi/v1beta1/*.go CRD: zundoko-operator/config/crds/config/crds/*.yaml Hikawa Controllerとそのテスト: zundoko-operator/pkg/controller/hikawa/*.go リソースのマニフェストのサンプル: zundoko-operator/config/crds/config/samples/*.yaml これらの内、CRDと zundoko-operator/pkg/apis/zundokokiyoshi/v1beta1/zz_generated.deepcopy.go はAPI定義をもとに生成されるので、API定義を書いた後生成しなおすことになる。 API定義記述 リソースがどのような属性をもつかをGoで定義する。 テンプレートは生成されているので、ちょっと書き足すだけでできる。 以下はHikawaのAPI定義。 zundoko-operator/pkg/apis/zundokokiyoshi/v1beta1/hikawa_types.go: package v1beta1 import ( "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // HikawaSpec defines the desired state of Hikawa type HikawaSpec struct { IntervalMillis time.Duration `json:"intervalMillis"` NumZundokos int `json:"numZundokos,omitempty"` SayKiyoshi bool `json:"sayKiyoshi,omitempty"` } // HikawaStatus defines the observed state of Hikawa type HikawaStatus struct { NumZundokosSaid int `json:"numZundokosSaid"` Kiyoshied bool `json:"kiyoshied"` } // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Hikawa is the Schema for the hikawas API // +k8s:openapi-gen=true type Hikawa struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec HikawaSpec `json:"spec,omitempty"` Status HikawaStatus `json:"status,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // HikawaList contains a list of Hikawa type HikawaList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []Hikawa `json:"items"` } func init() { SchemeBuilder.Register(&Hikawa{}, &HikawaList{}) } 自分で書いたのはHikawaSpecとHikawaStatusの中だけ。 Specの方には期待する状態、Statusの方には現在の実際の状態を表すフィールドを定義するのがパターン。 例えば、HikawaSpec.NumZundokosが期待するZundokoの数で、HikawaStatus.NumZundokosSaidが実際に作成されたZundokono数。 Hikawa ControllerはReconciliationループの中で、SpecとStateが同じになるように処理をすることになる。 ZundokoとKiyoshiのAPI定義は、Specに「Zun」、「Doko」、または「Kiyoshi!」を入れるためのSayフィールドだけを書いた。 Hikawa Controller記述 Hikawa Controllerもテンプレートが生成されているので、それを参考に書ける。 まずはどのリソースをwatchするかを書く。 zundoko-operator/pkg/controller/hikawa/hikawa_controller.go前半抜粋: func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller c, err := controller.New("hikawa-controller", mgr, controller.Options{Reconciler: r}) if err != nil { return err } // Watch for changes to Hikawa err = c.Watch(&source.Kind{Type: &zundokokiyoshiv1beta1.Hikawa{}}, &handler.EnqueueRequestForObject{}) if err != nil { return err } err = c.Watch(&source.Kind{Type: &zundokokiyoshiv1beta1.Zundoko{}}, &handler.EnqueueRequestForOwner{ IsController: true, OwnerType: &zundokokiyoshiv1beta1.Hikawa{}, }) if err != nil { return err } return nil } Hikawaは普通にwatchして、Zundokoはownしているリソースとしてwatchしている。 Reconciliationループは以下のように書いた。 zundoko-operator/pkg/controller/hikawa/hikawa_controller.go後半抜粋: // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=hikawas,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=hikawas/status,verbs=get;update;patch // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=zundokos,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=zundokos/status,verbs=get;update;patch // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=kiyoshis,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=kiyoshis/status,verbs=get;update;patch func (r *ReconcileHikawa) Reconcile(request reconcile.Request) (reconcile.Result, error) { instanceName := request.NamespacedName.String() log.Info("Reconciling a Hikawa: " + instanceName) // Fetch the Hikawa instance instance := &zundokokiyoshiv1beta1.Hikawa{} err := r.Get(context.TODO(), request.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { // Object not found, return.
eyecatch
Mon, Nov 26, 2018

React + Reduxアプリケーションプロジェクトのテンプレートを作る ― その11: FlowからTypeScriptへ移行

ReactとReduxを学ぶために、開発環境というかプロジェクトテンプレートをスクラッチから作っている。 (最終的な成果はGitHubに置いた。) 前回はCode Splitting、Flow、Jest、Enzymeをセットアップした。 前回でこのシリーズを終わりにするつもりだったけど、型システムをFlowからTypeScriptに移行したのでそれについて書く。 (adsbygoogle = window.adsbygoogle || []).push({}); TypeScript TypeScriptはMicrosoft製のAltJS。 もともとはCoffeeScriptのように言語の機能面(e.g. class構文やアロー関数)を補強しつつ、静的型付けをサポートする言語だったが、最近はECMAScriptが前者をカバーしてるので、後者を主な目的として使う人が多い。 2012年に誕生した言語で、同様に静的型付けをサポートするFlowよりも2歳ほど年上。 TypeScript vs Flow 個人的には、静的型付けだけを目的にするならAltJSである必要はなく、静的型付けだけを補完するFlowのほうが筋がいいような気がする。 TypeScriptはECMAScriptの進化に追従すべく、追加される機能や構文をサポートするためのエンハンスを繰り返しているが、そこはBabelに任せて静的型付けに注力したらいいような。 とはいえ、以下のような点を鑑み、結局TypeScriptを選択した。 TypeScriptの方が人気 GitHubのプロジェクトのスター数はTypeScriptが4万超えでFlowが2万弱。 観測している限り、FlowからTypeScriptへ移行したというのは聞くが、逆は聞かない。 人気があるということはコミュニティやエコシステムが大きいということ。 TypeScriptがノってる BabelやCreate React AppがTypeScriptをサポートして来ていて、なんだか時流にのっている。 Flowは型定義ファイルの管理方法が微妙 Flowはflow-typedという専用のツールを使ってファイルをダウンロードし、ダウンロードしたものをGitとかのVCSでバージョン管理するというやりかた。 TypeScriptはnpmで管理されてるので、Yarnでダウンロードもバージョン管理もできる。VCSのリポジトリに自前のコードしか入れないで済むのもいい。 TypeScriptの方が型定義ファイルが沢山提供されてる Flowの10倍くらいある。 TypeScriptの方がエラーメッセージが分かりやすい というのをどこかで聞いた。 Flowの方が段階的に型を導入できる、というのは昔の話 今はTypeScriptもオプションによって段階的に導入できるというのが定評。 そもそも最初から型付けするならどうでもいい。 Flowの方が厳密な型チェックしてくれる、というのも昔の話 TypeScriptが追い付いてきて、今はほぼ同程度らしい。 TypeScript+VSCodeの開発体験が最高すぎるらしい どっちもMicrosoft製なので。 TypeScriptの方がドキュメントが充実してる TypeScriptの方が、いざというときにソースが読みやすい TypeScriptはTypeScriptで実装されてて、FlowはOCamlで実装されてる。 参考: https://github.com/niieani/typescript-vs-flowtype https://texta.pixta.jp/entry/2018/06/07/120000 https://narinymous.hatenablog.com/entry/2018/03/02/032130 https://base.terrasky.co.jp/articles/zuUtT 前回の記事ではFlowを導入したんだけどTypeScriptに移行する羽目に。 FlowとTypeScriptとで型の表現方式や表現力にあまり差はなかったのでそこはまあ手間ではなかったんだけど、以下のような問題に対応する必要があった。 ビルド時にTypeScriptの方が時間がかかる。 TypeScriptのリンタであるTSLintが、FlowのESLintよりルールが貧弱 TypeScriptのコンパイラがチェックしてくれるからいいのかもしれないけど。 TypeScriptはAltJSなので、何かと連携するときに何かと面倒 Jestでユニットテストするときはどうするんだっけとか プレーンJavaScriptと混在した環境ではTSLintとESLint併用しなければいけないんだっけとか FlowからTypeScriptへの移行 脱Flow とりあえずFlowを取り除く。 $ yarn remove flow-bin flow-typed @babel/preset-flow eslint-plugin-flowtype babel-eslint $ rm -f .flowconfig .babelrc: { "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage" } ], - "@babel/preset-flow", "@babel/preset-react" ], "plugins": ["styled-components", "@babel/plugin-syntax-dynamic-import"] } .eslintrc.js: module.exports = { env: { browser: true, 'jest/globals': true, }, - parser: 'babel-eslint', - extends: ['airbnb', 'plugin:flowtype/recommended', 'prettier'], - plugins: ['flowtype', 'jest'], + extends: ['airbnb', 'prettier'], + plugins: ['jest'], }; 環境はこれでよくて、あとは各.jsファイルと.jsxファイルから// @flowを消して、型情報も消す。 (型情報はTypeScriptでも同じようなのを書くので残しておいてもいい。) TypeScript導入 パッケージインストール 以下のパッケージを入れる。 typescript: TypeScript本体。コンパイラ(tsc)等を含む。 @types/*: 各3rdパーティライブラリの型定義ファイル(DefinitelyTyped)。(型定義はライブラリ本体のパッケージに含まれている場合もある。) awesome-typescript-loader: TypeScriptを処理するためのwebpackのローダ。他の選択肢としてts-loaderがあるが、公式のチュートリアルがawesome-typescript-loaderをメインで紹介してるのでこっちにする。 $ yarn add -D typescript @types/react @types/react-dom @types/react-redux @types/redux-logger @types/history @types/react-router-dom @types/uuid @types/styled-components awesome-typescript-loader TypeScriptはv3.1.6、awesome-typescript-loaderはv5.2.1が入った。 TypeScriptの設定 TypeScriptの設定ファイルであるtsconfig.jsonはtscコマンドでテンプレートを生成できる。 $ yarn tsc --init 生成されたファイルをプロジェクトルートに置いて、ちょっといじって以下の感じに。 (jsonなのにコメント書ける…) tsconfig.json: { "compilerOptions": { /* Basic Options */ "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'.
eyecatch
Wed, Nov 7, 2018

React + Reduxアプリケーションプロジェクトのテンプレートを作る ― その10: Code Splitting、Flow、Jest、Enzyme

ReactとReduxを学ぶために、開発環境というかプロジェクトテンプレートをスクラッチから作っている。 (最終的な成果はGitHubに置いた。) 前回はReact Routerをセットアップした。 今回は残りの要素をまとめてかたづける。 (2018/11/21更新) (adsbygoogle = window.adsbygoogle || []).push({}); Code Splitting webpackでリソースをバンドルすると、一回の通信でアプリの要素全てをロードできるので効率いいような気がするけど、アプリの規模が大きくなってくるとバンドルサイズが大きくなって、初期ロード時間が長くなり、つまり初期画面の表示に時間がかかるようになってしまう。 そもそも、いつもアプリの全画面をみるとは限らないので、いつもアプリの全要素をロードするのは無駄。 そんな問題に対応する技術がCode Splitting。 バンドルを分割し、(理想的には)必要な時に必要な分だけロードする技術。 Code Splittingのやりかたはいくつかあるが、ダイナミックインポートとReact.lazyとReact Suspenseとwebpackのプリフェッチディレクティブを使ったやつを、フォントモジュールに適用してみる。 src/components/App.jsx: -import React from 'react'; +import React, { Suspense } from 'react'; import { Route, Redirect } from 'react-router-dom'; import Home from './Home'; -import Fonts from '../fonts'; +const Fonts = React.lazy(() => import(/* webpackPrefetch: true */ '../fonts')); const App = () => ( <div> <Route exact path="/" render={() => <Redirect to="/home" />} /> <Route exact path="/home" component={Home} /> - <Fonts /> + <Suspense fallback={<div />}> + <Fonts /> + </Suspense> </div> ); export default App; コード変更はこれだけ。 import()がダイナミックインポートで、ECMAScriptで現在策定中の機能。 これを使えるようにするためには、Babelのプラグインを追加する必要がある。 yarn add -D @babel/plugin-syntax-dynamic-import .babelrc: { "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage" } ], "@babel/preset-react" ], - "plugins": ["styled-components"] + "plugins": ["styled-components", "@babel/plugin-syntax-dynamic-import"] } ダイナミックインポートの設定も完了。 これでフォントモジュールはメインのバンドルとは別ファイルになり、初期画面の表示時にはロードされず、ブラウザの空き時間に非同期にロードされるようになる。 Flow Reactに限らない話だけど、JavaScriptは動的型付け言語なので、特に規模が大き目なアプリを開発するとなると保守性が悪くなりがちで辛い。 ので、できれば静的型付けでやりたい。 JavaScriptを静的型付けにするには、TypeScriptとFlowという二つの選択肢がある。 今回、FlowがReactと同じくFacebook製なので、Reactと相性がいいかと思ってFlowを選択したけど、人気やエコシステムの充実度から見るとTypeScriptのほうがよかった気がする。 ので、Flowについてはさらっと書く。 Flow導入 Flowは、ソースに型情報を付けて静的型チェック可能にしつつ、実行時には型情報を取り去って普通のJavaScriptとして実行できるようにする仕組み。 型チェックするツールはflow-binパッケージで配布されていて、型情報の除去は@babel/preset-flowを使ってBabelでできる。 yarn add -D flow-bin @babel/preset-flow .babelrc: { "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage" } ], + "@babel/preset-flow", "@babel/preset-react" ], "plugins": ["styled-components", "@babel/plugin-syntax-dynamic-import"] } これで、yarn flowでFlowを実行できるようになった。 $ yarn flow version yarn run v1.7.0 $ C:\Users\kaitoy\Desktop\bin\pleiades\workspace\react-redux-scaffold\node_modules\.bin\flow version Flow, a static type checker for JavaScript, version 0.77.0 Done in 0.38s.
eyecatch
Fri, Nov 2, 2018

React + Reduxアプリケーションプロジェクトのテンプレートを作る ― その9: React Router

ReactとReduxを学ぶために、開発環境というかプロジェクトテンプレートをスクラッチから作っている。 (最終的な成果はGitHubに置いた。) 前回はRedux Sagaをセットアップした。 (2018/11/21更新) (adsbygoogle = window.adsbygoogle || []).push({}); フロントエンドのルーティング Webアプリケーションにおけるルーティングとは、クライアントがリクエストしたURLに対して、返すべきリソースを選択する処理。 昔はバックエンド(i.e. サーバサイド)でやってたけど、バックエンドでリソースを返すということは、ページ遷移が発生するということなので、ネイティブアプリケーションに比べてUXが落ちてしまう。 一方、ページ遷移を発生させないようにAjaxでサーバとやりとりしつつ、ちまちまDOMをいじるのは大変。 DOMをごっそり書き換えて、ページ遷移なしに画面を切り替えることはできるけど、ナイーブにやると以下のような問題がある。 URLと画面の紐づけがなく、URLを指定して直接開けない ブラウザの進む、戻るが使えない 宣言的に書けない こういった問題に対応するため、フロントエンドでのルーティング技術が生まれた。 フロントエンドのルーティングでは、URLが変わってもリクエストはサーバに飛ばない。 代わりに、フロントエンドフレームワークがそのURLを見て、適切な画面を選んでレンダリングする。 ハッシュベースのルーティング URLが変わってもリクエストがサーバに飛ばないとは何事か。 それを実現するやりかたは2通りある。 古くはハッシュ(#、フラグメント識別子)をつかったやり方。 例えば、http://example.com/でUIをサーブしているとすると、http://example.com/#fooとか、http://example.com/#barで別々のページの状態を表現する。 ハッシュ以降が変わってもブラウザがサーバにリクエストを投げることはないので、クライアント側でハンドリングできる。 (因みに、ハッシュを含んだURLをブラウザのアドレスバーに入れても、ハッシュを除いたURLでリクエストが送られる。この挙動の根拠となる規格はRFCなどを調べても見つからなかったけど…) ハッシュの書き換えは、JavaScriptで以下のようにしてできる。 location.hash = newHash; こういう処理を、例えばWeb UIのボタンをクリックしたときなんかに実行してURLを変えて、その上で画面を更新してやればいい。 そのあと、ブラウザの戻るボタンなんかを押されると書き換える前のURLにもどるわけだけど、これを検知するためにsetInterval()とかで定期的にlocation.hashを監視してたりした。 History APIによるルーティング ハッシュベースのルーティングは見るからにしょぼい。 URLのハッシュ以降しか使えないのもしょぼいし、内部の処理も泥臭い。 これが、HTML 5でHistory APIがでて変わった。 History APIはJavaScriptのAPIで、ブラウザの履歴を操作できる。 const state = { hoge: "hogeee" }; history.pushState(state, "", "/foo/bar"); こんな感じのを実行すると、URLが/foo/barに変わる。(が、もちろんサーバにはリクエストは飛ばない。) で、ブラウザの戻るボタンを押すと、popstateイベントが発生するので、それにイベントハンドラを登録しておけば、もとのURLに戻った時にも適時画面を書き換えられる。 popstateイベントからは、pushState()に渡したstateオブジェクトを取得できる。 ところで、ブラウザのアドレスバーに/foo/barを直打ちするとどうなるかというと、普通にWebサーバを設定しておくと、/foo/bar/index.htmlを返そうとして、無いので404エラーになっちゃう。 ので、サーバ設定では、どのURLも同じリソース(e.g. /index.html)をしといて、そこからJavaScriptを呼んで、URLを読み取って、画面を描いてやればいい。 HTML 5が普及するにつれ、このようなHistory APIを使ったフロントエンドルーティングをするフレームワークやライブラリが色々出てきた。んだろうと思う。 React Router Reactのエコシステムとしては、React Routerがフロントエンドルーティングを実現してくれる。 React Routerは、宣言的にフロントエンドルーティングを実現できるReactコンポーネントのライブラリ。