坂本研のゼミ室

研究室のお知らせ管理をホワイトボードからWebに移行したいのでβ版作ってみた

宮崎大学アドベントカレンダー2019 6日目の記事です。

qiita.com

はじめに

研究室内のホワイトボードによる情報伝達が難しくなってきたため、 研究室のメンバーが連絡事項を投稿・削除できるWeb掲示板(β版)を作成しました。

この記事では主に、ページとコンポーネントソースコードを紹介します。 その他のソースコードはこちらにあります。(他機能追加中のため修正するかもしれません🙇‍♂️)

github.com

以下の環境で動作確認しています。

$ node -v
v11.10.1

使用技術

作ったもの

f:id:TakaShinoda:20191203010811p:plain

機能

  • ID, 投稿されたメッセージ内容, 日付(月/日)をFirebase Realtime Databaseに追加
  • Firebase Realtime Databaseから新着10件のデータを取得して表示
  • 指定されたIDのメッセージを削除
  • Firebase Authenticationを用いた認証機能などはまだ実装できていません🙇‍♂️

関連記事は以下にまとめていますのでご覧いただけましたら幸いです。

環境構築

  • React
npm install --save react react-dom
  • Redux
npm install --save redux react-redux redux-thunk
  • Next.js
npm install --save next
  • Firebase
npm install --save Firebase
  • Material-UI
npm install @material-ui/core

実装

{
    "scripts": {
        "dev": "next",
        "build": "next build",
        "start": "next start",
        "export": "next export"
    }
}
  • 上の環境構築のコマンド打ってインストールする

  • next.config.jsを作成して以下のようにしてindex.jsをトップページにする。

module.exports = {
    exportPathMap: function() {
        return {
            '/': {page: '/' }
        }
    } 
}
  • pagesフォルダ(Next.jsのWebページを配置する場所)を作成する。
  • componentsフォルダ(コンポーネント関係をまとめる場所)作成する。

ページを作っていく

ここからはpagesフォルダ内での作業です。

  • index.jsを作成
import Link from 'next/link';  //リンクを表示するために<Link>タグを使用しています
import Layout from '../components/Layout';  //後述するLayoutコンポーネントを読み込んでいます。

export default () => (
    <Layout header='Whiteboard' title='Home'>
        <Link href='./contact_board'>
            <a>連絡板 &gt;&gt;</a>
        </Link>
         {/**今後他のページを増やすときは以下のようにしてください */}
        {/**<Link href='./ファイル名'>
            ここは<a>以外も使えます
        </Link>*/}
    </Layout>
);
  • 連絡事項が表示されるページcontact_board.jsを作成
import Link from 'next/link';
import Layout from '../components/Layout';
import Firelist from '../components/Firelist';
import Button from '@material-ui/core/Button';

export default () => (
    <Layout header='Whiteboard' title='連絡板'>
        <Firelist />
        <br />
        <Link href='/contact_add'>
            <Button variant="contained" color="primary">新規投稿</Button>
        </Link>
        &nbsp;
        <Link href='/contact_del'>
            <Button variant="contained">投稿削除</Button>
        </Link>
        <br />
        <Link href='/'>
            <a>&lt;&lt; 戻る</a>
        </Link>
    </Layout>
)
  • 新規投稿するページcontact_add.jsを作成
import Link from 'next/link';
import Layout from '../components/Layout';
import Fireadd from '../components/Fireadd';

export default () => (
    <Layout header='Whiteboard' title='新規投稿'>
        <Fireadd />
        <Link href='/contact_board'>
            <a>&lt;&lt; 戻る</a>
        </Link>
    </Layout>
);
  • 投稿を削除するページcontact_del.jsを作成
import Link from 'next/link';
import Layout from '../components/Layout';
import Firedelete from '../components/Firedelete';
import Firelist from '../components/Firelist';

export default () => (
    <Layout header='Whiteboard' title='投稿削除'>
        <Firedelete />
        <Firelist />
        <Link href='/contact_board'>
            <a>&lt;&lt; 戻る</a>
        </Link>
    </Layout>
);
  • _app.jsを作成
import App, { Container } from 'next/app';
import React from 'react';
import withReduxStore from '../lib/redux-store';
import { Provider } from 'react-redux';

class _App extends App {
    render() {
        const {
            Component,
            pageProps,
            reduxStore
        } = this.props
        
        return (
            <Container>
                <Provider store={reduxStore}>
                    <Component {...pageProps} />
                </Provider>
            </Container>
        );
    }
}

export default withReduxStore(_App)

コンポーネントを作っていく

ここからはomponentsフォルダ内での作業です。

レイアウト用のコンポーネント
  • 全体のレイアウトを行うLayout.jsxを作成

HeaderとFooterを読み込んで、コンテンツは{this.props.children}としています.

import React, { Component } from 'react';
import Header from './Header';
import Footer from './Footer';
import style from '../static/Style';

class Layout extends Component {
    render() {
        return (
            <div>
                {style}
                <Header header={this.props.header} title={this.props.title} />
                {this.props.children}
                <Footer footer="Copyright (C) 2019 hoge inc." />
            </div>
        );
    }
}

export default Layout;
  • Header.jsxを作成
import React, { Component } from 'react';

class Header extends Component {
    render() {
        return (
            <header>
                <div>{this.props.header}</div>
                <h1>{this.props.title}</h1>
            </header>
        );
    }
}

export default Header;
  • Footer.jsxを作成
import React, { Component } from 'react';

class Footer extends Component {
    render() {
        return (
            <footer>
                <div>{this.props.footer}</div>
            </footer>
        );
    }
}

export default Footer;

レイアウトコンポーネントができたのでスタイルシートを用意します。

一旦components(作業ディレクトリ/components)フォルダから、作業ディレクトリに戻ってstaticフォルダを作成しそこに移動する。((作業ディレクトリ/static)

  • デザインを好きなように記述するStyle.jsを作成する。
export default 
<style>
{`
body {
    margin:10px;
    padding:5px;
    color:#669;
}
//以下略

`}
</style>;
Firebaseにデータを追加・取得・削除するコンポーネント

components(作業ディレクトリ/components)フォルダに戻ります。

取得・追加・削除のソースコードGitHubにあります。

  • 連絡事項を取得するFirelist.jsx
  getFireData() {
    let db = firebase.database();
    let ref = db.ref("sample/");
    let self = this;
    ref
      .orderByKey()
      .limitToLast(10)
      .on("value", (snapshot) => {
        self.setState({
          data: snapshot.val()
        });
      });
  }

  getTableData() {
    let result = [];
    if (this.state.data == null || this.state.data.length == 0) {
      return [
        <tr key="0">
          <th>NO DATA</th>
        </tr>
      ];
    }
    for (let i in this.state.data) {
      result.push(
        <tr key={i}>
          <th>{this.state.data[i].ID}</th>
          <td>{this.state.data[i].message}</td>
          <th>{this.state.data[i].date}</th>
        </tr>
      );
    }
    return result;
  }
  • 新規追加するFireadd.jsx
    doChangeMsg(e) {
        this.setState({
            msg_str: e.target.value
        })
    }

    doAction(e) {
        this.addFireData();
        Router.push('/contact_board');
    }

    getLastID() {
        let db = firebase.database();
        let ref = db.ref('sample/');
        let self = this;
        ref
        .orderByKey()
        .limitToLast(1)
        .on("value", snapshot => {
            let res = snapshot.val();
            for(let i in res) {
                self.setState({
                lastID: i
                });
                return;
            }
        });
    }

    addFireData() {
        if (this.state.lastID == -1) {
            return;
        }
        let id = this.state.lastID * 1 + 1;
        let db = firebase.database();
        let date = new Date().toString().slice(4,10); // 日付を取得
        let ref = db.ref('/sample/' + id);
        ref.set({
            ID: id,
            message: this.state.msg_str,
            date: date
        });
    }
  • 投稿を削除するFiredelete.jsx
    doChange(e) {
        this.setState({
            id_str: e.target.value
        })
    }

    doAction() {
        let result = confirm('本当に削除してもよろしいですか?');
        if(result) {
            this.deleteFireData();
        }
        Router.push('/contact_board');
    }

    deleteFireData() {
        let id = this.state.id_str;
        let db = firebase.database();
        let ref = db.ref('sample/' + id);
        ref.remove();
    }

Next.jsでFirebaseの処理を行う

firebaseのオブジェクトは、最初に一度初期化の処理を行わないといけないのですが、Next.jsでは複数のページにアクセスできるので、ファイルによってはスクリプトが複数回実行されてエラーになります。 そのため、Reduxを組み込んでストアに共通する値を保管するようにしました。(作業ディレクトリ/store.js)

import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import firebase from "firebase";

//Firebaseの初期化(自分のFirebaseプロジェクトからコピーしてくる)
const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  databaseURL: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

var fireapp;
try {
  firebase.initializeApp(firebaseConfig);
} catch (error) {
  console.log(error.message);
}

export default fireapp;

const initial = {
}

function fireReducer (state = initial, action) {
  switch (action.type) {
      case 'TESTACTION':
        return state;

      default:
          return state;
  }
}
  
export function initStore(state = initial) {
  return createStore(fireReducer, state, applyMiddleware(thunkMiddleware))
}

このstore.jsがどこから読み込まれるかというと、これを作成する必要があります。(作業ディレクトリ/lib/redux-store.js) このredux-store.jsでimportされる時に一度だけスクリプトが読み込まれます。

import React, { Component } from 'react';
import { initStore } from '../store';

const isServer = typeof window === 'undefined'
const _NRS_ = '__NEXT_REDUX_STORE__'

//ストア作成
function getOrCreateStore (initialState) {
  if (isServer) {
    return initStore(initialState)
  }


  if (!window[_NRS_]) {
    window[_NRS_] = initStore(initialState)
  }
  return window[_NRS_]
}

//Appコンポーネントを作成
export default (App) => {
  return class AppWithRedux extends Component {
    static async getInitialProps (appContext) {
      const reduxStore = getOrCreateStore()
      appContext.ctx.reduxStore = reduxStore
      let appProps = {}
      if (typeof App.getInitialProps === 'function') {
        appProps = await App.getInitialProps(appContext)
      }
      return {
        ...appProps,
        initialReduxState: reduxStore.getState()
      }
    }

    constructor (props) {
      super(props)
      this.reduxStore = getOrCreateStore(props.initialReduxState)
    }

    render () {
      return <App {...this.props}
        reduxStore={this.reduxStore} />
    }
  }
}

おわりに

Databaseのルールを設定してなかったので、書き換えられ放題な状況でした。 また、現在ゴミ捨てなどの何かしらの当番を管理できるような機能を追加中です。