(2017/12/13)弊社アドベントカレンダーに追加したので気持ち程度修正しました。
##CSV一括登録機能?
下記のようなことをしたい!
- ドラッグアンドドロップでCSVをアップロード
- CSVをJSONにパース
- JSONを配列に変換し、Storeへ格納
- フォーム量産
今回は上2つの実装について書きます。
##D&D
ReactのD&Dライブラリはこんな感じ。
react-dnd
が有名だけど、今回はreact-dropzone
を使った。
OS標準のFile Promptを呼び出すのが簡単だった。
- react-dnd
- React-Dropzone-Component
- react-dropzone( https://react-dropzone.js.org/ )
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来てたのでもうすぐ直るはず。
- csvtojson
- csv
- papaparse( http://papaparse.com/ )
Shift-JIS
CSVは文字コードがShift-JISになってる可能性があるので、文字化けしないようにエンコードする必要ある!!!!
つらい!!!!Unicodeであって欲しい!!!
でも便利なライブラリある!!!!ありがとうございます
encoding.js
https://github.com/polygonplanet/encoding.js/blob/master/README_ja.md
npmではencoding-japanese
という名前で配信されてる。
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.convert
のto
にutf-8
を指定すると実行できなかったのでUnicodeに変えたらうまくいきました。