XXとしてる
2019年8月28日水曜日
C/C++の中でpopenで実行したコマンドの出力を色々触りたいときのメモ
Linuxには色々コマンドがあるけど、それをCとかC++とかで使うときはsystem()にコマンドを渡すとできる。 ```cpp #include <stdlib.h> system("ls -a"); ``` が、コマンドの出力をそのままC/C++内で使うことはできない。 一応ファイルに出力するようにコマンドを実行すればそのファイルを開けばコマンドの出力結果を使用できるが、 できればsystem()を実行したらその出力をそのまま受け取りたい。受け取りたくない? というわけで色々調べたところ、popen()を使用すると出来るみたいだ。 [Man page of popen](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/popen.3.html) ### ソースコード ```cpp #include <stdio.h> //for popen(), pclose(), fileno() #include <unistd.h> // for read() // ... 他色々インクルード // 実行したコマンドの出力を1行ごとに受け取り、predicateに渡す関数 void runProcess(const std::string& cmd, std::function<void(const string&)> predicate) { // 読み込みフラグでpopen()を実行 auto pf = popen(cmd, "r"); if(pf == nullptr) { throw std::runtime_error("例外を投げる"); } // auto fd = fileno(pf); char c; std::string line; while(read(fd, &c, 1) > 0) { if(c == '\n') { predicate(line); line.clear(); } else { line += c; } } if (!line.empty()) { predicate(line); } pclose(pf); } ``` 使用例 ```cpp // 単純にlsコマンドの結果を出力する runProcess("ls", [](const string& line) { cout << line << endll; }); ``` 上のコードの説明としては、 - popen()には必ず読み込みフラグ('r')を指定する。 - popen()で開いた子プロセスのファイルディスクリプタを取得する - read()で子プロセスからの出力を読み込む - popen()で取得したFILE*はpclose()で必ず閉じること read()の周りのコードを見ると実行したプロセスの出力が1秒とか10秒とか何も出力されない間隔があったりするとうっかりループから抜け出てしまうのではないかと思ったのだが、上のコードでも問題なく動作する。 #### fileno()なんてあったんだ 実装中に悩んだことはread()で使用するファイルディスクリプタをどのように取得するのかが初めはわからなかったことである。 fileno()で取得するというとても簡単なことであったのが、fileno()の存在を知るまではFILEから直接取り出そうとか考えていたぐらいなので初めてのことはグダグダになるだなぁと。 それがわかれば、あとはすんなりと実装できた。 コード実装時には[pipe()のmanページ](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/pipe.2.html)とか参考にしている。 (read()周りのループはそのまま持ってきている) ## 終わりに 今回はlddコマンドで実行ファイルが参照している共有ライブラリを検索するとき、参照している共有ライブラリが参照している共有ライブラリも一緒に検索出来たら便利かなと思ったのが、この記事を書くきっかけになった。
2019年4月16日火曜日
ProxyとMutationObserver
Reactでサイトを作り変えているところ、こんな感じな画面になった。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhXV7R_vpVJvTXVxYke2zI2jWHcPDAH1DBk-VR2feT_2HWUZyup-kW-p8OKBFUnGWI_YvohL6UPtrGHT9MTl-YcYZ09Z5aWcZg3bub_LKmLee9KUss8umAKHu-6xKm4ZifIbUi8G-R8Tv8/s1600/Screenshot_2019-04-16+%25E6%25BC%25A2%25E5%25AD%2597%25E7%25B5%2584%25E3%2581%25BF%25E7%25AB%258B%25E3%2581%25A6%25E5%25B7%25A5%25E5%25A0%25B4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img style="border: 1px solid black;" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhXV7R_vpVJvTXVxYke2zI2jWHcPDAH1DBk-VR2feT_2HWUZyup-kW-p8OKBFUnGWI_YvohL6UPtrGHT9MTl-YcYZ09Z5aWcZg3bub_LKmLee9KUss8umAKHu-6xKm4ZifIbUi8G-R8Tv8/s320/Screenshot_2019-04-16+%25E6%25BC%25A2%25E5%25AD%2597%25E7%25B5%2584%25E3%2581%25BF%25E7%25AB%258B%25E3%2581%25A6%25E5%25B7%25A5%25E5%25A0%25B4.png" width="212" height="320" data-original-width="616" data-original-height="928" /></a></div> <div style="text-align: center">下に余白がある</div> フッターが中途半端な部分にあって、ちょっとダサい感じ。 なのでbodyタグの高さが画面より低かったら画面下部に固定するようにしたかったが、一筋縄ではいかなかったので、メモを残していく。 ## とりあえずReactのrender関数で 手っ取り早いのでReact.Coponentのrender関数内でフッターのスタイルを動的に切り替えようと試みてみた。 ```js export class Footer extends React.Component<Props, State> { render() { const body = document.body let STYLE = { position: undefined, bottom: undefined } as React.CSSProperties if (body.scrollHeight < window.innerHeight) { STYLE.width = '100vw' STYLE.position = 'absolute' STYLE.bottom = 0 } return // ... } } ``` <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwTDeWjQayVVpdoD-bSWF7fTcWIQNqWRHyOnRLv8qETX9-voJ7hD_aF2Gz-cdT-6USIIUcanWP3xgknbHq_g0bDzpwhZ971uEgCKUl_Hvrz3lN-WTv8BX8bbxAhNROt_Lh9_5mkVthWpg/s1600/lol.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwTDeWjQayVVpdoD-bSWF7fTcWIQNqWRHyOnRLv8qETX9-voJ7hD_aF2Gz-cdT-6USIIUcanWP3xgknbHq_g0bDzpwhZ971uEgCKUl_Hvrz3lN-WTv8BX8bbxAhNROt_Lh9_5mkVthWpg/s1600/lol.png" data-original-width="128" data-original-height="142" /></a></div> <div style="text-align: center">へへ、bodyの高さが0のままだぜ</div> そりゃ、Reactで今から描画しようとしているんだから、書きかけな状態ではまだ全体の高さはわからないよ。 というわけで、次に移行する ## Reactのライフサイクル関数をフックする それじゃ描画後に高さを計算したらいいんじゃないと言うわけで`componentDidMount()`内でスタイルの切り替えを行うようにしてみた。 [State and Lifecycle](https://reactjs.org/docs/state-and-lifecycle.html) ```js export interface State { isFixedBottom: boolean } export class Footer extends React.Component<Props, State> { componentDidMount() { // 描画後に呼び出される const body = document.body this.setState({isFixedBottom: body.scrollHeight < window.innerHeight}) } render() { // this.state.isFixedBottomを使ってスタイルを切り替えるように変更 } } ``` <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" data-original-width="107" data-original-height="189" /></a></div> <div style="text-align: center">やった!高さがちゃんと取得できたぞ!</div> ちなみにページ内容をタブで切り替えられるようにしていたので`componentDidMount()`だけでは不十分で`componentDidUpdate()`で更新したときにも処理をはさまないといけない。 ```js export interface State { isFixedBottom: boolean } export class Footer extends React.Component<Props, State> { componentDidUpdate() { //なにか更新があったときに呼び出される const body = document.body this.setState({isFixedBottom: body.scrollHeight < window.innerHeight}) } } ``` <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0P7uxtF0Y-pI6P4YqQVUEG8dzo5NOT4WCAKofIJ_n5e3x4gS_BA7H3oZCUs25655Zas2E5cw6U9JWSHXgducdOpiGotJeJtEukbd9XoMdGZknIe6i78fqjoTewvGEhxrVFwA_J8zMJc4/s1600/chirp.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0P7uxtF0Y-pI6P4YqQVUEG8dzo5NOT4WCAKofIJ_n5e3x4gS_BA7H3oZCUs25655Zas2E5cw6U9JWSHXgducdOpiGotJeJtEukbd9XoMdGZknIe6i78fqjoTewvGEhxrVFwA_J8zMJc4/s1600/chirp.png" data-original-width="84" data-original-height="124" /></a></div> <div style="text-align: center">いや〜、出来た出来・・うん?</div> <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" data-original-width="210" data-original-height="135" /></a></div> <div style="text-align: center">'componentDidUpdate()'が呼びされない・・だと・・・</div> <div style="text-align: center">なぜだぁ・・・なぜなんだ・・・</div> <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF22uBZBdA63ptQUblnFWQJ1WRoOfhg3xx7uUuIhYH8M97SELnTnQ8dZYAblq02RL_YVV91rwH22Nn3eIKe1K5EF56vefY_B6bdtBanRZAhq4T5kURgr7SBSVmNJcHpEI0S7GV_MUnlzI/s1600/be+attacked.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF22uBZBdA63ptQUblnFWQJ1WRoOfhg3xx7uUuIhYH8M97SELnTnQ8dZYAblq02RL_YVV91rwH22Nn3eIKe1K5EF56vefY_B6bdtBanRZAhq4T5kURgr7SBSVmNJcHpEI0S7GV_MUnlzI/s1600/be+attacked.png" data-original-width="225" data-original-height="156" /></a></div> <div style="text-align: center">フッターそのものは更新されないぞ</div> その通りである、実際に更新されるのはページ内の他のコンポーネントなので、フッターそのものは更新されないのである。 困った。これは困った。 フッターではなくてページ単位でフックするようにしたらいいのだと思うけど、コンポーネント間のやり取りが出来てしまって管理が大変になるし、 毎回ページにそれ関係のコードを追加するのがめんどくさいぞ。 いったいどうしたらいいんだ・・・ ## ProxyをつかってダイレクトにBodyの高さを監視する じゃあさぁ・・・bodyタグの高さの変更をフッターコンポーネント内で監視するようにしたらいいんじゃない? というわけで早速調べてみると、JavaScriptにProxyというものがあってそれがぴったり今回の問題に使えそうであった。 [Proxy](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy) ```js export class Footer extends React.Component<Props, State> { constructor(props: Props) { super(props) const bodyProxy = new Proxy(document.body, { set: (target: HTMLElement, name: string | number | symbol, value: any): boolean => { if ('scrollHeight' !== name) { return true } // 現在の状態と異なっていたときに変更するようにしたほうがいいけど簡略のためにこうしている this.setState({isFixedBottom: document.body.scrollHeight < window.innerHeight}) return true } }) document.body = bodyProxy // エラー } } ``` <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh0sQWVrB2Eq82yDbC4m6Tcr8PnAwQMmGdWJQAcAuakGBsSsc4_i32hR-vhyMqRbnRec6O-PDUmnNs-c8fc4cWMfPK6uCC3UHLk16Di295Ck4rghE3qCnWhtzYEZOd7ruTyBN59j71_9xw/s1600/be_depressed.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh0sQWVrB2Eq82yDbC4m6Tcr8PnAwQMmGdWJQAcAuakGBsSsc4_i32hR-vhyMqRbnRec6O-PDUmnNs-c8fc4cWMfPK6uCC3UHLk16Di295Ck4rghE3qCnWhtzYEZOd7ruTyBN59j71_9xw/s1600/be_depressed.png" data-original-width="58" data-original-height="111" /></a></div> <div style="text-align: center">が、ダメ・・・</div> <b>Proxy経由で値を変更しないと設定したハンドラーは実行されないので</b>、なんとかしてdocumentにあるbodyをそれと差し替えないといけない。 上のコードのようにReactでは直接値を書き換えることは禁止されているので、どうしたらいいんだ・・・。 ## DOMのイベントハンドラーに都合のいいイベントありますか? <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSLohHdaMR2_tXMnG1QJlhyphenhyphen9TB4bK-E287w6I4q9VNWH9CToHuzZM9DILY0XlIDo94xy0jwpsD-vK54LyLeYMCsZEfSv1_pNL3IsJyvP7Yc637wXewnlBDX3BM081VHFa5VkisQPcxHDE/s1600/express.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSLohHdaMR2_tXMnG1QJlhyphenhyphen9TB4bK-E287w6I4q9VNWH9CToHuzZM9DILY0XlIDo94xy0jwpsD-vK54LyLeYMCsZEfSv1_pNL3IsJyvP7Yc637wXewnlBDX3BM081VHFa5VkisQPcxHDE/s1600/express.png" data-original-width="75" data-original-height="110" /></a></div> <div style="text-align: center">ないです!</div> `Document.onvisibilitychange`はウィンドウやタブ単位の可視性が変わったときに発行されるイベントで今回とは異なる。 `Window:afterprintイベント`もドキュメントが描画されから終わった後に発行されるので、これも異なる。 JSではうまく検知できない問題? そんなことない! #### <i>MutationObserver!!</i> jsにはMutationObserverというDomツリーの変更を監視できるインターフェースが用意されている。 [MutationObserver ドキュメント](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) Proxyでは生成したインスタンス経由で値を変更しないとコールバックが呼び出されなかったので断念したが、これを使えばできる! それで以下のようなコードを書くと、 ```js export class Footer extends React.Component<Props, State> { constructor(props: Props) { super(props) this.state = { isFixedBottom: false } let config = { childList: true, subtree: true, } as MutationObserverInit const callback = (_mutationList: MutationRecord[], _observer: MutationObserver): void => { const lowBodyHeight = document.body.scrollHeight < window.innerHeight if (this.state.isFixedBottom !== lowBodyHeight) { this.setState({ isFixedBottom: lowBodyHeight }) } } let observer = new MutationObserver(callback) observer.observe(document.body, config) } // ... ``` 無事、目的を達成することが出来る。 `observer.observe(document.body, config)`の部分で監視を始めている。 第一引数に監視対象を、第二引数に監視内容を渡す。 今回は子要素も含めたノードで子の追加/削除があったときにコールバックを呼び出すように設定してある。 これで他のコンポーネントの更新があったかが擬似的にわかるようになったので、 あとはそれに合わせてフッターを画面下部に固定するかしないかを決めるだけであっさりと出来上がり。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" data-original-width="107" data-original-height="189" /></a></div> <div style="text-align: center">終わり</div>
(画像は以下のものを使用させていただきました。)
2019年4月12日金曜日
JS内でCSSをインポートしたいときはcss-loaderのmodulesをtrueにしよう。
# タイトル通りです。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" data-original-width="210" data-original-height="135" /></a></div> ## 解決例 webpackを使っているときの話で、以下のような設定にしましょう! ```js export.modules = { module: { rules: [ { test: /\.css?$/, use: [ "style-loader", { loader: "css-loader", options: { modules: true // 必要! } } ] }, // SASS/SCSSのとき { test: /\.scss?$/, use: [ "style-loader", { loader: "css-loader", options:{ modules: true // 必要! } }, "sass-loader" ] }, ] } } ``` ## この機能のことをCSS Modulesって言うんだって! [CSS Modules](https://github.com/css-modules/css-modules)から > CSS Moduleは全てのクラス名とアニメーション名がデフォルトでローカルにスコープされるCSSファイルである。 > 全てのURLs(`url(...)`)と`@imports`は要求したフォーマットでモジュールの中にある(`./xxx`と`../xxx`は相対パスを意味し、`xxx`と`xxx/yyy`は例えば`node_modules`の中といったモジュールフォルダーを意味する)。 CSSはあまり詳しくないのだけど、色々発展しているそうでCSS Modulesもその機能の一つだそうだ。 これを使うことで次のようなコードが書くことが出来る。 ファイルの位置関係については同じディレクトリにあるものとしてほしい。 ```js import * as React from 'react' import * as ReactDOM from 'react-dom' import MyStyle from './my-style.css' ReactDOM.render( <div className={MyStyle.Title}>見出しだよ!</div> , document.getElementById('root')) ``` CSSファイルはこんな感じ。 ```css /*my-style.css*/ .Title { font-size: 72px; color: red; } ``` この機能によってCSSがJSコード内で使いやすくなる恩恵があるとかないとか。 CSSのクラス名を変更したらJS側も変更しないと何らかのエラーが起きやすくなっていると思うので、是非使いたい機能だ。 (使う際はJS側で扱いやすいように名前をキャメルケースで書いたほうが捗ると思う。) ## TypeScriptを使っているときは型定義も一緒に作ってね! [こちらを参考にしています](https://qiita.com/jerrywdlee/items/3c525001f8029312d5fa) これで名前とか変更してもコンパイル時にわかるようになるし、IDEとかでコード補間が効いてコーディングしやすくなる。 ```js // my-style.d.ts declare module '*.scss' { const content: { Title: string; } export default content } ``` 記事に書いてあるが、CSSファイルを作るたびに型定義を書くのは面倒なので一つのファイルにまとめて書くのがおすすめ。 ```js declare module 'foo.css' { const content: { bar: string; baz: string; }; export = content; } declare module 'bar.scss' { const content: { baz: string; }; export = content; } ``` ## 終わり <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" data-original-width="107" data-original-height="189" /></a></div> <div style="text-align: center">フロントエンドって依存パッケージがたくさんあって大変だね!</div> 開発環境を整備するのが一番大変かもしれない…
(画像は以下のものを使用させていただきました。)
2019年4月11日木曜日
Webpackで開発中に複数のページを確認したいとき
Reactを触ってみて、`create-react-app`コマンドでお手軽にかつService Workerとかのテンプレートも含まれて簡単に開発できるなぁと思っていたところ、 複数ページを作ったらときはどのように確認したらいいのかわからないことに気がついた。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtR9ntWQ125x-Zi_27VDmBwSGteuZV75rp0LSCRAIpn89XDmMiAlJXtc5QqtscXxD-nlgX0jUx8j9U1UzuPjDf5mvDUJt90aO9BNSf2UO58sNqYy364i1nSm4cSQahmnAnUDZkueuKsMA/s1600/wait.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtR9ntWQ125x-Zi_27VDmBwSGteuZV75rp0LSCRAIpn89XDmMiAlJXtc5QqtscXxD-nlgX0jUx8j9U1UzuPjDf5mvDUJt90aO9BNSf2UO58sNqYy364i1nSm4cSQahmnAnUDZkueuKsMA/s1600/wait.png" data-original-width="60" data-original-height="105" /></a></div> <div style="text-align: center;">ちょっとわからないです・・・</div> `create-react-app`で作ると中でWebpack的なことが行われているから、拡張できない? そもそもReactってSingle Page Application向けのライブラリなんだから、ページを複数作ることを考えていない感じなのか? React Routerもあるしなぁ・・・。 けどな〜一つのページで作っていくと後々困りそうだし、検索してみたらMultiple Page Applicationの中にSPAを組み込むことができるってあるから、 出来るならそうしたいな〜。 てな感じで結構な時間悩んでいたので、とりあえずWebpackを使って複数のページの開発を試してみようと思ったら、`webpack-dev-server`が一つのページにしか対応していないことにわかった。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" data-original-width="210" data-original-height="135" /></a></div> <div style="text-align: center;">これは困ったぞ!</div> <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgNjdJsEn2_m1C6LYf6OpMuaGjVZHg0jWHYW0UdsW8HgAxHY0k0tpw85zg6d58EgIkQ_zsdtNh9RMmYs_g0yv4JncEbcbbzBhS03HWvPgtlwhyxFTVskmVTF1emi8PJy1cFME7NY35Rmb0/s1600/be_tampered.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgNjdJsEn2_m1C6LYf6OpMuaGjVZHg0jWHYW0UdsW8HgAxHY0k0tpw85zg6d58EgIkQ_zsdtNh9RMmYs_g0yv4JncEbcbbzBhS03HWvPgtlwhyxFTVskmVTF1emi8PJy1cFME7NY35Rmb0/s1600/be_tampered.png" data-original-width="224" data-original-height="152" /></a></div> <div style="text-align: center;">これを使うんだ!</div> <div style="text-align: center; font-size: 24px;"><b><i>'''webpack-dev-middleware'''</i></b></div> ## 問題解決! <a href="https://webpack.js.org/guides/development/#using-webpack-dev-middleware">Webpackの公式ガイドのwebpack-dev-serverの下に書いてあったwebpack-dev-middleware</a>を使えば複数ページでも確認が出来るぞ! 使うときは自前のサーバーを建てる必要があるけど、nodejsならexpressがあるし、なによりドキュメントにサンプルコードがあるのであっさりと準備が終わる。 ```js // 開発時の確認用サーバーを起動するためのプログラム const express = require('express'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const app = express(); const config = require('./webpack.config.js'); const compiler = webpack(config); // Tell express to use the webpack-dev-middleware and use the webpack.config.js // configuration file as a base. app.use(webpackDevMiddleware(compiler, { publicPath: config.output.publicPath })); // Serve the files on port 3000. app.listen(3000, function () { console.log('Example app listening on port 3000!\n'); }); ``` 必要になるパッケージのインストールこんな感じ。 ```bash yarn add -D express webpack webpack-cli webpack-dev-middleware ``` あと、webpackの設定もHTMLを複数出力するように修正すればOK! `webpack-dev-middleware`も内部で`webpack-dev-server`を使っているのでその設定も必要になる。 後、HtmlWebpackPluginの`chunks`オプションに使いたいパックされたものを指定すれば、ページに必要なリソースだけを読み込むことが出来る。 ```js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { mode: 'development', entry: { //必要な分だけ書く。 index: './src/index.js', about: './src/about.js' }, devtool: 'inline-source-map', devServer: { contentBase: './dist' }, plugins: [ new HtmlWebpackPlugin({ chunks: ['index'], //使用するチャンク名を指定する。 template: './src/index.html', filename: './index.html' }), new HtmlWebpackPlugin({ chunks: ['about'], template: './src/about.html', filename: './about.html' }) ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: '/' } }; ``` ## 余談 より本番環境に近づけるために (URLから'.html'を省く) 少し使ってみて、ページ遷移をするときに'.html'を付けないといけないのってよくよく考えてみると本番環境と違うよねと気がついたので、省略する方法を考えてみた。 あくまで開発用でページ遷移ができるだけでいいので、細かな動作の違いは考えないことにする。 ```js // 開発時の確認用サーバーを起動するためのプログラム const express = require('express'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const path = require('path') const app = express(); const config = require('./webpack.config.js'); const compiler = webpack(config); //開発用にhtmlファイルにリダイレクトするコード app.all('*', function (req, res, next) { if (path.extname(req.url) === '' && req.url !== '/') { const redirectURL = req.url+'.html' console.log(`!!Development Mode!! Redirect to ${redirectURL} from ${req.url}`) res.status(303).redirect(redirectURL) } else { next() } }) // Tell express to use the webpack-dev-middleware and use the webpack.config.js // configuration file as a base. app.use(webpackDevMiddleware(compiler, { publicPath: config.output.publicPath })); // Serve the files on port 3000. app.listen(3000, function () { console.log('Example app listening on port 3000!\n'); }); ``` 拡張子がないURLに対して`.html`を付けたものでリダイレクトするだけのコードを追加した。 動作確認のためなので、これで十分だと思う。
(画像は以下のものを使用させていただきました。)
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) <b>※訳注 MongoDBも使ってます。</b> ## 前準備 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ドライバーがあるからそれをプロジェクトにインストールしよう。 <b>※記事が書かれてからMongoDBで更新があったみたいなのでMongoDB周りは次のパッケージを使用しています</b> [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)に書かれている。 <b>CORsについて追記 終わり</b> ## 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) <b>注釈終わり</b> さぁ、Reactを使ってフロントエンド側を作っていこう! ### React `src/index.html`は次のコードをペーストしてね。 ```html <!DOCTYPE html> <html> <body> <div id="app" /> </body> </html> ``` そして、`src/index.js`は次の内容でしよう。 ```js import React from "react"; import ReactDOM from "react-dom"; ReactDOM.render( <div> Hello World! </div> , 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 = () => <div>loading</div> export default Loading ``` よし、実際にルーティングを作っていこう。 `src/routes/home`ディレクトリを作って、その中に`index.js`と`home/home.js`を追加しよう。 ```js // src/routes/home/home.js import React from 'react' const Home = () => (<div>Home!</div>) 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 = () => <LoadableComponent /> 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 = () => <div>404 Not Found :( </div> 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 = () => <LoadableComponent /> 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 ( <Router> <Switch> <Route exact path='/' component={Home} /> <Route exact component={NotFound} /> </Switch> </Router> ) } } 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( <AppRouter />, 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( <ApolloProvider client={client}> <AppRouter /> </ApolloProvider>, 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 ( <div> Home still! </div> ) } } export default Home ``` 上のコードは何してるんだろうね? まずHomeコンポーネントを関数形式から標準的なコンポーネントに変えたよ。 次に、これが魔法みたいなんだけど、`graphql`ラッパーを使ってHomeコンポーネントをラップしているんだ。 これで自動的にgraphqlのクエリを実行するようになるんだ。 (嘘はついていないよ、あまりに一瞬なことだからわからないだけで、ログを確認してみてね) 確認したら、Homeコンポーネントを次のように変更してね。 サーバーにあるnot-todoのリストを取得して表示するようにしているよ。 ```js class Home extends React.Component { render() { const { getNotTodos = [] } = this.props.data return ( <div style={{display: 'flex', justifyContent:'center', marginTop: '10%'}} > <table style={{border:'1px solid black'}}> <tbody> <tr> <th style={{border:'1px solid black'}}>name</th> <th style={{border:'1px solid black'}}>description</th> </tr> { getNotTodos.map(notTodo => ( <tr key={notTodo.name}> <td style={{border:'1px solid black'}}>{notTodo.name}</td> <td style={{border:'1px solid black'}}>{notTodo.description}</td> </tr> )) } </tbody> </table> </div> ) } } ``` 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) <b>注釈終わり</b> 次は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 ( <div style={{display: 'flex', justifyContent:'center', marginTop: '10%'}} > <div style={{display: 'flex', flexDirection:'column'}}> <h3 style={{margin: '0px'}}>Add something to not do!</h3> <input placeholder='Name' name='name' onChange={this.onChange} /> <input placeholder='Description' name='description' onChange={this.onChange} /> <button onClick={this.save}>Save</button> </div> <table style={{border:'1px solid black'}}> <tbody> <tr> <th style={{border:'1px solid black'}}>name</th> <th style={{border:'1px solid black'}}>description</th> </tr> { getNotTodos.map(notTodo => ( <tr key={notTodo.name}> <td style={{border:'1px solid black'}}>{notTodo.name}</td> <td style={{border:'1px solid black'}}>{notTodo.description}</td> </tr> )) } </tbody> </table> </div> ) } } ``` 上のコードのコンストラクタでアプリに状態を持ち込んで、その状態を保存するためのボタンと機能を追加したよ。 保存する中でgraphqlのミューテーションを利用しているのがわかるかな? とても簡単に作れるでしょ? ## 終わり (実際に手を動かしながらいくつかのバグや警告を発見したので元の記事からコードを修正しています。)
2019年4月8日月曜日
空間参照系
ちょっとOpenLayersを触っていて、地図の単位でもある空間参照系というものに初めて触れたのでメモを残しておく ## 空間参照系の種類 ### EPSG:3857 WebにおいてはGoogleMapが初めて採用したEPSG:3857という空間参照系が広く使われている。 地図の画像データを全て正方形で表現できる利点があるため、コンピュータにとって都合がいいので地図アプリでは広く使われている。 ただし、欠点として高緯度付近は歪んでいるのだが、あまり問題にならないだろうという考えの下で生まれた空間参照系なのでそうゆうものである。 ### EPSG:4326 一方、EPSG:4326というアメリカ国防省が策定したものもあり、こちらはメルカトル図法とよく似ているので、地図といえばこちらを思い浮かべると思う。 EPSG:4326は別名WGS84(World Geodetic System 84)とも呼ばれている。 EPSG:4326は緯度経度の値がわかりやすいので、作業を行う際はこちらで行い、OpenLayersで表示するときは上のEPSG:3857を使用する形にするつもりである。 ## 空間参照系の変換 OpenLayersではEPSG:3857とEPSG:4326の間の変換しか対応していない。 空間参照系には用途に合わせてかなりの数の種類があるため、もしかしたらデフォルト機能だけでは満足できないかもしれない。 そのときは[proj4.js](http://proj4js.org/)というパッケージと[epsg.io](https://epsg.io/)というサイトを利用することで対応できるようになるそうだ。 [参考サイト](https://qiita.com/baikichiz/items/8205deb0bdbae48b0b17) ## GeoJSON 地図のある地点を表現するファイルフォーマットもいくつかあり、GeoJSONはJSON形式で表現されたものである。 [geojson.io](http://geojson.io)というサイトが位置情報を編集する上で手軽で、GeoJSON形式以外にも出力できるので便利である。 ちなみにgeojson.ioはEPSG:4326を採用しているので、EPSG:3857を使用するときはなんらかの変換が必要なので注意すること。 OpenLayersでGeoJSONを使うときはファイル内容をそのまま読み込むことが出来るが、座標系の変換や見た目の情報などは反映してくれないので自前でパーサーを作ってなんやこらする必要が出てくると思う。 ## 最後に 空間座標系多すぎ!!
2019年4月7日日曜日
はじめてのCron
試験的にWebサイトの問題をTwitterでツィートしている。 が、 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwTDeWjQayVVpdoD-bSWF7fTcWIQNqWRHyOnRLv8qETX9-voJ7hD_aF2Gz-cdT-6USIIUcanWP3xgknbHq_g0bDzpwhZ971uEgCKUl_Hvrz3lN-WTv8BX8bbxAhNROt_Lh9_5mkVthWpg/s1600/lol.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwTDeWjQayVVpdoD-bSWF7fTcWIQNqWRHyOnRLv8qETX9-voJ7hD_aF2Gz-cdT-6USIIUcanWP3xgknbHq_g0bDzpwhZ971uEgCKUl_Hvrz3lN-WTv8BX8bbxAhNROt_Lh9_5mkVthWpg/s1600/lol.png" data-original-width="128" data-original-height="142" /></a></div> <div style="text-align:center;">twitterに問題をツィートするの面倒だな〜</div> <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiq2yk6qZy-3nq-LGEn-wvpVZrtX4jIMPFCfls1m25l7LkMbn_7-ovU8uE_Upq_lbn5P5lznEfldu8mjt7DU8-SwC4fUglf7uouyYppHBltEzoUX3FxTNHj6sCbzc5WbCAwciAPLVC1HCs/s1600/chirp.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiq2yk6qZy-3nq-LGEn-wvpVZrtX4jIMPFCfls1m25l7LkMbn_7-ovU8uE_Upq_lbn5P5lznEfldu8mjt7DU8-SwC4fUglf7uouyYppHBltEzoUX3FxTNHj6sCbzc5WbCAwciAPLVC1HCs/s1600/chirp.png" data-original-width="84" data-original-height="124" /></a></div> <div style="text-align:center;">それに毎日しないといけないってのも意外と大変。</div> <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSLohHdaMR2_tXMnG1QJlhyphenhyphen9TB4bK-E287w6I4q9VNWH9CToHuzZM9DILY0XlIDo94xy0jwpsD-vK54LyLeYMCsZEfSv1_pNL3IsJyvP7Yc637wXewnlBDX3BM081VHFa5VkisQPcxHDE/s1600/express.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSLohHdaMR2_tXMnG1QJlhyphenhyphen9TB4bK-E287w6I4q9VNWH9CToHuzZM9DILY0XlIDo94xy0jwpsD-vK54LyLeYMCsZEfSv1_pNL3IsJyvP7Yc637wXewnlBDX3BM081VHFa5VkisQPcxHDE/s1600/express.png" data-original-width="75" data-original-height="110" /></a></div> <div style="text-align:center;">あ〜ぁ、ツィートの予約機能があったら楽できるのになぁ〜。</div> <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh24nj0gSsYZpaX5Z5BFNYAFXHtXmUCaodYKfCqs0bJnRXpIHxgPxXK7kQ8I0amJLdjPyoDedaIyyniA8Ld4Fj6kuCjQsgNCROZrhnAI7TyKk-nN31_Q7fwZRdIowVEWMPEBU5-X1NSV9w/s1600/be_tampered.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh24nj0gSsYZpaX5Z5BFNYAFXHtXmUCaodYKfCqs0bJnRXpIHxgPxXK7kQ8I0amJLdjPyoDedaIyyniA8Ld4Fj6kuCjQsgNCROZrhnAI7TyKk-nN31_Q7fwZRdIowVEWMPEBU5-X1NSV9w/s1600/be_tampered.png" data-original-width="224" data-original-height="152" /></a></div> <div style="text-align:center;">ほら、これを使え!</div> <div style="text-align:center;font-size:32px;">・</div> <div style="text-align:center;font-size:32px;">・</div> <div style="text-align:center;font-size:32px;">・</div> <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF22uBZBdA63ptQUblnFWQJ1WRoOfhg3xx7uUuIhYH8M97SELnTnQ8dZYAblq02RL_YVV91rwH22Nn3eIKe1K5EF56vefY_B6bdtBanRZAhq4T5kURgr7SBSVmNJcHpEI0S7GV_MUnlzI/s1600/be+attacked.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF22uBZBdA63ptQUblnFWQJ1WRoOfhg3xx7uUuIhYH8M97SELnTnQ8dZYAblq02RL_YVV91rwH22Nn3eIKe1K5EF56vefY_B6bdtBanRZAhq4T5kURgr7SBSVmNJcHpEI0S7GV_MUnlzI/s1600/be+attacked.png" data-original-width="225" data-original-height="156" /></a></div> <div style="text-align:center;">よそ見しているんじゃねぇ!</div> ## Twitter APIを使う(node-twitter) というわけでTwitter APIでツィートが出来るとのことなので、cronと組み合わせて決まった時間に問題をツィートする機能を作ってみた。 Twitter APIはnode-twitterを使うことで簡単に目的を果たすことができた。 しかし、cronの扱いに困った困った困っちゃんになってしまったので、メモを残しておく。 ## Cron設定ってどうするの? なんか、/etc/cron***的なところにcron用のファイルを置いておくとcronデーモンが勝手に仕事をしてくれるそうっすよ。 - cron.d 汎用的なタスクを行うときにファイルを置く - cron.daily 一日一回実行したいときにファイルを置く - cron.hourly 一時間に一回実行したいときにファイルを置く - cron.weekly 一週間に一回実行したいときにファイルを置く - cron.month 一月に一回実行したいときにファイルを置く - crontab cron.daily,cron.hourly cron.weekly cron.month内にあるcronファイルを実行する内容になっている ちなみにcrontabというコマンドも存在していて、今回はそれを使った。 ```bash crontab template-file #Gitで記録するわけには行かないので後からAPIキーを書き込む crontab -e ``` ## Twiiter APIを使うには環境変数を設定しないといけないぞ! 今回の困った部分。 環境変数の指定の仕方だが、なんのことないcronファイルにそのまま書ける。 一度知ってしまえば簡単である。(数十回ぐらい設定をいじっては直していました・・・) が、環境変数の展開はできないそうなのでbashなど既存の環境を使用するのではなく、cron用に一から環境を設定してあげる意識で使うといいと思う ``` USE_ENV='use variable' NEW_PATH='' # 実際にcron内部に設定されている環境変数を確認したいときは以下のコマンドを使うといい 10 * * * * env > /tmp/env.log ``` ## おしまい 最近Linux関係の調べものが多くなってきた感がある。 あとはsystemctlも扱えるようになればいいなぁと思ったり思わなかったり。
(画像は以下のものを使用させていただきました。)
2019年4月5日金曜日
Golang環境構築
簡単なメモとしてGolangの環境構築で使ったものを書いていく。 ## goenv はじめは公式ホームページからバイナリをインストールしていたが結構面倒。 で、ちょっと調べんてみると<a href="https://github.com/syndbg/goenv">goenv</a>というものがいろんなバージョンを一括管理出来て便利とのことだったので使ってみた。 Githubに書かれているインストールの手順はちょっと面倒だけど、Macだとパッケージ管理ツールからインストールできるとかないとか。 ```bash git clone https://github.com/syndbg/goenv.git ~/.goenv #環境変数の設定 echo 'export GOENV_ROOT="$HOME/.goenv"' >> ~/.bash_profile echo 'export PATH="$GOENV_ROOT/bin:$PATH"' >> ~/.bash_profile #バッシュ環境にgoenvを登録する echo 'eval "$(goenv init -)"' >> ~/.bash_profile #(オプション)Golangの環境変数もgoenvで管理したいときは以下のも追加する # ただし、上のeval "$(goenv init -)"を実行してからでないといけない echo 'export PATH="$GOROOT/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="$GOPATH/bin:$PATH"' >> ~/.bash_profile ``` 上のコマンドで初期設定は完了する。 あとはターミナルを再起動するなり、なんなりすると使う準備は完了する。 次のコマンドでインストールしたいバージョンを指定してGolangをインストールする。 ```bash goenv install 1.12.1 #細かいバージョンまでわからないときは途中まで書くとあとに続く候補を教えてくれる。 goenv install 1.7 ``` ## direnv GolangではGOPATHなど専用の環境変数を設定することで作業ディレクトリをしていする。 なのでgolangを使うときはあるディレクトリにソースコードやらを入れる必要があるが、ちょっと違うディレクトリでも使いたいとなると毎回環境変数を設定し直す必要が出てくる。 これは面倒なのでdirenvというコマンドを使用し環境変数の設定を自動的に行ってくれるようにする こちらもgolang製のコマンドだが、OSのパッケージ管理ツールからインストールできる。 `eval "$(direnv hook bash)"`でdirenvの初期化を行っているので実行忘れをすると当然動かない。 ```bash apt-get install direnv ## 次のコマンドを忘れない。 # ※2019/4/18 ~/.bash_profileから~/.bashrcに修正 echo 'eval "$(direnv hook bash)"' >> ~/.bashrc ``` 使うときは、環境変数を変更したいディレクトリに.envrcファイルを作る。 あとはその中に設定を書いていけばいい。 ```bash export GOROOT=$(go env GOROOT) export GOPATH=$(pwd) ``` ## あとは便利パッケージをインストールをして終わり。以上! <a href="https://qiita.com/okatai/items/a6f966d2075d69d21e74">こちらのページにあるものをインストールするといいみたい</a> - goimports - Golint などなど。
2019年4月3日水曜日
時間がずれている!!
Amazon lightssailで自作のWebページを公開したところ、データベースの時刻がどうもずれてしまっていることに気がつく。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" data-original-width="210" data-original-height="135" /></a></div> <div style="text-align:center;">やばい、このままだとデータが正しく取得できないぞ!</div> ## なんか9時間きっかりずれているなぁー 9時間といえばUTC時刻の基準と成っているイギリスと日本の時差になる。 9時間といえばUTC時刻の基準と成っているイギリスと日本の時差になる。 9時間といえばUTC時刻の基準と成っているイギリスと日本の時差になる。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgiUrYNqPB7hKyov1D53ZHy9KCNsg9KIcRnrrJW2TtAlD8ibpg2wssPgTXEWDIsCTutN9Sac4y5cQDSVdWZe5vU-SNO5ngtiMZKa5V-lkln3REyI_7rNogka65ggqkJtvy1MXFdUnls6fQ/s1600/be_surprised.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgiUrYNqPB7hKyov1D53ZHy9KCNsg9KIcRnrrJW2TtAlD8ibpg2wssPgTXEWDIsCTutN9Sac4y5cQDSVdWZe5vU-SNO5ngtiMZKa5V-lkln3REyI_7rNogka65ggqkJtvy1MXFdUnls6fQ/s1600/be_surprised.png" data-original-width="80" data-original-height="139" /></a></div> <div style="text-align:center;">ハッ、わかったぞ!</div> 原因はタイムゾーンが開発PCと異なっているんだ! そうとわかれば早速タイムゾーンを合わせるぞ===っ!! ```bash timedatectl set-timezone Asia/Tokyo ``` あとは、データベースにデータを入れ直して確認すると・・・ <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgnwA5Jo_EYgeUagNVxYEoBOuQiCLPrFti-PZq1mf-bn77E8FVP2n9fBtlns31NSiO7ENo50pt2r3vljsmbaQy62nTUWypuohlExtBn2YD0FPAdjswrjrvypW4csvulrvlpOTMoAIbjytk/s1600/Screenshot_2019-04-03+kanji-assembly.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgnwA5Jo_EYgeUagNVxYEoBOuQiCLPrFti-PZq1mf-bn77E8FVP2n9fBtlns31NSiO7ENo50pt2r3vljsmbaQy62nTUWypuohlExtBn2YD0FPAdjswrjrvypW4csvulrvlpOTMoAIbjytk/s200/Screenshot_2019-04-03+kanji-assembly.png" width="200" height="100" data-original-width="1600" data-original-height="802" /></a></div> <div style="text-align:center;">やったぜ!!</div> ## 終わりに <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF22uBZBdA63ptQUblnFWQJ1WRoOfhg3xx7uUuIhYH8M97SELnTnQ8dZYAblq02RL_YVV91rwH22Nn3eIKe1K5EF56vefY_B6bdtBanRZAhq4T5kURgr7SBSVmNJcHpEI0S7GV_MUnlzI/s1600/be+attacked.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF22uBZBdA63ptQUblnFWQJ1WRoOfhg3xx7uUuIhYH8M97SELnTnQ8dZYAblq02RL_YVV91rwH22Nn3eIKe1K5EF56vefY_B6bdtBanRZAhq4T5kURgr7SBSVmNJcHpEI0S7GV_MUnlzI/s1600/be+attacked.png" data-original-width="225" data-original-height="156" /></a></div> <div style="text-align:center;">公開する前に確認しろ!!</div> <div style="text-align:center;">すいませーん!!</div> 公開して15分足らずで、サーバー管理をしている人の心理を追体験できました!
(画像は以下のものを使用させていただきました。)
2019年3月31日日曜日
Google App Engineにデプロイしたときの話
ようやくWebサイトを公開してもいい感じになったのでGoogle App Engineでデプロイしようとした。 ## さぁ、デプロイだ! ```bash gcloud app deploy gcloud app browse ``` <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtR9ntWQ125x-Zi_27VDmBwSGteuZV75rp0LSCRAIpn89XDmMiAlJXtc5QqtscXxD-nlgX0jUx8j9U1UzuPjDf5mvDUJt90aO9BNSf2UO58sNqYy364i1nSm4cSQahmnAnUDZkueuKsMA/s1600/wait.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtR9ntWQ125x-Zi_27VDmBwSGteuZV75rp0LSCRAIpn89XDmMiAlJXtc5QqtscXxD-nlgX0jUx8j9U1UzuPjDf5mvDUJt90aO9BNSf2UO58sNqYy364i1nSm4cSQahmnAnUDZkueuKsMA/s1600/wait.png" data-original-width="60" data-original-height="105" /></a></div> <div style="text-align:center;">? 画面が真っ白だぞ??</div> ログを見ると何やら失敗しているっぽい。 ## npmで試す ドキュメントの方はnpmを使っているので、いったんyarnを使うのやめてnpmを使って試してみる。 が、ダメ。 ログを見ると ``` Error: Cannot find module '<install packages>' ``` とあった。 yarnの方にもそういえば同じログがあったので、パッケージのインポート周りで失敗しているっぽい? ## もしかして? devDependenciesしてます? してまーす。 というわけで、慌ててdependenciesにインストールし直す。 が、ダメ。 ``` 0x6d2937abc60 node::Abort() [node] ``` と出て、闇がさらに深くなってしまった。 ## デフォルトのnuxtで試してみる 何がおかしいのかさっぱりなので、Nuxtを初期化した状態のものでデプロイが出来るか確認してみる。 npmを使い、`npm create nuxt-app`で現在のプロジェクトに近い設定で初期化。 設定した項目は - Express - Vutify - PWA などなど ちゃんと`npm run build`をしてからデプロイを実行! へへ、これは流石に動くだろう。 ``` Error: Server Error The server encountered an error and could not complete your request. Please try again in 30 seconds. ``` 何も変わらない・・・だと・・・ ## 助けて、Qiita!! ポートとホスト名をそれぞれ8080、0.0.0.0と環境変数で指定してあげるといいんだよ! ありがとう!Qiita!! ありがとう!記事を書いてくれた人!! ``` Error: Server Error The server encountered an error and could not complete your request. Please try again in 30 seconds. ``` なん・・・だ・・と・・・・ ## まだだ!まだまっさらな状態のNuxtが残っている。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnS6Tp9am9LL9iwSAiTkIV715EGk2RXd16r7moM4aQcHdTfzklOqY1jltFp0aRqCVX6wSbNW3waCaLHO2mrR5Mf_10nzLxjPRZ8r93LKpnimLBHSBWaqqR08kbICnwfm-rCJiYMPXYOxs/s1600/Screenshot_2019-03-31+default.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnS6Tp9am9LL9iwSAiTkIV715EGk2RXd16r7moM4aQcHdTfzklOqY1jltFp0aRqCVX6wSbNW3waCaLHO2mrR5Mf_10nzLxjPRZ8r93LKpnimLBHSBWaqqR08kbICnwfm-rCJiYMPXYOxs/s320/Screenshot_2019-03-31+default.png" width="320" height="301" data-original-width="510" data-original-height="480" /></a></div> <div style="text-align:center;">やったー!動いた!!</div> ## 何が違うの? どうやらnuxt buildをしているかしていないかが失敗の原因っぽい。 サーバー側ではExpressを使用しているが、そのときのnpm run startはnode経由でサーバーを立ち上げている。 なにか違いがあるのか、nuxtのコードを読んでみたが、次のコードでNuxtインスタンスを生成していた。 ```js //node_modules/@nuxt/cli/dist/cli-chunk.js async getNuxt(options) { const { Nuxt } = await core(); const nuxt = new Nuxt(options); await nuxt.ready(); return nuxt } ``` ちなみにnuxt startの実行したときに処理されるコードはここになると思う。 ```js //node_modules/@nuxt/cli/dist/cli-chunk4.js async run(cmd) { const config = await cmd.getNuxtConfig({ dev: false, _start: true }); const nuxt = await cmd.getNuxt(config); // Listen and show ready banner await nuxt.server.listen(); __chunk_2.showBanner(nuxt); } ``` これと生成時にExpressを使うように設定したときのサーバーコードを比較してみると、特に違った箇所が見受けられなかった。 両者ともNuxtインスタンスを作成してリクエスト待ちを行っている。 違うといえば、Expressの方はexpressにnuxt.renderを渡しているという点か… ```js async function start() { // Init Nuxt.js const nuxt = new Nuxt(config) const { host, port } = nuxt.options.server // Build only in dev mode if (config.dev) { const builder = new Builder(nuxt) await builder.build() } else { await nuxt.ready() } // Give nuxt middleware to express app.use(nuxt.render) // Listen the server app.listen(port, host) consola.ready({ message: `Server listening on http://${host}:${port}`, badge: true }) } ``` ## printfデバッグしてみる printfデバッグはつらい。が、やるしかない。Google App Engineのログに残ってくれたらいいのだが・・・ (※ローカル環境で試すだけで十分です) ちなみにこんな感じにログを差し込んだ ```js async function start() { // Init Nuxt.js const nuxt = new Nuxt(config) console.log('通ったよ') const { host, port } = nuxt.options.server // Build only in dev mode if (config.dev) { const builder = new Builder(nuxt) await builder.build() } else { await nuxt.ready() } console.log('通ったよ1') // Give nuxt middleware to express app.use(nuxt.render) console.log('通ったよ2') // Listen the server app.listen(port, host) consola.ready({ message: `Server listening on http://${host}:${port}`, badge: true }) console.log('通ったよ3') } ``` 実行結果は・・・ ``` I GET 500 0 B 8.8 s Firefox 66 / GET 500 0 B 8.8 s Firefox 66 5ca0b63f00ff066b499bee7a370001627e6b616e6a692d617373656d626c792d3233363230380001323031393033333174323134333230000100 A 通ったよ // 省略 A 1: 0x7779f97ec60 node::Abort() [node] ``` と、真ん中あたりの条件分岐が怪しいことがわかった。 これを繰り返していくと、await nuxt.ready()で失敗していることがわかった。 nuxt.ready()内部に問題があるというわけではなく、Nuxtインスタンスを生成するときに渡した設定が原因ではないかと思う。 というわけで比較してみると、buildオプションが大きく異なっていることがわかった 試しに、Expressだけ有効にしてほかは何も使わない設定でNuxtプロジェクトを作成すると無事デプロイに成功した! ## 悪いビルドオプションは誰だぁ!! 「誰もいません!!」 「じゃ、誰だ悪さをしているんだ!!」 「こいつです!!!」 <div style="font-size: 64px;"><b>Progressive Web Apps</b></div> 正しくはmodulesの@nuxtjs/pwa。 原因はわからないが、とりあえずこいつが原因だとわかった。 それを使わない状態でyarnを使っても、問題なくデプロイ出来たので、@nuxtjs/pwaが悪い。 以上、原因究明終わり。 ## 終わりに 検証のため幾度もなくデプロイを繰り返したが、デプロイする際にはGoogle Cloud Buildを利用する必要があり、それは有料サービスである。 つまり、お金がかかるということである。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" data-original-width="210" data-original-height="135" /></a></div> <div style="text-align:center;">まずいのではないですか?</div> が一日120ビルド分までは無料だし、それを超えても$0.003/ビルド分と無いに等しいので実質無料である。 一年間$30まで無料な体験版もあり、Google App Engineは試しやすいIaaS環境だと思う。 ※ ビルド分 ビルドにかかった時間を分で割った単位
(画像は以下のものを使用させていただきました。)
2019年3月29日金曜日
パスワードの保存をしようとしてブラウザ機能を利用できなかったときの話
今作っているWebサイトで色々なサイトでよくあるパスワードを保存する機能を実装してみようかと調べてみたら、 formタグ内のボタンのtypeをsubmitにしてあげるといいとあったので早速試してみた。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" data-original-width="107" data-original-height="189" /></a></div> <div style="text-align:center;">おっ、できたぞ!簡単じゃん!</div> と思っていたら、ログインが何故かできない。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtR9ntWQ125x-Zi_27VDmBwSGteuZV75rp0LSCRAIpn89XDmMiAlJXtc5QqtscXxD-nlgX0jUx8j9U1UzuPjDf5mvDUJt90aO9BNSf2UO58sNqYy364i1nSm4cSQahmnAnUDZkueuKsMA/s1600/wait.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtR9ntWQ125x-Zi_27VDmBwSGteuZV75rp0LSCRAIpn89XDmMiAlJXtc5QqtscXxD-nlgX0jUx8j9U1UzuPjDf5mvDUJt90aO9BNSf2UO58sNqYy364i1nSm4cSQahmnAnUDZkueuKsMA/s1600/wait.png" data-original-width="60" data-original-height="105" /></a></div> <div style="text-align:center;">もしかして、formタグのアクションが実行されている?</div> 既にある処理が眠っていたFormタグのアクションで上書きされている?ような感じで、うまく動作してくれない状態になってしまった。 ## 普通はformタグのアクションとリダイレクトで作っているはずが・・・ 実のところformタグとリダイレクトで認証機能を作っていたら、とっくの前にこの問題は解決、いや問題すらなかったのだが、 <b><i>なぜか</i></b>、自分はAjaxで認証機能を作っていたのでブラウザの持つ機能を使えない状態になっていた。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" data-original-width="210" data-original-height="135" /></a></div> <div style="text-align:center;">何しているんだっ!</div> いや<a href="https://ja.nuxtjs.org/examples/auth-external-jwt">Nuxt.jsのサンプル</a>とか、以前<a href="https://tositeru.blogspot.com/2019/03/authenticate-nodejs-api-with-json-web.html">Blogに書いた参考サイト</a>とかだと特にそんなこと書いていなかったので・・・、 てっきりそういうものかと思って作っていました… ## 自前でパスワード管理をすることに 便利な機能が使えなかったら自分で作るしかないじゃんと、ブラウザ側で記録して、毎回参照する形で妥協して対応した。 ちょっと背伸びした結果がこれなので、次はたくさんのひとが使っていて、型が扱えて、便利なフレームワークで作ってみたいと思う。
(画像は以下のものを使用させていただきました。)
2019年3月28日木曜日
SequelizeのバージョンをV5に上げたときの話
SequelizeのバージョンがV5に上がっていたのでアップグレートしたときの話をつらつらと書いていく。 ## yarn upgrade そもそもアップグレートすることが初体験なので、パッケージ管理ツールの使い方も知らない状態だった。 とりあえず、以下のコマンドでアップグレードした。 ```bash yarn upgrade sequelise@^5.2.1 ``` が、以下のようでもいい。 ```bash yarn upgrade sequelize --latest ``` 上のコマンドで最新のバージョンが使われるようになる(--latest)。 はじめのものはバージョン指定したもので、`^`(caret)はメジャーバージョンを変えない範囲でアップグレードをすることを示している。 要は、@^3.1.2なら3.1.2<= 〜 <4.0.0範囲のバージョンが使われるとのこと [yarn caret-ranges](https://yarnpkg.com/lang/en/docs/dependency-versions/#toc-caret-ranges) これで無事アップグレートができた ## DeprecationWarning: A boolean value was passed to options.operatorsAliases. This is a no-op with v5 and should be removed. SequelizeV5からSequelizeインスタンスを作成するときに比較演算などの演算子には必ず別名をつけることを推奨するようになっている。 これはセキュリティ的な問題で実際に使用する演算子だけをインスタンス生成時に別名として指定することで、攻撃者が好き勝手な演算子を使えなくする効果がある。 ```js //アプリで使っている演算子だけを別名定義する const usedOperatorsAliases = { $and: Sequelize.Op.and, $or: Sequelize.Op.or, } const sequelize = new Sequelize('database', { operatorsAliases: usedOperatorsAliases }) const model = Sequelize.define(...) model.findOne({ where: { $or: ... $and: ... } }) ``` いちいち定義するのがめんどくさいなら、[公式ドキュメントのOperators security](http://docs.sequelizejs.com/manual/querying.html#operators)からコピペするといい Sequelize CLIの方はまだ対応していないみたいなのでこの警告が出ている。 だけど開発用に使用するだけなので、全ての演算子の別名を指定する形で問題はないかと思う。 ## 終わり <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" data-original-width="107" data-original-height="189" /></a></div> <div style="text-align: center;">アップグレート完了!</div> 思いの外、修正する部分が少なく助かった
(画像は以下のものを使用させていただきました。)
2019年3月24日日曜日
Gitの衝突とマージ
今作っているものはGitでmasterとdevelopでブランチを分けて作業している。 なのであるタイミングでdevelopの内容をmasterへマージするのだが、マージコンフリクトが起きてしまった。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" data-original-width="210" data-original-height="135" /></a></div> <div style="text-align: center;">またかよっ!</div> そもそも一人で作っているのになんでmasterとdevelopでコンフリクトが起きるんだよっと思ったら、 うっかりマスターに変なコミットしてしまって、git revertで打ち消していたことを思いだした。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwTDeWjQayVVpdoD-bSWF7fTcWIQNqWRHyOnRLv8qETX9-voJ7hD_aF2Gz-cdT-6USIIUcanWP3xgknbHq_g0bDzpwhZ971uEgCKUl_Hvrz3lN-WTv8BX8bbxAhNROt_Lh9_5mkVthWpg/s1600/lol.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwTDeWjQayVVpdoD-bSWF7fTcWIQNqWRHyOnRLv8qETX9-voJ7hD_aF2Gz-cdT-6USIIUcanWP3xgknbHq_g0bDzpwhZ971uEgCKUl_Hvrz3lN-WTv8BX8bbxAhNROt_Lh9_5mkVthWpg/s1600/lol.png" data-original-width="128" data-original-height="142" /></a></div> <div style="text-align: center;">ハハッ、やっちまったな!</div> <div style="text-align: center; font-size: 32px;">.<br>.<br>.<br>.<br>.<br>.</div> <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF22uBZBdA63ptQUblnFWQJ1WRoOfhg3xx7uUuIhYH8M97SELnTnQ8dZYAblq02RL_YVV91rwH22Nn3eIKe1K5EF56vefY_B6bdtBanRZAhq4T5kURgr7SBSVmNJcHpEI0S7GV_MUnlzI/s1600/be+attacked.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF22uBZBdA63ptQUblnFWQJ1WRoOfhg3xx7uUuIhYH8M97SELnTnQ8dZYAblq02RL_YVV91rwH22Nn3eIKe1K5EF56vefY_B6bdtBanRZAhq4T5kURgr7SBSVmNJcHpEI0S7GV_MUnlzI/s1600/be+attacked.png" data-original-width="225" data-original-height="156" /></a></div> git reset --hardで強制的に消しても良かったけど、なんとなくそのままにしておいたツケが回ってきた感じである。 ## rebaseよりmerge こういった問題に直面したとき自分はgit rebaseとgit push -fを使っていたが、 コミットログが消えてしまうのは困ることに気がついてからマージでなんとかしようと心がけている。 がまだまだ初心者。[ドキュメントを見ながらの作業](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-%E9%AB%98%E5%BA%A6%E3%81%AA%E3%83%9E%E3%83%BC%E3%82%B8%E6%89%8B%E6%B3%95#r_advanced_merging)になるので、心の中ではコンフリクトは起きないでくれと願っている。 基本的にmaster側でどうにかするのではなくて、develop側でコンフリクトを解消する方針を立てている。 今回もそれに従って、masterにある変更点をdevelopにマージすることでコンフリクトを解消し、それをmasterにマージする流れになる。 なんだか二度手間ではあるが、OSS的にはそっちのほうがいいとかないとか ただ、今回は何も考えなくマージするとdevelop側のファイルが消えてしまう状態になってしまった。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtR9ntWQ125x-Zi_27VDmBwSGteuZV75rp0LSCRAIpn89XDmMiAlJXtc5QqtscXxD-nlgX0jUx8j9U1UzuPjDf5mvDUJt90aO9BNSf2UO58sNqYy364i1nSm4cSQahmnAnUDZkueuKsMA/s1600/wait.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtR9ntWQ125x-Zi_27VDmBwSGteuZV75rp0LSCRAIpn89XDmMiAlJXtc5QqtscXxD-nlgX0jUx8j9U1UzuPjDf5mvDUJt90aO9BNSf2UO58sNqYy364i1nSm4cSQahmnAnUDZkueuKsMA/s1600/wait.png" data-original-width="60" data-original-height="105" /></a></div> <div style="text-align: center;">うわー出たよ、Git使っていて困るパターンだ…</div> ## git merge -Xours \<branch\> ドキュメントにはコンフリクトがある場合どちらか一方の変更を取り込むよう指定できるオプションについて書かれていたので、まずそちらを試してみた。 ```bash git checkout develop # コンフリクトがあったらdevelopの方を採用する git merge -Xours master # こっちだとmasterの変更点が採用される git merge -Xtheirs master ``` が、ダメ。 そもそもコンフリクトが起きていなかったので、これではダメである。 ## git merge -s ours \<branch\> ドキュメントのその次の説明ではGitを騙して何もマージしていいないのにマージしたことにするオプションを紹介していたのでそちらを試してみた。 ちょうど、master/developブランチで開発をしている想定での説明だったのでこれはいいのではないかと期待を抱いてタイピングする。 ```bash git checkout develop #マージをしたつもりマージ developの内容そのままで保持される git merge -s ours master ``` <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSKIxJpYiqBTqd4LvVKPjCC_BG990xDE7Cp67Q7PYrQBy7w7uuz6mvHCIh7vuTWQrQ5_ijFrLLKXUOyy0weJ7drqGegTWytTYIZEJ71qV0i1kPLEiDJqIWezCKsIUIGWxnwCtaRYi-RC4/s1600/Screenshot+from+2019-03-23+20-26-20.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSKIxJpYiqBTqd4LvVKPjCC_BG990xDE7Cp67Q7PYrQBy7w7uuz6mvHCIh7vuTWQrQ5_ijFrLLKXUOyy0weJ7drqGegTWytTYIZEJ71qV0i1kPLEiDJqIWezCKsIUIGWxnwCtaRYi-RC4/s320/Screenshot+from+2019-03-23+20-26-20.png" width="320" height="92" data-original-width="771" data-original-height="221" /></a></div> <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrU9fN4DGRFJWrucOGBGcqHthNKmB2CRF0QLtjxZq8cbID5NbQYEhNCP3Wgl-3VYq-iPSSwVNxbKABWpXmgj12I6wSdj8CtPavewEsPxNwZt-Rt-pyv9_G4OQVd6MDgKseJ2i9ugcerjY/s1600/achieve.png" data-original-width="107" data-original-height="189" /></a></div> <div style="text-align: center;">やったぜ!</div> ミスした原因お前じゃないかとは思うが、Gitを使っていたときに問題に直面して解決したときは心の中ではこんな感じで一喜一憂な起伏が生じてしまう。 やったぜ! 上のコマンドはもともとdevelopブランチにバグ修正用のブランチをマージしたときに上のコマンドでmasterにもバグ修正用ブランチをマージすることで、 将来developをmasterにマージしたときにコンフリクトが発生させないために使うものだそうだ。 いつでも使えるコマンドではないが、今回のケースではちょうどいい動作であったので採用し問題を解決できた。 解決できてよかった!
(画像は以下のものを使用させていただきました。)
2019年3月23日土曜日
requireとimport
サーバーサイドとクライアントサイドでモジュールのインポート/エクスポートを異なる書き方をしていた。 サーバーサイドはrequireを、クライアント側はimportを使ってコーディングを行っていた。 まぁ、ちょうどどちらのサイドを書いているのか区別がつくからまぁいいかと思っていたところ、 サーバーサイドのコードをクライアントでも使いたい場面が出てきて、何も考えずサーバーサイドのコードなのでrequireでインポートしたところエラーが出てきた。 ```js import Hoge from './hoge' import Foo from '../Foo' // error!! const WantToUseModule = require('../server/module') ``` エラーが起きた場所はインポートしたいモジュールの中で、`"exports" is read-only`とエラーメッセージを表示していた。 <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" data-original-width="210" data-original-height="135" /></a></div> <div style="text-align: center;">割と困る</div> ブラウザ環境などクライアント側では一部でしかimport構文をサポートしていないため、このようなエラーが起きた。と思う。 ## サーバー側での対応 開発環境ではNodejsのバージョン10を使用しているため、完全ではないがES6のモジュール機能に対応している。 なので、サーバーサイドでもそれを使うことでエラーはなくなり、正常に動作した。 <div style="text-align: center;font-size:32px">.<br>.<br>.</div> <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjNbVBFEMzh3sqOUqeGpvvlI4ZVfW-r-30_30nT5GlxaFpgCYpPs_kRPoLuTV_A-IhPfHWZGRN-OmH0qq4XMVrb8WvVImynolv-Dx8R9lcdgVTNwr3-2SIaJcc0aLMPmvYOhGqwUA8Hc3A/s1600/be+attacked.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjNbVBFEMzh3sqOUqeGpvvlI4ZVfW-r-30_30nT5GlxaFpgCYpPs_kRPoLuTV_A-IhPfHWZGRN-OmH0qq4XMVrb8WvVImynolv-Dx8R9lcdgVTNwr3-2SIaJcc0aLMPmvYOhGqwUA8Hc3A/s1600/be+attacked.png" data-original-width="225" data-original-height="156" /></a></div> <div style="text-align: center;font-size:32px">はじめから使っとけ!!</div> そのとおりです。何も言えないです。
(画像は以下のものを使用させていただきました。)
2019年3月22日金曜日
動的インポートとJSでのポインタ渡し
webpackとOpenLayerを使うと大体こんな感じのコードになると思う。 ```js import Feature from 'ol/Feature.js'; import Geolocation from 'ol/Geolocation.js'; import Point from 'ol/geom/Point.js'; import {Draw, Modify, Snap} from 'ol/interaction.js'; import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer.js'; import Map from 'ol/Map.js'; import {OSM, Vector as VectorSource} from 'ol/source.js'; import {Circle as CircleStyle, Fill, Stroke, Style} from 'ol/style.js'; import View from 'ol/View.js'; // なんらかの処理 ``` <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtR9ntWQ125x-Zi_27VDmBwSGteuZV75rp0LSCRAIpn89XDmMiAlJXtc5QqtscXxD-nlgX0jUx8j9U1UzuPjDf5mvDUJt90aO9BNSf2UO58sNqYy364i1nSm4cSQahmnAnUDZkueuKsMA/s1600/wait.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtR9ntWQ125x-Zi_27VDmBwSGteuZV75rp0LSCRAIpn89XDmMiAlJXtc5QqtscXxD-nlgX0jUx8j9U1UzuPjDf5mvDUJt90aO9BNSf2UO58sNqYy364i1nSm4cSQahmnAnUDZkueuKsMA/s1600/wait.png" data-original-width="60" data-original-height="105" /></a></div> <div style="text-align:center;">毎回このインポート書くの面倒だなぁ・・・</div> というわけで、いわゆるc言語での必要なヘッダーをすべてインクルードした`all.h`みたいなものを作ってみた。 さらにES6では動的インポートというものも導入されたそうなのでそれも使ってみる。 これで不要なものはインポートしなくて済むから効率的だし一つのファイルをインポートするだけで使えるし、よかったよかった。 ```js // olDynamic.js All.h的なもの export default { Map: () => import(/* webpackChunkName: "ol/Map" */ 'ol/Map'), View: () => import(/* webpackChunkName: "ol/View" */ 'ol/View'), Layer: () => import(/* webpackChunkName: "ol/layer" */ 'ol/layer'), source: () => import(/* webpackChunkName: "ol/source" */ 'ol/source'), Feature: () => import(/* webpackChunkName: "ol/Feature" */ 'ol/Feature'), Geolocation: () => import(/* webpackChunkName: "ol/Geolocation" */ 'ol/Geolocation'), interaction: () => import(/* webpackChunkName: "ol/interaction" */ 'ol/interaction'), style: () => import(/* webpackChunkName: "ol/style" */ 'ol/style'), geom: { Point: () => import(/* webpackChunkName: "ol/geom/Point" */ 'ol/geom/Point') } } ``` ```js // 使う側 import olDynamic from './olDynamic' const Map = await olDynamic.Map() const Feature = await olDynamic.Feature() const Geolocation = await olDynamic.Geolocation() const Feature = await olDynamic.Feature() const Point = await olDynamic.geom.Point() const {Draw, Modify, Snap} = await olDynamic.interaction() const {Tile as TileLayer, Vector as VectorLayer} = await olDynamic.Layer() const {OSM, Vector as VectorSource} = await olDynamic.source() const {Circle as CircleStyle, Fill, Stroke, Style} = await olDynamic.style() const View = await olDynamic.View() // なんらかの処理 ``` <div style="text-align:center;font-size: 32px;">.<br>.<br>.</div> <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhnAKEh2YMuJDGnBhHK-yFOvpK2iHMtRRgiWUjt7TnJ_k7PMvLgXi8_8yxbpOTMC_8MguqeEUZRkSqZI6zzjWU1AvyuQLzRUQEucl-O-5UOu3y5eZ0kQdEfLe0x17-zVoPDZKJ9SrwQvgc/s1600/lol.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhnAKEh2YMuJDGnBhHK-yFOvpK2iHMtRRgiWUjt7TnJ_k7PMvLgXi8_8yxbpOTMC_8MguqeEUZRkSqZI6zzjWU1AvyuQLzRUQEucl-O-5UOu3y5eZ0kQdEfLe0x17-zVoPDZKJ9SrwQvgc/s1600/lol.png" data-original-width="128" data-original-height="142" /></a></div> <div style="text-align:center;">あんまり変わってなくない?</div> require()を使っているみたいでバージョンが逆戻りしている感じになっているし、毎回同期待ちしているのも効率的ではないから、悪くなっているぞ! ## 本題 という感じの経緯を辿ったが、やっぱり毎回importを書くのは面倒だし、動的インポートは使いたい。 だから、下のコードみたいに読み込めるようにした。 これだと[Quick Start](https://openlayers.org/en/latest/doc/quickstart.html)みたいに`ol.XXX`で使えるから割と理想的。 ```js import olDynamic from './js/ol-dynamic' async function main() { const ol = await olDynamic.imports(['Map', 'View', 'Layer', 'source', 'Feature', 'Geolocation', 'interaction', 'style', 'geom/Point']) // 何らかの処理 const map = new ol.Map({...}) } main() ``` `olDynamic.imports`の内容は以下のもの ```js imports = async function(list) { let importList = [] for(const l of list) { const dynamicImport = getProperty(this, l) importList.push(dynamicImport()) } const modules = await Promise.all(importList) let ol = {} for (let i=0; i<list.length; ++i) { setProperty(ol, list[i], modules[i]) } return ol } ``` 重要な部分は`Promise.all`とgetProperty/setProperty関数だ。 `Promise.all`のおかげで、はじめの失敗例みたいに一つ一つ読み込むのではなく並列的に読み込むようになった。 そして、`Promise.all`に渡すパラメータを生成するためにgetProperty関数を利用してリストを作った。 getProperty関数の内容は次のものになる ```js function getProperty(obj, path) { let prop = obj for(const p of path.split('/')) { if (!prop[p]) { throw new Error(`Access unknown property name... path=${path}`) } prop = prop[p] } return prop } ``` 最後に`Promise.all`の返り値からインポートしたモジュールのリストを作る。 setProperty関数をつかうのだが、それははじめgetProperty関数と似た感じになっていた。 しかし、実行してみるとちゃんと動いてくれない。 ```js //問題のあるsetProperty関数 function setProperty(obj, path, value) { let prop = obj for(const p of path.split('/')) { prop[p] = {} // この2行で意図しない処理になる prop = prop[p] // ... } prop = value // 意図しない処理になる } ``` <div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSIgaNs_Gn2upPJ_2hpeKX8ipWv72bwDS9l0xc3NLABbm_V82BalMiEjLc6T5zC5VYcbYttjeLGoNSN9QVQk601b4seu2UjkVtLW94BlZODZ-eh91ufY2ezTagRAHq6XR3Nc-xvrNgiA/s1600/adapt_to_comics.png" data-original-width="210" data-original-height="135" /></a></div> <div style="text-align:center;">値が空オブジェクトになっているぞ!!</div> 上のコメントで書いてある部分が悪さをしているコードになる。 ## ポイント渡し風にするときは再帰関数を使う JSではオブジェクトは参照渡しになっているが、上のコードを書いたときC言語で言うポインタ渡しになっていることを期待していた。 が、オブジェクトに他の値を代入すると参照関係が切れてしまって、`obj`とは全く関係のない値になってしまっていた。 このような場合は再帰関数を使うと期待した通りに動作してくれる。 少し読みづらくなっているが、predがその再帰関数となる。 ```js //修正後のsetProperty関数 function setProperty(obj, path, value) { const pred = (prop, pathList, value) => { const p = pathList[0] if (pathList.length === 1) { prop[p] = value } else { if (!prop[p]) { prop[p] = {} } pathList.shift() pred(prop[p], pathList, value) } } pred(obj, path.split('/'), value) } ``` pred関数内で行っている処理の内容は修正前のsetProperty関数と変わっていないが、再帰関数にしたことによってコードはちょっと見た目が変わっている。 が読みづらいし、Blogなので見栄をはってif文を消し関数型言語っぽくしてみるとこうなる。 ```js function setProperty(obj, path, value) { const pred = (prop, pathList, value) => { const p = pathList[0] pathList.length === 1 ? prop[p] = value : pred(prop[p] ? prop[p] : prop[p] = {}, pathList.slice(1), value) } pred(obj, path.split('/'), value) } ``` こっちのほうが見やすいかどうかは別としてすっきりとした。<strike>分岐はしているのは許して</strike> ## 終わりに ```import {Map, View} from 'ol'```でいいことには作ってから気がついた。 動的インポートだから差別化はできているからよし。多分。
(画像は以下のものを使用させていただきました。)
新しい投稿
前の投稿
ホーム
登録:
投稿 (Atom)