2018年8月10日

ReactでCSVファイルを読み込んでクライアント側で帳票を出力する

やりたかった事


あるWebサービスからCSV形式でデータをエクスポート出来るのですが、そのデータからちょっとした帳票を印刷したいという要望がありました。

そこでなるべく手っ取り早く、ブラウザだけで動くものが出来ないかと考えて作ってみたのが今回のプログラムです。

大まかな流れは、
  1. ローカルフォルダにあるCSVファイルを指定するとブラウザ上のJavaScriptでその内容を読み込む。
  2. 読み込んだ内容からHTMLで帳票を生成して表示する。
  3. ブラウザの印刷機能を使って手動で印刷またはPDFとして保存する。
という感じになります。


CSVファイルの例


User Client Project Description Start date Start time End date End time
Mike 株式会社◯◯◯ 売上管理システム開発 データ取込エラーの調査 2018-07-04 18:15:00 2018-07-04 18:45:00
Mike 株式会社◯◯◯ 売上管理システム開発 データ取込エラーの調査 2018-07-05 12:10:00 2018-07-05 13:31:00
Mike 株式会社◯◯◯ 売上管理システム開発 データ取込エラー対策の実装 2018-07-06 10:54:00 2018-07-06 11:05:00
Mike 株式会社◯◯◯ 売上管理システム開発 AWS・RDSの設定・動作確認 2018-07-13 14:01:57 2018-07-13 14:22:57
Mike 株式会社◯◯◯ 売上管理システム開発 EC2インスタンスの作成・動作確認 2018-07-18 08:10:14 2018-07-18 08:30:14
Mike △△株式会社 iOSアプリ開発 サーバーからの画像ダウンロード処理の変更 2018-07-20 11:55:05 2018-07-20 12:05:07
Mike △△株式会社 iOSアプリ開発 画像アップロード処理の実装 2018-07-20 13:00:14 2018-07-20 13:51:48
Mike 株式会社◯◯◯ 売上管理システム開発 月次レポート出力画面作成 2018-07-30 10:37:06 2018-07-30 10:59:56
Mike 株式会社◯◯◯ 売上管理システム開発 月次レポート出力画面の仕様変更・テスト 2018-07-30 11:27:59 2018-07-30 12:20:59
Mike 株式会社◯◯◯ 売上管理システム開発 次期バージョンの追加機能についてミーティング 2018-07-31 21:00:00 2018-07-31 21:41:00
view raw sample_data.csv hosted with ❤ by GitHub





作成したい帳票


実際に動いているもの

https://mikehibm.github.io/react-csv-example/




1. プロジェクトを作成する


プロジェクトは前回のエントリーと同様に Create React App + TypeScriptで作ることにします。
npm install -g create-react-app
create-react-app my-app --scripts-version=react-scripts-ts
cd my-app/
npm start

FileSelect.tsx と Report.tsx というファイルを新規作成して、App.tsx からこれらを呼び出して使うようにします。

最初は FileSelect コンポーネントを表示してCSVファイルの選択を行い、ボタンがクリックされたらファイルの内容を読み取って、Report コンポーネントに渡します。




Report コンポーネントが表示されている時は FileSelect コンポーネントは非表示になるようにします。




2. JavaScriptでCSVファイルの内容を読み込む


HTML5のFile APIを使います。FileReaderでの読み込みは非同期処理なので、処理をラップしてPromiseを返す関数を作りました。


function readFileAsText(file: Blob): Promise<string> {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(reader.error);
reader.onload = () => resolve((reader.result as string) || '');
reader.readAsText(file);
});
}





3. ファイルの内容をパースしてオブジェクトの配列に変換する


上の readFileAsText() からファイル全体の内容が文字列として返ってくるので、それをパースしてオブジェクトの配列に変換します。

具体的には、まず改行文字で区切って行単位の配列に分け、さらにその各行について「,」で区切って列単位の配列に分けます。つまり最終的には string[][] (文字列の配列の配列)になります。


function mapCSVToArray(csv: string): string[][] {
return csv.split('\n').map((row) => row.split(','));
}


ここでは簡単に改行文字(\n)と「,」で区切っているだけですが、実際には改行文字が違う、値に「,」が含まれている、などさまざまなケースがあり得るのでCSVのパース処理はちゃんとやろうとすると実は結構大変です。

なので必要であれば下の記事で紹介されている csv-parser などを使った方が良いかもしれません。

ブラウザ上でCSVファイルをパースする
https://qiita.com/ledsun/items/e38ee0dff8f26bf8d930




その後、文字列の配列(の配列)から、今度は帳票を生成する時に扱いやすいように、WorkItemというクラスのインスタンスの配列に変換します。

この時に、文字列型から日付型への変換や、CSVファイルに無い項目の値(duration)の計算なども同時に行っています。


import * as moment from 'moment';
export interface WorkItem {
user: string;
client: string;
project: string;
description: string;
startDate: Date;
endDate: Date;
duration: number;
}
export function mapArrayToWorkItem(data: string[][]): WorkItem[] {
return data
.map((row) => {
const startDate = moment(`${row[4]} ${row[5]}`, 'YYYY-MM-DD HH:mm:ss');
const endDate = moment(`${row[6]} ${row[7]}`, 'YYYY-MM-DD HH:mm:ss');
const duration = moment.duration(endDate.diff(startDate));
return {
user: row[0],
client: row[1],
project: row[2],
description: row[3],
startDate: startDate.toDate(),
endDate: endDate.toDate(),
duration: duration.asHours()
};
})
.filter((i) => i.client !== 'Client' && i.client); // 先頭と末尾の行を除外。
}
view raw WorkItem.js hosted with ❤ by GitHub



4. HTMLで帳票を生成する


WorkItemオブジェクトの配列を受け取って、tableタグで表形式のHTMLに変換します。

本当は帳票のタイトル、見出し、明細行などをそれぞれ別のコンポーネントに分けた方が良いのだと思いますが、今回はあまり時間がなかったので1コンポーネントで作ってしまいました。


import * as React from 'react';
import * as moment from 'moment';
import { WorkItem } from './WorkItem';
import './Report.css';
interface Props {
items: WorkItem[];
}
export const Report = (props: Props) => {
const totalHours = props.items.reduce<number>(
(prev: number, cur: WorkItem) => prev + cur.duration,
0
);
const startOfMonth = moment(props.items[0].startDate).startOf('month');
const endOfMonth = moment(props.items[0].startDate).endOf('month');
return (
<div className="Report">
<h4>
{startOfMonth.format('YYYY-MM-DD')} ~ {endOfMonth.format('YYYY-MM-DD')}
</h4>
<table>
<thead>
<tr>
<th>Description</th>
<th>Date</th>
<th>Time</th>
<th>Hours</th>
</tr>
</thead>
<tbody>
{props.items.map((i, index) => (
<tr key={index}>
<td className="description">
<span className="project">{i.project}</span>
<br />
{i.description}
</td>
<td>{moment(i.startDate).format('MM/DD')}</td>
<td className="start_time">
{moment(i.startDate).format('HH:mm')}~{moment(i.endDate).format('HH:mm')}
</td>
<td className="number">{i.duration.toFixed(2)}h</td>
</tr>
))}
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<th>Total</th>
<th className="number">{totalHours.toFixed(2)}h</th>
</tr>
</tbody>
</table>
</div>
);
};
view raw Report.tsx hosted with ❤ by GitHub



あとは画面に表示された帳票をブラウザの印刷機能で印刷するだけです。デフォルトではおそらく余計なヘッダーやフッター(ページタイトルやURL)も印刷されてしまいますが、印刷時の設定画面でこれらをオフにしておけばきれいに出力されるはずです。


今回作ったアプリの全ソースコードは下記にあります。

https://github.com/mikehibm/react-csv-example







2018年8月8日

Create React App + TypeScript で最初につまずいたこと

ちょっとしたサンプルプログラム的なものを Create React App + TypeScript で作ろうと思ったのですが、思わぬところでいきなり詰まってしまいました。


Create React App(以下CRA)で TypeScript と言えば、

https://github.com/wmonk/create-react-app-typescript

が定番だと思います。


さて、下の通りにプロジェクトを作成して実行しました。

npm install -g create-react-app
create-react-app my-app --scripts-version=react-scripts-ts
cd my-app/
npm start


もちろんここまでは全く問題なし。早速VS Codeでプロジェクトのフォルダを開いて、アプリケーションを作り始めました。



ところが、あるタイミングでふと

console.log("hogehoge");

を入れて実行してみようと思ったところ、いきなりコンパイルエラーになってしまいました。


「Calls to 'console.log' are not allowed.」というエラーですが、これにはちょっと驚かされました。console.logを入れただけでアプリケーションが動かなくなるなんて。。。


これですが、デフォルトで設定されているTSLintのルールがめちゃくちゃ厳格なものになっているのが原因なようです。


厳格すぎてこれでは使いづらいという声も多く、GitHubのIssuesでも議論になっているようですね。


オリジナルのCRA作者のDan Abramov氏からも、「この厳しいルールはCRAを作った時の思想(初心者でもすぐに動くものを作り始められる。本当にクリティカルなもの以外はワーニングのみでエラーにはしない。)と合わないから、ゆるくした方が良いんじゃない?」 という趣旨のIssueをわざわざ上げられているぐらいです。


create-react-app-typescript リポジトリ作者のWill Monk氏によると、「最初は自分が職場で使っているルールをコピーして使っていたのだけれど、後で一般的に推奨されているプリセット("tslint:recommended", "tslint-react"など)を使うように変更した」との事。「それでもまだルールが厳しすぎるなら、誰かがオリジナルのCRAと同等のゆるいルールの設定を作ってPRしてくれたら喜んでそれをマージするよ。」とも言っているので、彼自身は特にどうしても厳しいルールを他の人に強制する気は無いようです。


その後このIssue上で議論は進んでいるようですが、このブログ執筆時点の最新版ではまだルールの変更には至っていないようです。


さて、なにはともあれとりあえず console.log を使えるようにする方法ですが、上のIssueでも言及されているのですが、Microsoftによる下記のページにわかりやすくはっきりと記載されていました。

https://github.com/Microsoft/TypeScript-React-Starter


プロジェクトのルートフォルダにある tslint.json ファイルを下のように書き換えればOKです。(赤字を削除、青字を追加。)

{
-  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
+  "extends": [],
+  "defaultSeverity": "warning",

"linterOptions": {
     "exclude": [
       "config/**/*.js",
       "node_modules/**/*.ts"
     ]
   }
 }

これで問題なく開発が続けられるようになりました!






 

2018年5月6日

Dockerコンテナ内でASP.NET Core 2.1 Webアプリをビルド・実行する

やりたかった事:


Macに.Net SDKを入れずにASP.NET Core 2.1のWebアプリを開発したい。


試した事:


Docker(Version 18.03.1-ce-mac64)でMS公式の.NET 2.1 SDKイメージを動かしてみました。

まずは、SDKの公式イメージからコンテナを起動します。
docker run --rm -it \
  -v `pwd`:/app/ \
  -w /app/aspnetapp \
  microsoft/dotnet:2.1-sdk
これを実行すると、Mac側のカレントディレクトリがコンテナの/app/ディレクトリにマウントされて、かつコンテナ内のカレントディレクトリが/app/aspnetapp になった状態でシェルが起動します。

ここから、
dotnet new webapi 
を実行すればWeb APIのひな形のコードが生成されます。


さらに、
dotnet run
を実行すればデフォルトの5000番ポートでWeb APIが実行されますが、このままだとポートマッピングの設定をしていないのでホスト側からはアクセス出来ません。



とりあえずこれはCtrl+Cで終了してコンテナを終了し、次にdocker runコマンドに -p オプションを付けた上で、シェルを開くのではなく「dotnet watch run」コマンドを実行してみます。

使ったコマンドはこちら。
PORT=8080
docker run --rm -it --name watcher \
  -p $PORT:$PORT \
  -v `pwd`:/app/ \
  -w /app/aspnetapp \
  microsoft/dotnet:2.1-sdk \
  dotnet watch run $PORT
任意のポート番号でアプリを動かせるように、事前にProgram.csファイルにちょっと細工をしておきました。

#########
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args)
.Build()
.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
var port = args.Length > 0 ? args[0] : "8080";
return WebHost.CreateDefaultBuilder(args)
.UseUrls($"http://*:{port}")
.UseStartup<Startup>();
}
}
view raw Program.cs hosted with ❤ by GitHub
#########

起動後、指定したポート番号でアプリが動いているのが分かります。




分かった事:


docker runでコンテナを起動する際に -v オプションでカレントディレクトリをコンテナ内のディレクトリにマウントしておけば、ソースコードはホスト(Mac本体)側で編集し、BuildやPublishはコンテナ内で行うという事が出来ました。

コンテナ内で動かすコマンドを「dotnet watch run」としておけばコードを変更したら自動でアプリを再起動してくれます。

これでMac本体に.Net SDKを入れずにASP.NET Core 2.1のWebアプリを開発するという目的が達成出来ました!




参考URL:
dotnet-docker/aspnet-docker-dev-in-container.md at master
https://github.com/dotnet/dotnet-docker/blob/master/samples/aspnetapp/aspnet-docker-dev-in-container.md
MS公式Dockerイメージを使って.NET Core開発を行う(Mac) - ryuichi111stdの技術日記
http://ryuichi111std.hatenablog.com/entry/2016/11/07/020326








.