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に通す設定を行う:

f:id:uraway:20181128231949p:plain

で、最後に 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とiPhoneSafariでオフライン動作することを確認している

f:id:uraway:20181118233337p:plain

ソースコード

もちろんCRA製

GitHub - uraway/dictionary-app

lighthouseはこんな感じ。フィリピンというかセブではそもそも4Gですらないが… f:id:uraway:20181118224335p:plain

storybookで@babel7を使う

参考:

github.com

環境:

"@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をハイライトしたいならば、javascriptmarkup、そして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 = /^-&gt;\|(.*)\|\n/
                const lang = pre.innerHTML.match(langRegExp);
                if (!lang) return;
                const content = pre.innerHTML
                    .replace(langRegExp, '')
                    .replace(/</g, '&lt;')
                    .replace(/>/g, '&gt;')
                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

そのまま使うならなんにも考えなくていいんだけど、withStylesclasses をインジェクションしたい場合、ちょっとトリッキーな使い方になる。

createThemeは型コンパイルを行うだけで、ランタイム時には何もしない関数。WithStylesclasses に型を提供する。

->|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.confirmESLint でエラーが出るし、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 コンポーネントを描画。resolvecleanup を渡す。 Promiseresolve / 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
}

confirm

HOC

で、これを HOC 化したのがこちら

github.com

// 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の代用) とはちょっと違った。