20200526のReactに関する記事は7件です。

Reactで右クリックした場所にメニューを表示してみたい。

Reactで右クリックメニュー

先日書いたReact+Railsで付箋っぽいアプリを作る中で、右クリックメニューを作ってみたいなぁ、と、調べた内容と実装方法を書いていきたいと思います。

oncontextmenuのイベントハンドラを作れば良い

JavaScriptのドキュメントによれば、oncontextmenuイベントをハンドルすることで、独自のメニューが作れそうです。

実装方針

ここでは、以下の方針で実装してみることにしました。

  1. 右クリックした場所にメニューを表示する。
  2. メニューの要素は、Reactのコンポーネントとして作ってみる。
  3. 環境は、React+Railsで付箋っぽいアプリを作るの環境をそのまま使いましたが、多分Reactの動く環境なら、どこでも大丈夫。

実装だー

作るのは、以下の3つです。
1. メニューを表示するコンポーネント(menu.js)
1. 右クリックを受け付ける親コンポーネント(parent.js)
1. スタイルシート(parent.css)

メニューを表示するコンポーネント

適当なメニューと"close"が選べるポップアップを表示させます。

menu.js
import React from 'react';
import PropTypes from 'prop-types';

class Menu extends React.Component {
  // コンストラクタ
  constructor(props){
    super(props);        // おまじないですね。

    // メニューを表示するdiv要素を参照するための変数です。
    this.menuElm = null;

    // イベントハンドラのバインド
    this.onCloseButtonClick = this.onCloseButtonClick.bind(this);
    this.onMenuItemClick = this.onMenuItemClick.bind(this);
    this.onKeyUp = this.onKeyUp.bind(this);

    // メニューに表示するアイテムの配列です。(適当すぎてやばい。)
    this.messages = ["You", "are", "incredible"];

  }

  // メニュー要素を表示(visibility="visible")します。
  // 親要素から、呼び出されるメソッドです。
  show(clientX, clientY){
    // 以下のようにstyle.top,style.leftを指定することで、好きな場所にメニューを表示できます。
    this.menuElm.style.top = clientY + "px";
    this.menuElm.style.left = clientX + "px";
    this.menuElm.style.visibility = "visible";

    // 表示したらフォーカスを割り当てます。
    // (これで、keyイベントを受け付けてくれるようになります。)
    this.menuElm.focus();
  }

  // メニューを閉じ(visibility="hidden"に変更し)ます。
  close(){
    this.menuElm.style.visibility = "hidden";
  }

  // "close"クリック時のイベントハンドラ
  onCloseButtonClick() {
    // close()を呼び出します。
    this.close();
  }

  // メニューアイテムクリック時のイベントハンドラ
  onMenuItemClick(event) {
    // 自分自身を閉じて
    this.close();

    // 親要素から渡されたコールバック関数を呼び出します。
    // 引数には、アイテムに表示されているテキストを渡します。
    this.props.onMenuItemClick(event.target.innerHTML);
  }

  // エスケープキーで閉じるためのイベントハンドラです。
  onKeyUp(event) {
    event.preventDefault();
    // 文字列で比較できるとは思いませんでした。。
    if ("Escape" == event.key) {
      this.close();
    }
  }

  // レンダラー
  render(){
    return (
      <React.Fragment>
        { /* refで要素を参照することで、styleの変更ができるようになります。 */ }
        <div className="MenuBox" ref={(node) => this.menuElm = node} onKeyUp={this.onKeyUp} tabIndex="0" >
          {
            this.messages.map((message) => 
              <div className="MenuItem" onClick={this.onMenuItemClick} key={ message }>{ message }</div> 
            )
          }
          <div className="MenuItem" onClick={this.onCloseButtonClick}>Close</div>
        </div>
      </React.Fragment>
    );
  }
}

Menu.propTypes = {
  onMenuItemClick: PropTypes.func
};

export default Menu;

右クリックを受け付ける親コンポーネント

自身の領域が右クリックされたら、メニューを表示させます。
さらに、メニューで選択された内容に応じて、自身の表示内容を変化させてみました。

parent.js
import React from 'react';
import Menu from './menu';

class ParentPage extends React.Component {
  // コンストラクタ
  constructor(props){
    super(props);

    // stateを初期化
    this.state = {message: "Please Click Anywhere you like."}

    // メニュー要素への参照を初期化(後ほどレンダラーの中でrefを割り当てます。)
    this.menu = null;

    // イベントハンドラのバインド
    this.onContextMenu = this.onContextMenu.bind(this);
    this.onMenuItemClick = this.onMenuItemClick.bind(this);
  }

  // 右クリックイベントハンドラ
  onContextMenu(event) {
    // preventDefault()を忘れると、普通の右クリックメニューが表示されますよ。
    event.preventDefault();

    // メニュー要素の"show()"メソッドを呼び出します。
    // 引数にはマウスポインタの位置情報を渡してあげます。
    this.menu.show(event.clientX, event.clientY);
  }

  // 右クリックメニューでメニューが選択された際にコールバックしてもらうメソッドです。
  // 選択されたメニューの内容(innnerHTML)をstateに設定しています。
  // (これにより、画面左上のメッセージが切り替わるはず。)
  onMenuItemClick(message) {
    this.setState({message: message});
  }

  // レンダラー
  render(){
    return (
      <React.Fragment>
        { /* 自身の右クリックイベントハンドラをonContextMenu=で指定 */ }
        <div className="ParentBox" onContextMenu={this.onContextMenu} >
         { this.state.message }
         { /* コンポーネントもrefで参照できるので、子要素のメソッドを呼び出すことが可能になります。 */ }
         <Menu onMenuItemClick={this.onMenuItemClick} ref={(node) => this.menu = node} />
        </div>
      </React.Fragment>
    );
  }
}

スタイルシート

ブラウザのUIは、結局のところスタイルシートで「それっぽく見せている」だけなんですね。
何かの本にこんなことが書いてありました。
「コンピュータ使って実現しているものっていうのは、『そのように見える』だけのハリボテに過ぎない」
まさしくその通りだなぁ、と、改めて思うのでした。

parent.scss
// 親要素のスタイル
div.ParentBox {
  font-weight: bold;
  position: relative;
  width: 100vw;
  height: 100vh;
  background-color: #FFFFFF;
  border: 1px solid #000000;
}

// メニュー要素のスタイル
// position: absoluteにしないと、指定した場所に表示できないのでご注意を。
div.MenuBox {
  position: absolute;
  margin: 0px;
  padding: 5px;
  font-size: 10px;
  font-weight: thin;
  width: 100px;
  background-color: #888888;
  color: #000000;
  visibility: hidden;
  border-radius: 5px;
}

// focusをあてたときに周りが光らないようにしました。(気分の問題)
div.MenuBox:focus {
  outline: none;
}

// 各メニューにマウスポインタが乗っかった時のスタイルです。
div.MenuItem:hover {
  cursor: pointer;
  background-color: #E0E0FF;
}

実験

殺風景ですが、クリックしたところにメニューが表示される感じが実現できたはずです。
メニューを選択すると、メニューに表示されていた文字列が、そのまま親ページの右肩に表示されると思います。
Escapeボタンでも、"Close"のリンクでもメニューを消すことができます。
スクリーンショット 2020-05-26 20.44.32.png

補足

今回の内容については、DBが関係ないので、Railsの環境は正直不要で、Reactの環境だけあれば実験できます。
npmとwebpackだけで動作させた際のwebpack.config.jsの内容と、ちょっとしたソースの修正内容を記載しておきます。

ディレクトリ構成

特段特別なものはありません。
publicにindex.htmlを置いて、src配下にスクリプトとスタイルシートを置きました。

shell
app
├── node_modules(配下は割愛)
├── package-lock.json
├── package.json
├── public
│   └── index.html
├── src
│   ├── menu.js
│   ├── parent.js
│   └── parent.scss
└── webpack.config.js

webpack.config.js

app/webpack.config.js
module.exports = {
  mode: "development",
  entry: {
    app: "./src/parent.js"
  },
  output: {
    path: __dirname + '/public/js',
    filename: "[name].js"
  },
  devServer: {
    contentBase: __dirname + '/public',
    port: 8080,
    publicPath: '/js/'
  },
  devtool: "#inline-source-map",
  module: {
    rules: [{
      test: /\.js$/,
      enforce: "pre",
      exclude: /node_modules/,
      loader: "eslint-loader"
    },{
      test: /\.css$/,
      loader: ["style-loader","css-loader"]
    },{
      test: /\.scss$/,
      loader: ["style-loader","css-loader","sass-loader"]
    },{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader'
    }]
  }
};

public/index.html

ブラウザからアクセスするためのトップページです。

public/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1,shrink-to-fit=no" />
    <meta http-equiv="X-UA-Compatible" content="IE=Edge, chrome=1" />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/javascript" src="js/app.js" charset="utf-8"></script>
  </body>
</html>

src/parent.js

最後に、メインエントリポイントになるparent.jsの修正部分を書いておきます。
明示的にimportしないとスタイルが読み込まれなかったので、その対処と、
ReactDom.renderの呼び出しを追加しました。

src/parent.js
// 1,2はファイルの先頭(import文の並び)に追加
// 1. ReactDomの追加
import ReactDom from 'react-dom';

// 2. スタイルの参照を追加
import './parent.scss';

// : (中略)

// 3はファイルの最後に追加
// 3. レンダラーの呼び出し
ReactDom.render(
  <ParentPage />,
  document.getElementById('root')
);

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

create-react-appで「Unexpected end of JSON input」が発生した場合

はじめに

フロントエンドの勉強のためにReactでサンプルアプリを作ろうと思い、いきなり環境構築でつまずきました。
「react-sample」という名前のプロジェクトを作成するため、以下のコマンドを実行。
しかし、エラーが発生しインストール失敗。

create-react-app react-sample

エラー内容

以下のようなエラーが発生しました。

error An unexpected error occurred:
"/Users/(ユーザー名)/Library/Caches/Yarn/v6/
npm-saxes-3.1.11-d59d1fd332ec92ad98a2e0b2ee644702384b1c5b-integrity/
node_modules/saxes/.yarn-metadata.json: Unexpected end of JSON input".

解決策

色々調べていたら、どうやらyarnのキャッシュが関係しているとのこと。
以下のコマンドでキャッシュをクリア。

yarn cache clean

再度「create-react-app」したら無事にインストール成功しました。

Happy hacking!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

create-react-appで「Unexpected end of JSON input」が発生したとき

はじめに

フロントエンドの勉強のためにReactでサンプルアプリを作ろうと思い、いきなり環境構築でつまずきました。
「react-sample」という名前のプロジェクトを作成するため、以下のコマンドを実行。
しかし、エラーが発生し作成失敗。

create-react-app react-sample

エラー内容

以下のようなエラーが発生しました。

error An unexpected error occurred:
"/Users/(ユーザー名)/Library/Caches/Yarn/v6/
npm-saxes-3.1.11-d59d1fd332ec92ad98a2e0b2ee644702384b1c5b-integrity/
node_modules/saxes/.yarn-metadata.json: Unexpected end of JSON input".

解決策

色々調べていたら、どうやらyarnのキャッシュが関係しているとのこと。
以下のコマンドでキャッシュをクリア。

yarn cache clean

再度「create-react-app」したら無事に成功しました。

Happy hacking!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React+Next.jsで技術ブログ兼webサイトを作った

※自分がやった事、経歴等を纏めたサイトという意味でのポートフォリオ

はじめに

Ruby + JekyllによるGihubPagesは既にあるのですが、宮崎版コロナ対策サイトでVueに触れ、勉強がてら実際にJSによるサイト作成をする事にしました。

自分

大学研究でcppを利用しただけの、農学部卒。
ただいま無職、転職活動中(ここ2か月は自粛でstay home

作成に当たって

ReactとNext.jsのtutorialとdocsを一通りやりました。

サイト自体の目的

  • 経歴や作成したもののリンクをまとめる
    • GithubPagesやQiita、Gistへの投稿物を一か所にまとめる
    • Markdonwによるページ作成

つくったもの

prtsc-800.gif

まだ、ドキュメントや人のコードを参照しながら色々試している最中なので、コードが汚いです。

技術・要件など

環境

  • vm:virtualbox + vagrant
    • OS: Ubuntu18.04 bionic
  • node -v :v12.16.1
  • yarn -v :1.22.4

実作業

yarn create next-app

yarn create next-app next-portfolio
# =>
# ? Pick a template › - Use arrow-keys. Return to submit.
# ❯  Default starter app
#    Example from the Next.js repo

Example from the Next.js repoを選択したら

Default starter appの場合

今回はReact Next.jsの勉強も兼ねているので、defaultの方を利用した。

yarn devすると

image.png

作成されるディレクトリ、package.json
# directory
- public
  - favicon.ico, zeit.svg
- pages
  - index.js
- package.json
- node_modules
- README.md
- yarn.lock
// package.json
{                                                                  
  "name": "next-portfolio",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "9.3.5",
    "react": "16.13.1",
    "react-dom": "16.13.1"
  }
}

Material-UI 導入

見た目重視でmaterial-uiを導入し、主にサイドバーのpermanent / swipeable drawerとGrid
デザインの箇所に使用した。手軽に今風なものが作成できて良かったが、別のuiコンポーネントに変更した際のデザイン崩れが大きそうなので、あとで脱material-uiを図りたい。

yarn add @material-ui/core
yarn add @material-ui/icons

サイトトップ: src/pages/index.jsx の作成

  1. srcディレクトリを作成し、下にpagesを収める。
  2. src/components/Layout.jsxの作成

複数ページで共通デザインとなるLayout.jsxを作成する。ここでは省略したが、<aside />の中には、material-uiを利用したpermanent-drawerとモバイル用のswipeable-drawerを実装した。

/src/components/Layout.jsx
src/components/Layout.jsx
import React from 'react'
import Link from 'next/link'

import { makeStyles, useTheme } from '@material-ui/core/styles'
import Hidden from '@material-ui/core/Hidden'
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer'
import Drawer from '@material-ui/core/Drawer'
import DoubleArrowIcon from '@material-ui/icons/DoubleArrow'
import { List, ListItem, ListItemIcon, ListItemText, Divider } from '@material-ui/core'
import HomeIcon from '@material-ui/icons/Home'
import { MyDrawerList } from '../components/MyDrawerList'

const drawerWidth = 250
const useStyles = makeStyles((theme) => ({
  // ...
}))

export function Layout({ children }) {
  // ...
  const [state, setState] = React.useState({
    left: false,
  })

  // swipeable-drawerの開閉を制御するボタン
  const toggleDrawer = (anchor, open) => (event) => {
    if (event && event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
      return
}
    setState({ ...state, [anchor]: open })
  }

  const HomeDrawerList = () => {
    return (
      <MyDrawerList>
        <List>
          <Link href='/'>
            <ListItem button>
              <ListItemIcon><HomeIcon /></ListItemIcon>
              <ListItemText primary='Home' />
            </ListItem>
          </Link>
          // ...
        </List>
      </MyDrawerList>
    )
  }

  return (
    <React.Fragment key='left'>
      <Hidden lgUp>
        // モバイル端末用
        // if display-width > 1280px, display: none
        <SwipeableDrawer anchor='left' open={state['left']}
          onClose={toggleDrawer('left', false)} onOpen={toggleDrawer('left', true)}
        >
          <div className='swipeableList' role='presentation'
            onClick={toggleDrawer('left', false)} onKeyDown={toggleDrawer('left', false)}
          >
            <HomeDrawerList />
          </div>
        </SwipeableDrawer>
        <footer>
          <button onClick={toggleDrawer('left', true)}>
            <DoubleArrowIcon color='secondary' style={{ fontSize: 34 }} />
          </button>
        </footer>
      </Hidden>
      <Hidden mdDown>
        // 非モバイルディスプレイ用
        // if device-width < 1280px, display:none
        <aside>
          <Drawer className='permanentDrawer' variant='permanent' anchor='left'>
            <HomeDrawerList />
          </Drawer>
        </aside>
      </Hidden>
      <main className={classes.contents}>
        {children}
      </main>
      <style jsx>{`
        // ...
      `}</style>
    </React.Fragment>
  )
}

  1. pages/index.jsxの作成 まだReact等に不慣れなので、pages/index.jsにサイト1ページ目を作りこんで、後からcomponentに分割する方式をとった。

色々試した結果、サイトトップにあたる pages/index.jsxは下の様になった。また、<head><meta/></head>用のデータは/public/manifest.jsonから持ってくる事にした。

/src/pages/index.jsx
import React from 'react'
import Head from 'next/head'
import { Layout } from '../components/Layout'
import { Top, About, History, Works } from '../components/HomeContents'
const manifest = require('../../public/manifest.json')

export default function Home() {
  return (
    <>
      <Layout>
        <Head>
          <title>{manifest.name}</title>
          <meta name='title' content={manifest.name} />
          <meta name='description' content={manifest.description} />
          <meta property='og:title' content={manifest.name} />
          <meta property='og:description' content={manifest.description} />
          <meta property='og:image' content={`${manifest.vercel}/assets/prtsc700.jpg`} />
          <meta property='og:url' content={`${manifest.vercel}`} />
        </Head>
        <Top />
        <About />
        <History />
        <Works />
      </Layout>
      <style jsx global>{`
        // ...
      `}</style>
    </>
  )
}

/src/components/HomeContetnts.jsx`
/src/components/HomeContetnts.jsx
import React from 'react'
import Link from 'next/link'
import Grid from '@material-ui/core/Grid'

export function Top() {
  return ( <section id='top' className='topContainer' />  )
}

export function About() {
  return (
    <section id='about' className='content'>
      <h2>About</h2>
      <Grid container spacing={4}>
        <Grid item md={12} lg={5}>
          <picture>
            // ...
          </picture>
        </Grid>
        <Grid item md={12} lg={7}>
          <p>My name is Kawano Yudai.</p>
          <p>I graduated from Miyazaki Universiy as Bachelor of Agriculture.</p>
          <p>I belonged to agricultural engineering lablatory and studied crop row detecting tech by image processing with C++ and OpenCV.</p>
          <p style={{ color: '#F48FB1' }}><em>Now, I'm seeking job as developer. Please contact me from left drawer.</em></p>
        </Grid>
      </Grid>
    </section>
  );
}

export function Works() {
  return ( <section id='works' className='content' /> )
}

export function History() {
  return ( <section id='history' className='content' /> )
}

_app.jsx, _document.jsx, 404.jsxの作成

  • 参照

  • _app.jsx

    • global cssを追加する場所
  • _document.jsx

    • SSRされる箇所なので、onclickなどイベントハンドラは動かない
    • <Main />の外側にあるコンポーネントはブラウザによる初期化がされないので、Appロジック等を追加したければ、app.jsxに書いて。
    • <title><Head />styled-jsxを書いちゃ駄目。

Posts周辺の作成

ダイナミックルーティング

ディレクトリ構成

- pages (*directory)
  - index.jsx
  - posts (*directory)
    - hoge.jsx
    - [id].jsx

また、

/src/pages/posts/[id].jsx
import useRouter from 'next/route'
export default function Post(){
  const router = useRouter()
  const { id } = router.query
  return <p>Post: {id}</p>
}

ファイル名に[]が付いてるので変に見えるが。例えば

  • localhost:3000/posts/hoge/にアクセスするとpages/posts/hoge.jsxが読み込まれる。(これは普通)
  • localhost:3000/posts/foobarだと、pages/posts/foobar.jsxが読み込まれ、

image.png

dynamic routeLink( next/link )を併用する時は、hrefに合わせてasも使うなど注意点があるが、非常に便利な代物かと。

getStaticPropsgetStaticPaths

今回はmdファイルを/src/pages/docsに入れる。

  • baseUrl/postsへのアクセス時は、docs下のmdファイルを読込み、posts一覧の出力
  • baseUrl/posts/[id]の場合は、同様にして、post単体の出力
  • baseUrl/tagsの場合は、同様にpostsで使用されている投稿タグ一覧の出力
  • baseUrl/tags/[tag]なら、同タグを使用するposts一覧を出力
    • 重複tagが出るので、tag全取得 => toLowerCase() => sort() => 重複削除
  • docs配下に無いmdファイル名にアクセスした場合は、404

ページ出力がsrc/pages/docs/xxx.mdという外部データに依存した静的ページ出力をしたいので、getStaticPropsgetStaticPathsを使用した。

実装は下を参照しながらしました。タグの方は自分で用意しましたが。
Next.jsのチュートリアルのこのページ

image.png

tagsページのスタイルが未だ・・・

image.png

マークダウン

構文木について、しっかり学ばねばと思いました。

/src/lib/posts.jsx
/src/lib/posts/jsx
// /src/lib/posts.jsx
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import remark from 'remark'
import html from 'remark-html'
export async function getPostData(id) {  
  const fullPath = path.join(postsDirectory, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const matterResult = matter(fileContents)
  const LowerCaseTags = matterResult.data.tags.map((tag) => (tag.toLowerCase()))
  const highlight = require('remark-highlight.js')

  const processedContent = await remark()
    .use(highlight)
    .use(html)
    .process(matterResult.content)

  const contentHtml = processedContent.toString()

  return {
    id,
    contentHtml,
    LowerCaseTags,
    ...matterResult.data,
  }
}

<head><meta /></head>

image.png

以前にrubyとjekyllで作ったgithubpagesと比較して、syntax-highlightが粗いので改善が必要

image.png

また、front-matterは下の様にQiitaと同じにしてあって、またpost情報はgray-matterを使ってpostコンテンツ取得と同時に取得し、next/head<head><meta /><head>に格納してある。

# front-matter
---
date: '2020-05-26'
author: Kawano Yudai
title: 'Qiita: Next.jsでポートフォリオサイトを作成した'
tags: [Qiita, React, Next.js]
image: '/assets/posts/202003/miyazaki-oss1.jpg'
slide: false
---

SNSシェアボタン

snsシェアボタンも上記の<meta />と同様にした。初めて、hatenaに垢登録しました。

/src/pages/posts/[id].jsx
<button className='twitter'>
  <a href={`https://twitter.com/share?text=${postData.title}&hashtags=react,nextjs&url=https://next-portfolio-blue.now.sh/posts/${postData.id}&related=not_you_die`}
    target='_blank' rel='noopener noreferrer'><TwitterIcon /></a>
</button>
<button className='hatena'>
  <a href={`https://b.hatena.ne.jp/entry/https://next-portfolio-blue.now.sh/posts/${postData.id}`} className='hatena-bookmark-button' data-hatena-bookmark-layout='touch-counter'
    title={postData.title} target='_blank' rel='noopener noreferrer'><HatenaIcon /></a>
</button>

残る改善点、したい事など

サイト全体

  • Material-uiからの脱却
    • swipeable-drawer以外は自分で実装出来そうなので

image.png

  • CSSの統一(module.cssなのかstyled-jsxなのか等)
    • 現在は色々試すために、混在中
  • TypeScript化(触ってみたいだけ
  • AMP一部対応( 参照: Next.js next/amp
  • google analytics, PWA対応
  • カスタムドメイン
  • api routeを試す

posts, tags周辺

  • /tagsページの整備
  • コードブロックの言語またはファイル名の表示
    • Qiitaの様にしたい( 現状だとエラーになる )
  • syntax-highlightの改善
  • rssの対応

その他

働きたいでござる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

create react appで作ったアプリをIE11対応させる

動機

そろそろ世間的にIE11はフェードアウトしたかなと思っていたのですが、
2020年4月時点でまだ結構なシェアを誇っているとい非常に不都合な真実にぶち当たりました。
参考:https://webrage.jp/techblog/pc_browser_share/

如何にもモダンブラウザでしか動かなそうな機能を使っているならまだしも、
そうではない一般に公開するページで「ご利用のブラウザは非対応です。」と表示するわけにもいかないので、対応を試みました。

TL;DR;

  • ポリフィルを投入
  • 開発モードで動かない時には、package.jsonのbrowserslist.developmentをproductionと合わせる
  • それでもダメならnode_modules>.cacheを消してからyarn or npm install

ポリフィル

公式のpolyfillがあるので入れましょう。
https://github.com/facebook/create-react-app/tree/master/packages/react-app-polyfill

yarn か npmで追加した後、以下をindex.js またはindex.tsの頭につけてあげればOKです。

import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';

開発環境でのテスト

当然、投入した後に yarn start あたりを叩いて起動検証を試みるわけですが、
残念ながらこのままでは動きません。

原因はpackage.jsonのbrowserlistにあります。

create-react-appでプロジェクトを作った後、変更を加えていないと以下のような感じのはずです。

 "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }

詳しい仕様は公式を参照していただきたいところですが、
開発モード(普通にstart)の場合、Chrome,Firefox,Safariの最新版のみを想定したビルドになってしまうので、polyfillをimportしてもIE11では動きません。
polyfillの適用具合を確認したい場合は、productionの内容でdevelopmentの方を上書きして試しましょう。

それでも動かなかったら

node_modules/.cacheにキャッシュが残ってしまっている可能性が高いので、
消してnpmなりyarnなりを叩き直しましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React超入門

はじめに

本記事は初めてreactを触る人向けに書いています。
reactってなんだ?どう記述するの?という初歩的な内容をまとめています。

※ライフサイクルの説明は長くなるので割愛させていただきました。
以下の記事が秀逸なのでこちらを紹介させていただきます。
https://qiita.com/Julia0709/items/3c3fc8d29fd2e56ed7a9

Reactとは

FaceBookによって開発されたフロントエンドフレームワークで、Reactはリアクティブ・プログラミングで、値がどのように伝わっていくかを重視しています。

従来のjsとの違いとして、表示を更新するとき予め用意した値を設定しておけば元の値を変更すると表示も自動的に更新されます。

また、Reactは直接DOM操作するのではなく、プログラム的に仮のDOMを構築してそれを操作します。
これを仮想DOMといいます。この仮想DOMは通常のDOMに比べて軽くて高速です。

React開発に必要なもの

  • Node.js: JavaScriptの実行環境(必須)
  • Create React App: Node.jsに組み込まれたnpxを使ってターミナルで npx create-react-appフォルダ名 を入力して実行(スターターキットのようなもの。自分で環境構築ができる場合は不要)
  • React Developer Tools: デバッグ用ツール。Reactを使った開発をするならあった方が良い。(ブラウザの拡張ツール)

※トランスパイラ等はCreate React Appで用意されているのでここでは省略します。

JSX

Reactでは、HTMLのタグを直接JavaScriptのスクリプトに記述する仕組みを持っています。これをJSXといいます。JSXを使用することでようやくReactを使うメリットを実感できると思います。

JSXを使った記述と使わない記述の違いを見ていきます。
ブラウザに表示されているここに表示されます。Hello Worldに表示を変更させます。

<div id="root">ここに表示させます。</div>

JSXなし

import React from 'react'; //インストールしたreactを使うためにインポートしています。(今回はcreate-react-appで必要な物をまとめてインストールしているので、新しくインストールする必要はありません。)
const rootId = document.querySelector('#root');
const element = React.createElement (
    'p', {}, 'Hello World'
);
ReactDOM.render(element, rootId);

JSX

import React from 'react';
ReactDOM.render(<p>Hello World</p>,document.getElementById('root'));

JSXを使用した方が記述量が減り、可読性が上がるのが分かりますね。
JSXを使わない場合以下のReact.createElementという箇所で仮想DOMのエレメントを作成しています。

const element = React.createElement (
    'p', {}, 'Hello World'
);

React.createElementの第一引数にはタグ名、第二引数にはそのエレメントに用意される属性オブジェクト(必要ない場合はからのオブジェクトを指定)、第三引数には作成するエレメントの内部に組み込まれるものを用意します。

まとめると以下のようになります。

React.createElement(タグ名、属性、中に組み込む物);

作成した仮想DOMエレメントをReactDOMオブジェクトのrenderメソッドを使いレンダリングします。

JSXを使う場合はシンプルに
pタグで表示したいHello Worldを囲い、renderメソッドを使いレンダリングしています。

そして、ブラウザにアクセスすると差分を検知し、divタグで囲われたここに表示されます。Hello Worldに書き換わっていると思います。

デバッグツールで確認すると以下のように変更されていることが確認できます。これはJSXで記述した方でも同じように表示されるはずです。

<div id="root"><p>Hello World</p></div>

どちらの書き方が良いかは一目瞭然ですね。

JSXを使わないと表示するタグをcreateElementで1つずつ作成していかなければなりません。その煩雑さを解消するためにJSXを使うという訳です。

しかし、JSXはbabelによってトランスパイルされることで初めて成り立っています。どのようにbabelが変換しているかを頭でイメージできるかできないかでは習熟度が全然違ってきます。

コンポーネント

Reactではコンポーネント事に管理する分割統治の概念があります。
ここでは基本的なコンポーネントの書き方を書いていきます。

//index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from '.Components/app'; //Appコンポーネントがある階層を指定してインポートする。
ReactDOM.render(<App />,document.getElementById('root'));
//app.js
class App extends React.Component {
    render() {
        return <p>Hello World</p>
    }
}
export default App;//Appコンポーネントを他ファイルでインポートできるようにエクスポートする。

上記ではReact.Componentを継承したAppクラスを作って、render()メソッドでJSXをreturnしています。これによりReactDOM.render()から呼び出した際にJSXで出力されます。

このようにコンポーネント化することにより以下のメリットがあります。

  • 再利用性が高い
  • 保守性が高い
  • 管理がしやすい
  • アトミックデザインと相性が良い(パーツ・コンポーネント単位で定義していくUIのデザイン手法)

コンポーネントはスクリプト内に複数のコンポーネントを書いて、組み合わせて表示を作成することができます。

また、上記の例では敢えてファイルを複数に分ける想定で書いていますが、これはコンポーネントが複数存在する想定での書き方で、今回のようにテキストを表示させるだけの場合は1ファイルで完結させた方がシンプルで良いでしょう。

状態管理

Reactでは保管した状態を操作するために以下のようなモノが用意されています。

  • props  親コンポーネントから渡されたプロパティです。propsはimmutableなデータで、変更ができません。
  • state  コンポーネントが持っている状態です。stateはmutableなデータで変更することができます。

stateの初期値を受け取るにはthis.stateで値を取り出します。

簡単に説明すると、親コンポーネントで保持しているstateの値を子コンポーネント側でpropsで受け取ります。stateを更新することでコンポーネントの表示を変えたりしています。

以下のようにapp.jsを書き換えます。

//app.js
import React, { Component } from 'react';
class App extends Component {
    constructor(props){
        super(props);
        this.state = {
            clickCount: 0
        };
        this.handleClick = this.handleClick.bind(this);
    }
    handleClick() {
        this.setState((state) => ({
            clickCount: state.clickCount + 1
        }));
    }
    render(){
        return(
            <div>
                <button onClick={this.handleClick}>ButtonCount</button>
                <h1>{this.state.clickCount}</h1>
            </div>
        );
    }
}
export default App;

クリックしたら値が更新され数字が1つずつ増えていきます。
最初に画面に表示されている0の数字はconstructorで設定しており、ステートの値はオブジェクトリテラルとして記述します。

これでthis.stateの中から設定した値を取り出して利用することができるようになりました。
初期状態として表示される0constructorで設定した0が表示されているということです。

そしてButtonCountボタンを押下すると、<h1>タグで挟んでいる{this.state.clickCount}の値が更新されます。

今回はapp.jsで完結させていますが、clickCountの値を子コンポーネントに渡して、
子コンポーネントで値を表示するという実装だったとすると以下のようになります。

//app.js
import React, { Component } from 'react';
import Child from '.Component/child.js';
class App extends Component {
    constructor(props){
        super(props);
        this.state = {
            clickCount: 0
        };
        this.handleClick = this.handleClick.bind(this);
    }
    handleClick() {
        this.setState((state) => ({
            clickCount: state.clickCount + 1
        }));
    }
    render(){
        return(
            <div>
                <button onClick={this.handleClick}>ButtonCount</button>
                <Child clickCount={this.state.clickCount} />
            </div>
        );
    }
}
export default App;
//child.js
import React, {Component} from 'react';
class Child extends Component {
    render() {
        console.log(this.props);
        return <h1>{this.props.clickCount}</h1>
    }
}
export default Child;

親コンポーネントの<Child clickCount={this.state.clickCount} />箇所で子コンポーネントに値を渡し、子コンポーネントの{this.props.clickCount}箇所で親コンポーネントから渡された値を受け取り表示しています。

console.logで実際受け取っている値が見ることができます。

おわりに

今回のように小規模開発であれば問題ないのですが、大規模開発になるとコンポーネントも増えて、propsのバケツリレーになってしまいます。
このバケツリレーを避けるためにReduxというReactが扱う状態を管理するフレームワークなどを使うと良いでしょう。
Redux公式
Redux超入門 ←さらっと理解したい人向けに書きました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リモートワーク支援の為、2週間で無料サービスを出した話

久々の投稿になります。
EmpathというAI会社でアプリケーションサイドの開発をしています。
先日リモトーキー (R:EmoTalkie)というサービスをリリースしました。無料で使えます。
今回はR:EmoTalkieの開発背景 + 技術記事という構成で書かせていただきます。

開発背景

最近の世間の事情により、慣れている人、慣れていない人もリモートワークへ移行せざるを得ない状況になっています。
そんな中、やはり不安ストレス寂しさ、そして元気がなくなってしまう人が多くなってきているというのをよく聞くようになり、Empathで「何かしら力になりたい」ということでR:EmoTalkieを2020年4月24日にリリースしました。
スクリーンショット 2020-05-26 14.05.18.png
(実際のR:EmoTalkieの画面)

どんなもの?

R:EmoTalkieは通話アプリケーションの裏で動かして、使用者の音声を解析し、ミーティングへの参加率や活性度を可視化することで、積極的にミーティングへの参加を促すサポートツールとなっています。
※ブラウザアプリケーションとして実装していますが、音声は解析にしか使用しておらず、保存もしていません。安心して使ってください。
スクリーンショット 2020-05-26 14.05.56.png
(解析結果は数値では表さず、褒めたり励ます仕様に)

無料?

「この状況を乗り越えるための助けとなりたい」という思いはあったのですが、リソース不足、昨今の事情への知識不足から、完璧なサービスをリリースするには限界があると思っていました。
そこで、1つの提案という形で無料でリリースすることにより、我々のことを広く知ってもらい、共感を得た方々と一緒に作り上げたいという思いから無料にしています。

なぜ2週間?

今回はお試しのようなサービスでのリリースとはいえ、前述の通り「この状況を乗り越えるために何か助けとなりたい」という思いがあったため、早期に出すことが重要だと考えていました。
とはいえ、スタートアップでリソースが限られている為、作業者自体は1人で行う必要もあり、なんとか形として出せる期間が2週間でした。

開発

今回のアプリケーションではWebRTCのコードを一部書いていますが、通話機能をもたせておらず、あくまでWebRTCの音声取得部分だけを利用しています。
WebRTCをちゃんと実装したい場合は、他の人の記事ですがWebRTCハンズオン 本編を参考にしていただくと分かりやすいと思います。

前提

音響周りの開発、webRTC関連の開発は初めてです。弊社には音響エンジニアがいますが、リソース的に入ることが難しいため、私1人で分からないながらも格闘したものとなります。
間違っているところがありましたらコメント等でご指摘いただければ幸いです。

開発環境

エディター : Visual Studio Code
開発言語 : ReactJS
解析API : EmpathAPI
通信ライブラリ : axios

コード

読みやすいように、実際のコードから少々変更してポイントのみ記載しています。
また、WebRTCの解説等は別の有志の方々の記事の方が参考になるかと思いますので、詳しくはこちらでは書きません。

マイクの使用開始

今回はカメラは使用せず、マイクのみとなるので、下記のように設定します。

analyze.js
   navigator.mediaDevices
      .getUserMedia({
         audio: {
            echoCancellation: true,
            echoCancellationType: 'browser',
            noiseSuppression: true,
            sampleRate: { ideal: 11025 }
         },
         video: false
      })
      .then((stream) => {
         this.handleStartMeeting(stream);
      })
      .catch(this.handleFailed);

echoCancellationnoiseSuppressionを有効にしていますが、あまり実感できず、、こちら詳しい方がいましたらコメントしていただけると助かります...

Streamの取得

analyze.js
...
   processor.onaudioprocess = (audio) => {
      this.micAnim(); //音量によって変わるマイクアニメーション
      var input = audio.inputBuffer.getChannelData(0);
      var bufferData = new Float32Array(BUFFER);

      for (var i = 0; i < BUFFER; i++) {
         bufferData[i] = input[i];
      }
      audioData.push(bufferData);
   }
   this.recordingTimer(); //5秒区切りの音声作成用タイマー

最後の行で5秒区切りの音声作成用Functionを呼び出します。

onaudioprocessについては、Web Sounderで以下のように説明されています。

onaudioprocessイベントの発生は以下のように実装されているようです.
1. AudioDestinationNodeへの接続とイベントハンドラの設定が完了したときに発生
2. 1. のあとは, バッファサイズのデータを処理するごとに発生

5秒区切りの音声作成

EmpathAPIを使用する上で、重要になってくるのは、5秒区切りの音声であるという部分になるかと思います。
そこを下記のタイマー処理で、5秒区切りの音声ArrayをAPIにて送るということをしています。

analyze.js
   let Interval = setInterval(() => {
      var count = this.state.count - 1;
      if (count > 1) {
         this.setState({
            count: count,
         });
      } else {
         clearInterval(Interval);
         this.resetCount();
         if (this.state.speachCount > MIN_SPEECH_REC_TIME) {
            this.stopAndSend();
         }
         ....
      }
   }, 1000);

初期値5のcountを1秒起きのタイマーでカウントダウンをし、5秒経ったらstopAndSend()で一旦マイクを停止し、EmpathAPIへ音声データを投げる処理に入ります。
マイクの再スタートは作成するサービスによって変わってくるので、適宜再スタートさせてあげてください。
常時録音でもよいですが、EmpathAPIのコール数には上限があるため、1時間のミーティングで使用するとえらい数になり、すぐ上限を超えてしまうかと思います...

if (this.state.speachCount > MIN_SPEECH_REC_TIME)こちらの行では、最低発話時間を見ています。ある一定の音量以上で発話した最低時間を設定しないと、全く発話していないのにAPIを叩いてしまうことになります。また、物を落としたときなどの雑音も解析してしまうことにもなります。
今回は約1秒以上、音声が入ってきたものを発話としています。

WAV変換

EmpathAPIに音声データを送るにはこれでは不十分なので、EmpathAPIで受け取れるWAVへ変換していきます。

analyze.js
   let encodeWAV = function (samples, sampleRate) {
      ...
      let view = new DataView(buffer);
      ...
      view.setUint32(24, sampleRate, true); // サンプリングレート
      ...
      return view;
   }
   ...
   let dataview = encodeWAV(mergeBuffers(audioData), 11025);
   let audioBlob = new Blob([dataview], { type: 'audio/wav' });

こちらの書き方はいくつも扱っている記事があるので、詳しくは記載していきません。
重要なのはWAVとして扱うことと、サンプリングレートを11025とすることです。

EmpathAPI送信

こちらは前の記事でも取り扱ったのと同じく、axiosを使用していきます。

cllient.js
   const post = (wavData): Promise<*> => {
      let formData = new FormData();
      formData.append('apikey', key);
      formData.append('wav', wavData);

      return axios.post(url, formData, {
         headers: {
            'content-type': 'multipart/form-data',
         },
      }).then((res) => {
         return Promise.resolve(res);
      }).catch((response) => {
         return Promise.reject(response);
      })
   }

ポイントはapikeyとwavDataとなります。
apiKeyはEmpathAPIのAPI Key設定にあります。(※ログインが必要です。)
wavDataは前項のaudioDataを使用します。
また、忘れてはいけないのはEmpathAPIはmultipart/form-dataで受けつけているので、こちらも設定をします。
戻り値は{ "error": 0, "calm": 16, "anger": 9, "joy": 6, "sorrow": 17, "energy": 0 }のような値が返ってきます。

まとめ

ざっくりとしたポイント説明だったので、これだけでは作成は難しいかもしれません。が、大体こんな雰囲気で作られているということが伝わればと思います。
こうしたほうがいいよ、ここ間違っているよ、などがあればご連絡していただければと思います。

おわりに

今回リリースさせていただいたアプリはあくまで「Empath技術を使うとこんなことができるよ」という提示をしたに過ぎません。ここからはもっとリモートワーカー達がより過ごしやすくなるサービスとするには、たくさんのアイディアや技術者が必要となってきます。
弊社と共になにか創生したい!という方々はぜひこちらからご連絡をしていただければと思います。

参考

MDN web docs - getUserMedia
Web Sounder
WebRTCハンズオン 本編

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む