S3 + CloudFront の CORS 設定
手順をメモっとく
S3 の設定
[Permissions] > [CORS configuration] から以下のように設定を追加:
<CORSConfiguration> <CORSRule> <AllowedOrigin>*</AllowedOrigin> <AllowedMethod>GET</AllowedMethod> </CORSRule> </CORSConfiguration>
CloudFront の設定
[Behavior] > [Cache Based on Selected Request Headers] から Origin ヘッダだけをS3に通す設定を行う:
で、最後に curl で確認して、access-control-allow-origin: *
が返ってきていればよし
$ curl -X GET -I -H "Origin: https://example.com" https://example.cloundfront.net/path/to/image.png HTTP/2 200 content-type: image/png content-length: 43881 date: Wed, 28 Nov 2018 14:24:17 GMT access-control-allow-origin: * access-control-allow-methods: GET last-modified: Mon, 30 Oct 2017 07:22:28 GMT etag: "" accept-ranges: bytes server: AmazonS3 vary: Origin x-cache: Miss from cloudfront
PWAでオフラインでも使える辞書アプリ作ってみた
いつも英語学習にはアルクの英辞郎の辞書アプリ使ってたんだけど、フィリピンのネットスピードでは使いづらいものがあって、オフラインでも使える辞書アプリ探すかなーと思っていたところ
Chrome拡張の高速な英語辞書ツールをつくりました(Mouse Dictionary) - Qiita
という記事の中で、英辞郎の辞書データを購入できることを知ったので、どうせならとPWAで辞書アプリを自作してみた。
https://uraway.github.io/dictionary-app/
Chrome CanaryとiPhoneのSafariでオフライン動作することを確認している
ソースコード
もちろんCRA製
GitHub - uraway/dictionary-app
lighthouseはこんな感じ。フィリピンというかセブではそもそも4Gですらないが…
storybookで@babel7を使う
参考:
環境:
"@babel/core": "^7.1.2", "@storybook/react": "^3.4.11", "babel-loader": "^8.0.4",
storybookで@babel7を使うために、storybookのwebpack設定を少し上書きする。
.babelrc.js
(@babel7に対応したBabel設定ファイル)
module.exports = { presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"], plugins: ["@babel/plugin-proposal-class-properties"] }
.storybook/webpack.config.js
const babelConfig = require("../.babelrc") module.exports = (baseConfig, env, defaultConfig) => { defaultConfig.module.rules[0].loader = require.resolve('babel-loader'); defaultConfig.module.rules[0].query.presets = babelConfig.presets; defaultConfig.module.rules[0].query.plugins = babelConfig.plugins; return defaultConfig; };
はてなブログのシンタックスハイライトを jsx に対応させる
やたら重いので消した (2018/10/18)
はてなブログのシンタックスハイライトを jsx に対応させる
はてなブログのシンタックスハイライトは、2018/08 現在jsx
には対応していない。
http://help.hatenablog.com/entry/markup/syntaxhighlight
なので、PrismJSを使って対応させてみる。
PrismJS を読み込む
<head>
内に以下を追加。好みのテーマとライブラリを読み込む。CDN で読み込む場合は、ハイライトしたい言語の依存関係にあるものをすべて含める必要がある。依存関係はこちらで確認できる。jsx
をハイライトしたいならば、javascript
とmarkup
、そしてjsx
が必要なので、そのとおりに読み込む。
->|html| <script type="text/javascript"> document.addEventListener('DOMContentLoaded', () => { const cssSources = [ "npm/prismjs@1.15.0/themes/prism-dark.min.css" ]; const link = document.createElement('link'); link.rel = "stylesheet"; link.href = `https://cdn.jsdelivr.net/combine/${cssSources.join(',')}`; document.head.appendChild(link); const jsSources = [ "npm/prismjs@1.15.0", "npm/prismjs@1.15.0/components/prism-javascript.min.js", "npm/prismjs@1.15.0/components/prism-jsx.min.js", "npm/prismjs@1.15.0/components/prism-markup.min.js" ]; }); </script>
PrismJS では、パースされたトークン(構文の最小構成単位)にkeyword
クラスが与えられることがあるが、はてなブログのデフォルトの CSS が!important
でそれに対して適用されてしまうので、打ち消す必要がある:
->|html| <style> .token.atrule, .token.attr-value, .token.keyword { color: #d1939e !important; /* テーマによって値は異なる */ } </style>
ハイライトさせる
PrismJS は、次のようなcode
ブロックの中身をすべてパースし、クラス名に沿ってハイライトしてくれる。
->|html| <pre><code class="language-css">p { color: red }</code></pre>
が、マークダウン記法に慣れているといちいちタグで囲むのも、クラス名を割り当てるのも少々めんどくさいので、次のスクリプトを追加する。ついでに<
と>
もエスケープする:
->|js| document.addEventListener('DOMContentLoaded', () => { /* 略 */ var pres = document.querySelectorAll('pre'); pres.forEach((pre) => { const langRegExp = /^->\|(.*)\|\n/ const lang = pre.innerHTML.match(langRegExp); if (!lang) return; const content = pre.innerHTML .replace(langRegExp, '') .replace(/</g, '<') .replace(/>/g, '>') pre.innerHTML = `<code class="language-${lang[1]}">${content}</code>`; pre.setAttribute('class', `language-${lang[1]}`); }); });
これによって、次のようにブロックのはじめに->|jsx|
を書くと:
->|none| ->|jsx| class Todo component Component { render () { return <div /> } }
PrismJS がパース可能な HTML が作り出されるようになる:
->|none| <pre class="language-jsx"> <code class="language-jsx"> class Todo component Component { render () { return <div /> } } </code> </pre>
プラグイン
PrismJS には、プラグインがいくつか用意されており、拡張が可能。ただ、CSS がはてなのデフォルト CSS とバッティングする可能性があるので慎重に。
Line Numbers
行番号を追加するプラグインを入れてみる。 まずは、プラグイン用のスクリプトと、CSS を追加する:
->|js| document.addEventListener('DOMContentLoaded', () => { const cssSources = [ /* 略 */ "npm/prismjs@1.15.0/plugins/line-numbers/prism-line-numbers.min.css" ]; const link = document.createElement('link'); link.rel = "stylesheet"; link.href = `https://cdn.jsdelivr.net/combine/${cssSources.join(',')}`; document.head.appendChild(link); const jsSources = [ /* 略 */ "npm/prismjs@1.15.0/plugins/line-numbers/prism-line-numbers.min.js", ]; const script = document.createElement('script'); script.src = `https://cdn.jsdelivr.net/combine/${jsSources.join(',')}` document.body.appendChild(script); /* 略 */ });
pre
タグのクラスにline-numbers
を加える:
->|js| document.addEventListener('DOMContentLoaded', () => { /* 略 */ pres.forEach((pre) => { /* 略 */ pre.setAttribute('class', `line-numbers language-${lang[1]}`); }); });
これだけだと、プラグインが追加した行番号のフォントサイズとはてなのデフォルトのフォントサイズが違うためか、ずれてしまうことがあるので、行番号の方を修正する:
->|html| <style> /* 略 */ code[class*=language-], pre[class*=language-] { font-size: inherit; } </style>
Copy to Clipboard
ツールバーにコピーボタンを加える。 まずは同じように、JS と CSS ファイルを読み込む:
->|js| document.addEventListener('DOMContentLoaded', () => { const cssSources = [ /* 略 */ "npm/prismjs@1.15.0/plugins/toolbar/prism-toolbar.min.css" ]; const link = document.createElement('link'); link.rel = "stylesheet"; link.href = `https://cdn.jsdelivr.net/combine/${cssSources.join(',')}`; document.head.appendChild(link); const jsSources = [ /* 略 */ "npm/prismjs@1.15.0/plugins/toolbar/prism-toolbar.min.js", "npm/prismjs@1.15.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js" ]; const script = document.createElement('script'); script.src = `https://cdn.jsdelivr.net/combine/${jsSources.join(',')}` document.body.appendChild(script); /* 略 */ });
これで、ツールバーにコピーボタンが追加される。
しかし、デザインが好みじゃないので、カスタマイズしてみる。PrismJS の読み込み後に ClipboardJS を読み込み、そのコールバック内でコピーボタンを追加:
->|js| document.addEventListener('DOMContentLoaded', () => { /* 略 */ const jsSources = [ /* 略 */ "npm/prismjs@1.15.0/plugins/toolbar/prism-toolbar.min.js" ]; const script = document.createElement('script'); script.src = `https://cdn.jsdelivr.net/combine/${jsSources.join(',')}` script.onload = () => { const clipboardJSSrc = document.createElement("script"); clipboardJSSrc.src = "https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js"; clipboardJSSrc.onload = () => { addCopyBotton() } document.querySelector("head").appendChild(clipboardJSSrc); } document.head.appendChild(script); /* 略 */ }) function addCopyBotton() { Prism.plugins.toolbar.registerButton('hello-world', (env) => { var linkCopy = document.createElement('a'); linkCopy.innerHTML = '<i class="copy-icon material-icons">file_copy</i>' registerClipboard(); return linkCopy; function registerClipboard() { var clip = new ClipboardJS(linkCopy, { 'text': function () { return env.code; } }); } }); }
linkCopy
のアイコンはお好みのものをどうぞ。
正直カスタマイズがかなりめんどくさかったので、次なんかやるとしたらブログ移行してからかな…
TypeScript と Material-UI withStyles
TypeScript と Material-UI withStyles
ググって最初に出てきたのを試したけどだめだったが、material-ui のリポジトリ見てたら TypeScript のサンプルがあったので、これを見ながら使ってみる。
インストール
まずは material-ui をインストール:
->|bash| yarn add @material-ui/core
バージョンはこんなかんじ:
->|json| "dependencies": { "@material-ui/core": "^1.5.1", }, "devDependencies": { "typescript": "^3.0.1" },
TypeScript v2.7 くらいでやったときはコンパイルエラー起きたので注意。
withStyles
そのまま使うならなんにも考えなくていいんだけど、withStyles
で classes
をインジェクションしたい場合、ちょっとトリッキーな使い方になる。
createTheme
は型コンパイルを行うだけで、ランタイム時には何もしない関数。WithStyles
で classes
に型を提供する。
->|tsx| import Button from "@material-ui/core/Button"; import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles"; import { Theme } from "@material-ui/core/styles/createMuiTheme"; import * as React from "react"; interface OwnProps { label: string; } const styles = (theme: Theme) => { return createStyles({ button: { margin: theme.spacing.unit } }); }; type MyButtonProps = OwnProps & WithStyles<typeof styles>; const MyButton: React.SFC<MyButtonProps> = ({ label, classes }) => { return ( <Button color="primary" className={classes.button}> {label} </Button> ); }; export default withStyles(styles)(MyButton);
React v16.3 の Context と Fragment
よく使いそうな Context と Fragment についてメモ。
Context
そもそも、 Context はあらゆる階層のコンポーネント間で、データを共有する機能を持ちます。
しかし、v16.3 以前の React における Context には以下の注意書きがありました。
If you want your application to be stable, don’t use context. It is an experimental API and it is likely to break in future releases of React.
アプリを安定させたいなら使わないでね。実験的 な API なので、将来的に使えなくなるかもよ。
この注意書きは v16.3 で Context が変更されると共に削除されています。v16.3 で刷新された Context の使い方を見ていきましょう:
<Family />
を飛ばして、 <Person />
に値が渡されていることがわかります。
import React, { Component } from "react"; import { render } from "react-dom"; const Context = React.createContext(); function Family() { return <Person />; } function Person() { return ( <Context.Consumer> {({ name, age }) => ( <React.Fragment> <p>Name: {name}</p> <p>Age: {age}</p> </React.Fragment> )} </Context.Consumer> ); } class App extends Component { state = { name: "Mark", age: 12 }; render() { return ( <Context.Provider value={{ name: this.state.name, age: this.state.age }} > <Family /> </Context.Provider> ); } } render(<App />, document.getElementById("root"));
新しい Context は次の 3 つの API から構成されます:
createContext()
const { Provider, Consumer } = React.createContext(defaultValue);
Consumer と Provider を返す関数で、任意のデフォルト値を引数に取ります。
Provider
<Provider value={/* some value */}>
高レベル階層で使われ、value プロパティを受け取ります。
Consumer
<Consumer> {value => /* render something based on the context value */} </Consumer>
Provider 以下の階層で使われ、value を受け取り、JSX を返す関数を受け取ります。 Provider の value が変更された場合、このコンポーネントは再描画されます。
Fragment
上記の例で使用されている Fragment は v16 から追加されたビルトインコンポーネントで、リストを返すときにすごく便利です。
例えばテーブルを作りたい場合:
class App extends Component { render() { return ( <table> <tbody> <tr> <Columns /> </tr> </tbody> </table> ); } }
この Columns コンポーネントが複数の td 要素を返すとき、次の div のようなラッパーを用意する必要がありました:
function Columns(props) { return ( <div> <td>Hello</td> <td>World</td> </div> ); }
これでは警告が出てしまいます。実際のアウトプットは次のようになるためです:
class App extends Component { render() { return ( <table> <tbody> <tr> <div> <td>Hello</td> <td>World</td> </div> </tr> </tbody> </table> ); } } // Warning: validateDOMNesting(...): <div> cannot appear as a child of <tr>.
個人的には次のように配列を返すようにしてたりしたんですが、正しい作法なのかどうかわからないし、読みにくい:
function Columns() { return [<td>Hello</td>, <td>World</td>]; }
v16 以後は次のように書くことが出来ます:
function Columns(props) { return ( <React.Fragment> <td>Hello</td> <td>World</td> </React.Fragment> ); }
便利ですね。
React Component で作る window.confirm 代替品
window.confirm
はESLint でエラーが出るし、UI が良くない。なので似たような API で扱うことの出来る window.confirm
の代用品を作ってみた。
参考: http://reactkungfu.com/2015/08/beautiful-confirm-window-with-react/
環境: React (16.1.1), React-Bootstrap (0.32.1)
Confirmation Modal
まずはスタイリッシュな Modal Window を作成。Modal が作れるなら別に React-Bootstrap じゃなくても良い。
class ConfirmationModal extends Component { constructor() { super(); this.state = { show: true, }; } abort = () => { this.setState({ show: false }); }; confirm = () => { this.setState({ show: false }); }; render() { const { show } = this.state; const { body, title } = this.props; return ( <div className="static-modal"> <Modal show={show} onHide={this.abort} backdrop> <Modal.Header> <Modal.Title>{title}</Modal.Title> </Modal.Header> <Modal.Body>{body}</Modal.Body> <Modal.Footer> <Button onClick={this.abort}>Cancel</Button> <Button className="button-l" bsStyle="primary" onClick={this.confirm} > Confirm </Button> </Modal.Footer> </Modal> </div> ); } }
confirm
関数で div
を作成してそこに ConfirmationModal
コンポーネントを描画。resolve
と cleanup
を渡す。 Promise
の resolve
/ reject
時に cleanup
関数で DOM
を掃除しないとモーダル作成時に無限に div
が増えていくことになる。
const confirm = ({ body, title }) => { const wrapper = document.body.appendChild(document.createElement('div')); const cleanup = () => { ReactDOM.unmountComponentAtNode(wrapper); return setTimeout(() => wrapper.remove()); }; const promise = new Promise((resolve, reject) => { try { ReactDOM.render( <ConfirmationModal cleanup={cleanup} resolve={resolve} title={title} body={body} />, wrapper ); } catch (e) { cleanup(); reject(e); throw e; } }); return promise; };
ConfirmationModal
コンポーネントの abort
/ confirm
メソッドで真偽値を解決する。
class ConfirmationModal extends Component { constructor() { super(); this.state = { show: true, }; } abort = () => { const { resolve, cleanup } = this.props; this.setState({ show: false }, () => { resolve(false); cleanup(); }); }; confirm = () => { const { resolve, cleanup } = this.props; this.setState({ show: false }, () => { resolve(true); cleanup(); }); }; ... }
使い方
ConfirmationModal
の応答結果 (真偽値) を resolve
しているので、async
/ await
で次のように使うことが出来る:
async function() { const isConfirmed = await confirm({ title: 'CAUTION', body: 'Are you sure?', }); console.log(isConfirmed); // boolean }
HOC
で、これを HOC 化したのがこちら。
// confim.js import { createConfirm } from 'react-confirm-decorator'; import ConfirmationModal from './ConfirmationModal'; const confirm = props => createConfirm(ConfirmationModal, props); export default confirm;
好きな Modal のライブラリを使うことが出来る:
// ConfirmationModal.js import React from 'react'; import Modal from 'react-bootstrap/lib/Modal'; import Button from 'react-bootstrap/lib/Button'; import { setConfirm } from 'react-confirm-decorator'; const ConfirmationModal = ({ show, confirm, abort, title, body }) => ( <div className="static-modal"> <Modal show={show} onHide={abort} backdrop> <Modal.Header> <Modal.Title>{title}</Modal.Title> </Modal.Header> <Modal.Body>{body}</Modal.Body> <Modal.Footer> <Button onClick={abort}>Cancel</Button> <Button className="button-l" bsStyle="primary" onClick={confirm}> Confirm </Button> </Modal.Footer> </Modal> </div> ); export default setConfirm(ConfirmationModal);
import confirm from './confirm'; async function() { const isConfirmed = await confirm({ title: 'CAUTION', body: 'Are you sure?', }); console.log(isConfirmed); // boolean }
react-confirm という似たふるまいのライブラリを途中見つけたけど、やりたかったこと (window.confirm
の代用) とはちょっと違った。