JavaScript
CSV
reactjs
React

Reactで「CSV一括登録機能」 を開発した話

(2017/12/13)弊社アドベントカレンダーに追加したので気持ち程度修正しました。

CSV一括登録機能?

下記のようなことをしたい!

  • ドラッグアンドドロップでCSVをアップロード
  • CSVをJSONにパース
  • JSONを配列に変換し、Storeへ格納
  • フォーム量産

今回は上2つの実装について書きます。

D&D

ReactのD&Dライブラリはこんな感じ。
react-dndが有名だけど、今回はreact-dropzoneを使った。
OS標準のFile Promptを呼び出すのが簡単だった。

FileUpload.js
import Dropzone from 'react-dropzone';
import Button from 'components/atoms/Button';

class FileUpload extends Component {

  constructor(props) {
    super(props);
    this.state = {
      isDragReject: false,
    };
    ...
  }

  onDrop() {
    this.setState({
      isDragReject: false,
    });
  }

  onDropRejected() {
    this.setState({
      isDragReject: true,
    });
  }

  handleReflect(results) {
    this.props.onComplete(results);
  }

  handleParseCsv(files) {...}

  render() {
    let dropzoneRef;
    return (
      <div className={style.fileUpload}>
        <div className={style.dropArea}>
          <Dropzone
            onDrop={this.onDrop}
            onDropAccepted={this.handleParseCsv}
            onDropRejected={this.onDropRejected}
            accept="text/csv, application/vnd.ms-excel"
            disableClick
            multiple={false}
            className={style.dropzone}
            activeClassName={style.active}
            rejectClassName={style.reject}
            ref={(node) => { dropzoneRef = node; }}
          >
            <p>Try dropping CSV file here, or click to select files to upload.</p>
            {(() => (this.state.isDragReject ?
              <p className={style.rejectMessage}>ファイルタイプが違います。</p>
              : '')
            )()}
          </Dropzone>
          <Button
            onClick={() => { dropzoneRef.open(); }}
          />
        </div>
      </div>
    );
  }
}
  • stateのisDragRejectでアップロードが失敗した時のメッセージの出し分けしてる
  • 今回はCSVしか受け付けたくないので、Dropzoneコンポーネントのaccept="text/csv, application/vnd.ms-excel"のところでファイルタイプを指定してる。MacとWindowsでCSVのファイルタイプの指定の仕方が違うらしいので2つ書いてる

CSV to JSON

CSVをJSONにパースする部分はpapaparseを使った。ドキュメントが豊富で使いやすかった。
csvtojsonはよくわからんエラーが出て諦めた。issueたてたらPR来てたのでもうすぐ直るはず。

:imp::imp::imp:Shift-JIS:imp::imp::imp:

CSVは文字コードがShift-JISになってる可能性があるので、文字化けしないようにエンコードする必要ある!!!!
つらい!!!!Unicodeであって欲しい!!!
でも便利なライブラリある!!!!ありがとうございます:innocent::innocent:

encoding.js
https://github.com/polygonplanet/encoding.js/blob/master/README_ja.md

npmではencoding-japaneseという名前で配信されてる。

FileUpload.js
import Papa from 'papaparse';
import Encoding from 'encoding-japanese';

class FileUpload extends Component {

  constructor(props) {
    ...
  }

  handleReflect(results) {
    this.props.onComplete(results);
  }

  handleParseCsv(files) {
    const file = files[0];
    const reader = new FileReader();
    reader.onload = (e) => {
      const codes = new Uint8Array(e.target.result);
      const encoding = Encoding.detect(codes);
      const unicodeString = Encoding.convert(codes, {
        to: 'unicode',
        from: encoding,
        type: 'string',
      });
      Papa.parse(unicodeString, {
        header: true,
        dynamicTyping: true,
        skipEmptyLines: true,
        complete: (results) => {
          this.handleReflect(results);
        },
      });
    };
    reader.readAsArrayBuffer(file);
  }

  render() {...}
}
  • header: trueにするとヘッダーの文字列をJSONのキーにしてくれる
  • reader.onloadでファイルの読み込みが完了したときの処理を定義する
  • 読み込んだファイルをUint8Arrayオブジェクトを使ってバイト列に変換
  • encoding.jsを使って現在の文字コードを取得
  • Shift-JISをUnicodeに変換(Unicodeのときはそのまま)
  • 文字コード変換したデータをpapa parseに渡して、JSONにパースする
  • 今回はCSVの中に数字が含まれていたのでdynamicTypingを指定して、数字がstringに変換されないようにしている
  • 完了したら、親コンポーネントで定義しているデータをフォームに反映する処理(handleReflect)を実行する
  • さいごに、reader.readAsArrayBufferでファイルの読み込みを実行する

今回は、文字コードの変換とCSVのパースを組み合わせるところではまって大変でした。
Encoding.converttoutf-8を指定すると実行できなかったのでUnicodeに変えたらうまくいきました。