@ka2n

Technology and beer

ReactとRuby on Railsを連携させる時によくやる方法

既存のRails製プロダクトのフロントエンドを徐々にReactベースに置き換えていったり、一部のUIコンポーネントだけReactで動かしたりする際に使っているヘルパーをメモしておきます。

Rails側にView用のヘルパーを設置する。

# app/helpers/react_helper.rb
module ReactHelper
  # Reactコンポーネントを表示します
  # props: Propsに渡すJSONエンコード可能なデータ
  def render_component(component, props = {})
    raw render(partial: 'helpers/react/component', locals: {
                 component: component,
                 props: props
               })
  end
end
/ app/views/helpers/react/_component.html.slim
div.react-component[ 
    data-component=(component)
    data-props=(props.to_json)
    ]

次に、webpackerのエントリーポイント配下で以下のスクリプトを読み込ませる。

// app/javascript/react-bridge.tsx

import React from "react"
import ReactDOM from "react-dom"


const selector = ".react-component"

/** RailsのviewからReactComponentを探して描画 */
const mountComponents = () => {
  document.querySelectorAll<HTMLDivElement>(selector).forEach(async host => {
    try {
      const componentName = host.dataset["component"]
      const Component: React.ComponentType<any> = _componentStore.get(
        componentName
      )
      assert(
        Component,
        `bridge.react: component not defined with name: ${componentName}`
      )
      if (!Component) return

      let props: object | undefined = undefined
      try {
        const rawProps = host.dataset["props"]
        props = rawProps && JSON.parse(rawProps)
      } catch (e) {}

      let options: MountOption | undefined = undefined
      try {
        const rawOptions = host.dataset["options"]
        options = rawOptions && JSON.parse(rawOptions)
      } catch (e) {}

      render(
        <ComponentWrapper>
          <Component {...props} />
        </ComponentWrapper>,
        host
      )
    } catch (e) {
      assert(false, e)
    }
  })
}

/** ページ遷移時に古いコンポーネントをGC */
const cleanupComponents = () => {
  const nodes = document.querySelector(selector)
  nodes && ReactDOM.unmountComponentAtNode(nodes)
}

const ComponentWrapper: React.FC<{ noTheme?: boolean }> = props => {
  // コンテキスト等の挿入はここで
  return <>{props.children}</>
}

/** コンポーネントの読み込み機構を初期化して監視を開始 */
export const start = () => {
  document.addEventListener("turbolinks:load", mountComponents)
  document.addEventListener("turbolinks:visit", cleanupComponents)
}

/** コンポーネントをローダーに追加 */
export const registerComponent = (component: any, name?: string) => {
  name = name || component?.displayName || component?.constructor?.name
  _componentStore.set(name, component)
}

function assert(value: any, message?: string | Error): asserts value {
  if (!value) {
    if (process.env.NODE_ENV === "production") {
      if (message) {
        console.log(message)
      }
    } else {
      throw message || "assertion error"
    }
  }
}

const _componentStore = new Map<string, any>()

次にapplication.jsに以下を挿入

// app/javascript/packs/application.ts
require("../react-bridge").start()

コンポーネントをViewから呼び出せるように登録する。(application.js等で実行)

const SomeComponent = () => (<div>Hello</div>)

registerComponent(SomeComponent, 'SomeComponent');

RailsのViewから以下の様に呼び出す。

= render_component 'SomeComponent', { foo: 'bar' }

まとめ

screen.so: Linux client (i3wm), macOS host時に右のWindowsキーを⌘として使用する

状況が特殊すぎて自分以外に参考にならないと思うので簡単なメモ

i3にて$mod Mod4して普段はWindowsキーを使って画面移動等を行なっている。 そのままだとscreen.soでホスト側を操作する場合にコピペ等ができない。

追記: 2020-04-20

xmodmapだとキーボードを再接続した時や、スリープからの復帰時に内容がリセットされてしまうためxkbで設定できないか模索中。

setxkbmap -option "lv3:lwin_switch"

これを設定すると、左の⌘がISO_Level3_Shiftに変換される。これはmod5にあたるので、i3では$mod Mod5することで良い感じに運用できそう。(他のPCと共有している設定なので他のPCでも設定しておかないと。。) xorg.confから設定できるので、永続化も簡単。

過去の対処法

mod3が空いているのでxmodmapでとりあえず割り当ててしまう。

remove mod4 = Super_R
add mod3 = Super_R

ちょうど自分の環境ではmod3を使っていないし、右のWindowsキーを殆んど使っていないので良い機会だしこういった設定にした。

GitHubの通知をまとめて開くブラウザ拡張機能を作った

https://github.com/notifications に出てくる通知をまとめてタブで開いて消化していくのが日課なんだけど、それを効率化できるブラウザ拡張機能を作った。

通知ページの右上にOpen Allと書いたボタンを追加します。一応新旧UIで動くようにしてある。もうすぐ新UIがデフォルトになって旧UIは無くなる雰囲気が出ている。

Image from Gyazo

また、新UIではチェックボックスがあるので、チェックをした通知のみまとめて開くボタンも追加した。

Image from Gyazo

インストールは下記からです。

chrome.google.com addons.mozilla.org

ソースコードこちら

CloudFlare Workersを使って無料プランでも無理矢理キャッシュの出し分けを実現する

originのVaryヘッダーを元にキャッシュを制御するべきだけど、あらかじめキャッシュを分けたい条件が判別できるなら、以下の方法でURLを書き換えてしまえばOK.

import { createHash } from 'crypto'

export async function handleRequest(request: Request): Promise<Response> {
  const vary = request.headers.get('accept-language') || ''
  const varyHash = createHash('md5')
    .update(vary)
    .digest('hex')
  
  let url = new URL(request.url)
  url.searchParams.append('_vary', varyHash)
  request = new Request(url.toString(), request as RequestInit)

  return fetch(request, {
    cf: {
      cacheEverything: true,
    }
  })
}

まさに貧者のCDN