かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

APIのモックアップに便利なjson-server使ってみた。

フロントエンドの制作とかで使えるAPIモックアップjson-serverってのが便利だよーって教えてもらったので使ってみた。

インストール

$ yarn add -D json-server

デフォルトのデータを作成

db.json

{
  "articles": [
    {"id": 1, "title": "json-server", "author": "TEST"},
    {"id": 2, "title": "aikatsu", "author": "Johnny"}
  ]
}

サーバーを起動するスクリプトを作成

packege.jsonjson-serverを起動するnpmスクリプトを作成 packege.json

{
  "scripts": {
    "start": "json-server --watch db.json --port 3001"
  }
}

--watchオプションを付けておくとサーバー起動中に直接db.jsonを編集しても変更内容を反映してくれるようになる。
(メインのアプリを3000で起動することが多いので、json-serverは3001番を指定しました。)

json-serverを起動

$ yarn start

  \{^_^}/ hi!

  Loading db.json
  Done

  Resources
  http://localhost:3001/articles

  Home
  http://localhost:3001

エンドポイントにアクセスしてデータを取得してみる

curlコマンドでDBにアクセスしてみます

$ curl -X GET http://localhost:3001/articles

👇

[
  {"id": 1, "title": "json-server", "author": "CHAIKA"},
  {"id": 2, "title": "aikatsu", "author": "Johnny"}
]

articlesのデータが返されました。

idで単一のデータを取得

データにidキーが有るとhttp://localhost:3001/articles/{id}でデータにアクセスできるようです。
cf. json-server#getting-started

$ curl -X GET http://localhost:3001/articles/2

👇

{"id": 2, "title": "aikatsu", "author": "Johnny"}

GETパラメータを使ってデータを取得

?{key}={value}のいわゆるGETで使われるパラメータ指定はフィルターになるようです。
cf. json-server#filter

$ curl -X GET http://localhost:3001/articles?title=json-servr

👇

[
  {"id": 1, "title": "json-server", "author": "CHAIKA"}
]

フィルターなので配列が返されました。

POST PUT DELETE

GET以外にもPOST, PUT, DELETEも受け付けます。
これらでデータを変更するとdb.jsonが直接変更されます。

POST
$ curl -X POST http://localhost:3001/articles/ -d 'title=foo&author=bar'
{
  "title": "foo",
  "author": "bar",
  "id": 3
}

idが自動的に割り振られてデータが挿入されました。
複数のデータがある場合はPOSTするデータを上記のように'で囲うか、-d title=foo -d author=barの様にデータごと-dオプションをで渡して上げる必要があります。-d title=foo&author=barのようにデータをコーテーションで囲わず送ると、先頭の値だけが受け付けられました。

PUT
$ curl -X PUT http://localhost:3001/articles/3 -d 'title=星宮いちご&author=星宮らいち'
{
  "title": "星宮いちご",
  "author": "星宮らいち",
  "id": 3
}

確認

$ curl -X GET http://localhost:3001/articles/

👇

[
  {"id": 1, "title": "json-server", "author": "CHAIKA"},
  {"id": 2, "title": "aikatsu", "author": "Johnny},
  {"title": "星いちご", "author": "星宮らいち", "id": 3}
]

※ 該当するidのデータがまるっとPUTで送ったデータに置き換えらる

$ curl -X PUT http://localhost:3001/articles/3 -d name=紫吹欄
{
  "name": "紫吹蘭",
  "id": 3
}
$ curl -X GET http://localhost:3001/articles/3

👇

{"name": "紫吹蘭", "id": 3}

もとのパラメータにマージされるわけではないので注意が必要かもです。 (PUTとしては正しい挙動?)

DELETE
$ curl -X DELETE http://localhost:3001/articles/3
{}

確認

$ curl -X GET http://localhost:3001/articles/

👇

[
  {"id": 1, "title": "json-server", "author": "CHAIKA"},
  {"id": 2, "title": "aikatsu", "author": "Johnny}
]

オンメモリで動作させる

jsonファイルを指定して動作させるとPOSTやDELETEでjsonファイルそのものが変更されてしまいますが、JSファイルを指定して実行した場合オンメモリとなりファイルの変更はされません。
公式ドキュメントには1000個のユーザーを作成するサンプルが載っています。cf. json-server#generate-random-data

db.js

module.exports = () => require('./db.json');

package.json

{
  "scripts": {
    "start-js": "json-server --watch db.js --port 3001"
  }
}

実行

$ yarn start-js

元のdb.jsonの内容はデフォルトデータとして利用され変更されることなくPOST, PUT, DELETEでデータが追加・変更・削除可能で、json-serverを走らせているプロセスが生きている限り変更した内容が保持されているようです。

[WIP] ネストしたデータを取得する方法が不明

例えば次のようなデータ

{
  "units": [
    {
      "id": 1,
      "name": "soleil",
      "members": [
        {"id": 1, "name": "星宮いちご", "division": "スターライト学園" },
        {"id": 2, "name": "霧矢あおい", "division": "スターライト学園" },
        {"id": 3, "name": "紫吹蘭", "division": "スターライト学園" }
      ]
    },
    {
      "id": 2,
      "name": "Luminas",
      "members": [
        {"id": 1, "name": "大空あかり", "division": "スターライト学園" },
        {"id": 2, "name": "氷川すみれ", "division": "スターライト学園" },
        {"id": 3, "name": "新条ひなき", "division": "スターライト学園" }
      ]
    },
    {
      "id": 3,
      "name": "WM",
      "members": [
        {"id": 1, "name": "神崎美月", "division": "LOVE MOONRISE" },
        {"id": 2, "name": "夏樹みくる", "division": "ナツキグリーニングガーデン" }
      ]
    }
  ]
}

soleilのデータは次のURLで取得できます。

http://localhost:3001/units/1

soleil内のmembersから星宮いちごちゃんを取得しようとして

$ curl -X GET http://localhost:3001/units/1/members/1
$ curl -X GET http://localhost:3001/units/1/members?name=星宮いちご

としてもデータを取得することが出来ませんでした。
アクセス方法が間違っているのか、そもそもネストしたデータを取れるようになっていないのかは深く調べていないので現状わかっていません。

所感

フラットなデータをやり取りするAPIjson-serverをインストールしてデータを用意するだけでサクッと作る事ができますので、Firebaseとか実際のサービスと繋ぐ前の開発用途にとても良さそうだと感じました。
ネストしたデータを取る方法がわからないままですが、GitHubのREADMEにはrouterの設定方法なども書いてあるので、自分で設定してしまえばネストしたデータとか複雑なデータでも返せるWEBAPIが作れるのではないかという気がしています。


[参考]

Team Geek ―Googleのギークたちはいかにしてチームを作るのか

Team Geek ―Googleのギークたちはいかにしてチームを作るのか

👆お正月に読みました。チーム・文化作りの本、アンチパターンも載っていて大変良かったです。
HRT心がけて生活していきたいです⭐

2018年に読んで良かったと思った本を5つ

あけましておめでとうございます。
Hyvää uutta vuotta!

私の夢は図書館のような部屋に住み本に埋もれて死ぬことです。
2018年に読んで良かったと思った本を5つ紹介したいと思います。
(普段プログラムのことが多いのでビジネス書的なのから順番に)

人生にゆとりを生み出す 知の整理術 - pha (思想・マインドセット)

人生にゆとりを生み出す 知の整理術

人生にゆとりを生み出す 知の整理術

知識を付け自分をアップデートしつづ付ける習慣をつけるためのテクニック集。

昨今、特にSNS等で知識がないことが強みのように振る舞う人を見かけるが、実際は知識があれば避けられる不幸があり知識があればより楽に生きることができる。
そして知識は誰も独占できない共有の財産ですべての人に平等に開かれているべきである。
楽にお金をかけずとも知識や情報に触れる習慣の付け方や、忘れっぽい未来の自分のためにblogやTwitterにアウトプットするテクニックなどが、小さな章ごとに別れて書かれているのでどこから読んでも大丈夫なようになっている。

人間は本来怠惰な生き物なので、習慣になってしまえば逆にその習慣を辞めることが難しい。そんな性質を利用して毎日少しでも興味ある部分を読んでみるのはどうだろうか?

1万円起業 - クリス・ギレボー (ビジネス書)

1万円起業 文庫版

1万円起業 文庫版

副業のすゝめ。

100$くらいの資金からビジネスを始めて成功した1500人へのインタビューを元に書かれた本。 起業するぞ!と息巻いて始めたというより、やってた事がビジネスになると気づいて起業したという感じの例が多い。
小さくとりあえず始めて見ること、小さく始めれば失敗してもダメージも少ないし方向転換も簡単だ。

やりたいこと・できること・求められていること を見極めることの大切さ、間違った方向の情熱だけではビジネスにならない。顧客には魚の釣り方を教えるのではなく魚を提供しよう。自分ではやってもないのに「そんなの無謀だ」とか「こうした方が良い」と助言してくる身内や友人の話は無視しよう。なぜなら彼らは実際には自分では何もやってないのだから。クレームを言ってくる人は何をやっても一定数いるから無視しよう。自分を安売りはするな。などなど名著「小さなチーム大きな仕事」でも書かれていたようなことも成功者のインタヴューとして出てくるので真理なのだと感じた。

本書を読むに当たって気をつけるべきは、紹介されている例の多くが英語圏での事である。ということだろう。
つまり、英語圏でビジネスをするという事は、日本人が日本語でビジネスを始めるより圧倒的にパイとなる人口が多いので、n%の人に刺さるサービスだとすれば当然母数が多いほうがビジネスとしては成功しやすい訳ですから。

放射線について考えよう。 - 多田将 (科学・テクノロジー・物理学)

放射線について考えよう。

放射線について考えよう。

イラスト描かせてもらってWEBサイト作らせてもらった作品なので自画自賛感がありますが、
イラストを描かせてもらうに当たって何度も本文を読み込み、放射線についての知識はもちろんだけど、何より自分で考えられるようになることの大切さ。定量的に、どの程度なのかと考えられるようになることの大切さ。を学ぶことが出来ました。

放射線についても、一つ一つ原理原則から段階を追って説明されており、イラストや例え話を使っていわゆる理系でなくとも理解しやすいように配慮された本になっていると思います!WEBサイトではフルカラーで全文公開されており、著者・出版社が正しい知識をもってもらえる事こそを目的としていることが伺えます。

放射線について、見えないからお化けのように怖がったままになるのではなく。正しい知識を付けてお化けを白日のもとに晒しましょう!
何より、解らないからといって自分で考えず誰かの意見を信じてしまうということは、騙されてしまっても仕方なく、それこそが危険な行為なのですから!!

アルキメデスのお風呂 - ニコ・ニコルソン (漫画)

アルキメデスのお風呂 1 (MFC)

アルキメデスのお風呂 1 (MFC)

科学的なことをがっつりした本や文章で読むのは苦手!という方には、ニコ・ニコルソン先生の新連載「アルキメデスのお風呂」がオススメ。

自分に自信のないぽっちゃり女子・原陽子が、王子様のような見た目の理系男子・大池学に一目惚れしたことから始まるラブコメディ。弁当屋で毎日、機械のように揚げ物を作っていた陽子は、ある日「私は誰にも必要とされてない」と思いつめて、駅で飛び込み自殺を図る。それを間一髪で止めたのが、通りがかりの大池だった。大池は茨城県東海村にある国営の研究施設で、素粒子物理学を研究する若き天才。陽子はひょんなことから研究施設の調理スタッフとして働くことになり、彼と運命の再会を果たす。
出典: 唐揚げ女子がニュートリノボーイに一目惚れ、理系ラブコメ「アルキメデスのお風呂」 - コミックナタリー

「病気や事故でなく、無知で死ぬのですか?」
ブコメ漫画を読んでいるだけで陽子の視点を通して、この世界を作っている小さな粒子の事やその研究のことを知ることができる。何よりマスコミや政治家がよく口にする基礎研究分野に対する「その研究って何の役に立つの?」に対する返答が描かれていることが素晴らしい。
加えて上記の「放射線について考えよう。」「宇宙のはじまり」の著者 多田将(高エネルギー加速器研究機構 素粒子原子核研究所 准教授。)による、わかりやすい用語解説コーナーも扉ごとにあり理解を深めることもできる。

銃・病原菌・鉄 - ジャレド・ダイアモンド (世界史)

今当たり前のように社会の中にある物事に対する"なぜ"(why)が色々と気になり始める本。

先史文明が誕生する以前、太古の人類は同じような生活を行っていたはずなのに、現代では大陸による富の差が生まれているのはぜだろうか?なぜ大陸や地域によって文明の発展に差が生まれたのだろうか?ヨーロッパが他の大陸を植民地にできるだけの力の差を得ることができたのは何故だろうか?
長らく人種による優劣のように語られてきた差が生じた原因を、人種や文明の優劣ではなく地学的な地域の特性から紐解いていく。
農耕が始まった地域に共通する特性、現代でも家畜化されている大型草食動物が羊・ヤギ・牛・豚・馬しかないことの理由、緯度経度による文化伝播の難易度の違い、集団生活が始まると疫病が発生するメカニズム、文字の発明とその伝播、家族単位・部族社会から国家・帝国が生まれる理由。

文化の発生と発展を原理から考えてみることで、現代社会に対して今までとは違った視点を持つことができるのではないだろうか。

青い星まで飛んでいけ - 小川 一水 (SF)

青い星まで飛んでいけ (ハヤカワ文庫JA)

青い星まで飛んでいけ (ハヤカワ文庫JA)

恋愛SF。ファーストコンタクトを集めた短編集。

本屋で偶然手にとった「コロロギ岳から木星トロヤへ」がたいへん好きな本で、失礼ながら作者については私自身の名前を覚えるのが苦手という理由もあって、それ以上作者の本を探すなどしていなかった事に気づき、この短編集を手にとった。

短編の多くは科学技術が進み人類が宇宙に進出している世界で主人公は電脳化されてたり人類によって創られたネットワーク化された自己修復機能を持つ機械群だったりする。そんな彼らが彼ら以外の存在とのファーストコンタクトを恋愛のように描いた物語。それらは電子信号の交換だったり、電子世界でシミュレートされた擬似戦争だったりするわけだけど…

正直SFってジャンルに詳しいわけではないのでSFとしてどうなのかという事は言えないけれど、読みながら死を失った生命の時間に対する概念や、脳の機能が電気信号なら自我はそこに宿るのか?電気信号化された世界では自己と他者の境界はどうなるのか?死の定義とは?と、ゴースト・イン・ザ・シェルの「現代の科学は未だに生命を定義することができない」という風な事を考えていた。
個人的に「守るべき肌」が好きだった。

プラス1冊 凍てつく海の向こうに - ルータ・セペティス (歴史小説)

凍てつく海のむこうに

凍てつく海のむこうに

原題『Salt to the Sea』

WW2末期ソ連軍が迫る今はない国東プロイセン。それぞれの秘密を抱えるリトアニア・東プロイセンポーランド・ドイツ4つの国出身の若者が国を脱出しようと海を目指す。物語はそれぞれの視点の語りで並行に進む。戦争に巻き込まれ戦争によって作られたそれぞれの秘密を徐々に明かしながら物語は進み脱出船ヴィルヘルム・グストロフ号で合流する。
実際にあったハンニバル作戦(ドイツ海軍が行った脱出作戦)、海事史上最大の犠牲者を出したヴィルヘルム・グストロフ号撃沈事件を下書きにした戦争に引きずり込まれた若者たちの小説。

私自身、欧州の歴史特に大戦期の北欧諸国の歴史に興味があり趣味で調べているので、ドイツ帝国のもととなったプロイセンソ連・ドイツに占領され、さらにソ連に再占領されたリトアニアポーランド、そしてドイツの少年兵と歴史には残らないし、個人では到底抗うこと出来ず戦争に巻き込まれた個人による、国家の言い分ではなく彼ら個人の見えているものを主体とした物語は映画を観ているようでたいへん興味深かった。

そして、原題「Salt to the Sea」はキリスト教的な概念で多くの日本人には馴染みがないだろうから、美しい邦題を新しくつけてくれたこと、巻頭表紙裏に物語が始まった際の国境線のある地図、巻末表紙裏は現在の国境線のある地図になっていて、物語が過去の終わったものではなく現在にも地続きであることを強く意識させる演出になっている装丁もたいへん素晴らしい本。

 

読んだ本はだいたい読書メーターにメモを取るようにしていおり、ここに上げたものはほぼそこからの焼き増しです。
この読書メーター良かった部分や気になった部分を書いておくだけで、結構記憶に残るので大変良いです。(映画メーターってのもあったのですがいつの間にかサービス終了して消えてました...)
 

去年1年を振り返ったポエム

昨年は10月から都会に半復帰しました。
都会は田舎ちほーと違って公共交通機関が発達しているので、電車で移動中にたくさん本を読めるので大変良いです。(同じ移動でも車の運転時間に取られるのとでは圧倒的にインプットできる時間の差が生まれると思います)
また、商業のイラストのお仕事を初めてさせてもらったり、compass漁って毎日のように勉強会に参加したり、工数3ヶ月確保しておいた案件が半年リスケになって貧困ライフを満喫することになったり波乱に満ちた1年でした。

2019年は技術向上と生活の安定。それから、ここ数年間繰り返されてた短納期・低報酬なお仕事のループで疲れ果ててあれだけ楽しかったデザインやお絵かきが苦痛になってしまっていたので、少しづづリハビリして楽しい習慣化できるようにしたいです。
心が死ぬとはまさに

かつてあれほどまで真剣で切実だった想いが綺麗に失われている事に気付きもう限界だと知った
新海誠秒速5センチメートル」 より引用

ということなのです。


ベルリンは晴れているか (単行本)

ベルリンは晴れているか (単行本)

PHP / JavaScript ファイル生成してDLさせたいけどmax_input_varsにかかってしまう問題にパワープレイで立ち向かった話

PHP製の管理ツールで、CSVを生成してDLするシステムを作っていました。
データ量が多いとエラーになるということで調べたら、PHPmax_input_varsにかかって居るのが原因でした。
そして、使っているサーバーがhetemlでmax_input_varsの上限を変えることが出来なかったので管理ツールということもありパワープレイで対策してみた記録。

要件
form

<form id="js-form">
  <div>
    <input type="text" name="data[0][0]" value="データA-1">
    <input type="text" name="data[0][1]" value="データB-1">
    <input type="text" name="data[0][2]" value="データC-1">
    <input type="text" name="data[0][3]" value="データD-1">
  </div>
  <div>
    <input type="text" name="data[1][0]" value="データA-2">
    <input type="text" name="data[1][1]" value="データB-2">
    <input type="text" name="data[1][2]" value="データC-2">
    <input type="text" name="data[1][3]" value="データD-2">
  </div>
  <div>
    <input type="text" name="data[2][0]" value="データA-3">
    <input type="text" name="data[2][1]" value="データB-3">
    <input type="text" name="data[3][2]" value="データC-3">
    <input type="text" name="data[3][3]" value="データD-3">
  </div>
  <label><input type="checkbox" name="is_sjis" checked="checked" value="1"> Shift JIS</label><br>
  <input type="submit" value="CSV Download">
</form>
  • フォームデータはname="data[0][0]"のようなCSVの行列の連想配列形式
  • フォームをデータからCSVを作ってDLできる
  • CSVエンコードをShift-JIS / UTF-8 選択できる (エクセルで文字化けすると言われたのでSJIS対応)

結論

以下長いので、先に結論からか言えば、max_input_varsを回避してフォームが送れれば済む話なのと、JavaScriptShift_JISのファイルを作成してダウンロードさせる問題がかなり大変そうだったので、

Javascriptで新しいform elementを作成してinputタグのvalueに元のフォームデータをJSON化したものを入れて、新しく作ったフォームをform.submit()させるパワープレイで解決しました。

解決方法までジャンプ👇

方針 JavaScriptでフォームデータを1つの文字列にしてAjaxで送ってCSVをDL

フォームのinput数が多いのが問題なので、JavaScripeでデータをまとめて1つの文字列データにして送ってしまえばmax_input_varsにかかることはありません。
管理ツールにはjQueryが使われていたので、そのままjQueryを使いました。

フォームデータをJSON形式にまとめる

jQueryにはフォームデータをまとめる.serialize()メソッドがありますが、$form.serialize()シリアライズすると

const $form = $('#js-form');
const params = $form.serialize();
console.log( params );
// data%5B0%5D%5B0%5D=%E3%83%87%E3%83%BC%E3%82%BFA-1&data%5B0%5D%5B1%5D=%E3%83%87%E3%83%BC%E3%82%BFB-1&data%5B0%5D%5B2%5D=%E3%83%87%E3%83%BC%E3%82%BFC-1&data%5B0%5D%5B3%5D=%E3%83%87%E3%83%BC%E3%82%BFD-1&data%5B1%5D%5B0%5D=%E3%83%87%E3%83%BC%E3%82%BFA-2&data%5B1%5D%5B1%5D=%E3%83%87%E3%83%BC%E3%82%BFB-2&data%5B1%5D%5B2%5D=%E3%83%87%E3%83%BC%E3%82%BFC-2&data%5B1%5D%5B3%5D=%E3%83%87%E3%83%BC%E3%82%BFD-2&data%5B2%5D%5B0%5D=%E3%83%87%E3%83%BC%E3%82%BFA-3&data%5B2%5D%5B1%5D=%E3%83%87%E3%83%BC%E3%82%BFB-3&data%5B3%5D%5B2%5D=%E3%83%87%E3%83%BC%E3%82%BFC-3&data%5B3%5D%5B3%5D=%E3%83%87%E3%83%BC%E3%82%BFD-3&is_sjis=1

URLエンコードされ&区切りなので、戻すのが少し面倒そうです。

jQueryには.serializeArray()メソッドがあり、こちらはserialize()エンコード&区切りにされる前の配列の状態を取ることが出来ます。

const $form = $('#js-form');
const params = $form.serializeArray();
console.log( params );
// [0: {name: "data[0][0]", value: "データA-1"}, ..., 12: {name: "is_sjis", value: "1"}]

{name: inputタグのname: value: inputタグのvalue}というオブジェクトの配列になります。これをJSON.stringify()するとJSON形式のデータに変換することができます。
JS側でdata[][]連想配列に戻してからJSON化しても良いのですが、文字列の"data[n][n]"から連想配列に戻すのはパワーがかかりそうだったので、データの処理はAPI (PHP)側の任せることにしました。

JSON化したデータをAjaxで送り、API側でCSVを作成して書き出す

フォームのデータをJSON化してAjaxで送る処理
const maxInputVars = 1000;
const $form = $("#js-form");

function postJSONData(jsonData) {
  $.ajax({
    url: "api_url",
    type: "post",
    crossDomain: false,
    dataType: "text",
    data: {"form_data": jsonData}
  })
  .done(function(res) {
    console.log(res);
  })
  .fail(function(jqXHR, status, err) {
    console.log(status, err);
  });
}

$form.on('submit', function() {
  const postNum = $('input').length;
  if( (postNum + 50) >= maxPostVars ) {
    const params = JSON.stringify( $form.serializeArray() );
    postJSONData( params );
    return false;
  } else {
    // max_input_vars にかからそうな場合は通常にformを送信
    return true;
  }
});
CSVを作成して返すAPI (PHP)
<?php
if( isset($_POST)
 && isset($_POST['form_data'])
 && !empty($_POST['form_data']) ) {
  $data = [];
  // Shift_JIS化するかどうか
  // チェックボックスはチェックされないとそもそもデータが送られないのでデフォルト値を設定しておく
  $is_excel = false;
  // JSONを配列に戻す
  $postData = json_decode( $_POST['form_data'], true );
  // data[n][n] 文字列から連想配列に戻す
  foreach( $postData as $row ) {
    $key = $row['name'];
    $val = $row['value'];
    if( $key == 'is_sjis' ) {
      $is_excel = (bool) intval($val);
    } else {
      preg_match('/^data\[(\d+)\]\[(\d+)\]$/', $key, $matches);
      if( count($matches) == 3 ) {
        $dRow = intval($matches[1]);
        $dCol = intval($matches[2]);
        if( !isset( $data[ $dRow ] ) || !is_array( $data[ $dRow ] ) ) {
          $data[ $dRow ] = [];
        }
        $data[ $dRow ][$dCol] = $val;
      }
    }
  }
  if( count(data) > 0 ) {
    $fileName = "MY_DATA-" . date("Y-m-d") . ".csv"; 
    create_scvfile_by_array($data, $fileName, $is_excel);
  }
}

function create_scvfile_by_array($data, $fileName = 'data.csv', $is_excel = false) {
  $csv = '';
  foreach($data as $row) {
    if( is_array($row) ) {
      $csv .= join(',', $row);
    } else {
      $csv .= $row;
    }
    $csv .= "\n";
  }

  $charset = 'UTF-8';
  if( $is_excel ) {
    $csv = mb_convert_encoding($csv, 'SJIS', 'UTF-8');
    $charset = 'sjis-win';
  }

  header("Content-Type: application/octet-stream; charset={$charset}");
  header("Content-Disposition: attachment; filename={$fileName}");
  echo $csv;
  exit();
}
exit();

AjaxでPOSTした場合そのままファイルダウンロードはできないっぽい

上記で試してみた所、CSVファイルの生成自体は上手くいっているのですが、 Content-Disposition: attachmentCSVファイルをechoしてもダウンロードにはならずJS側に取得されるようです。

Ajaxで返されたデータをファイルにしてダウンロードさせる

Ajaxで返されたデータをファイルにしてDLさせる方法を模索します。
検索しているとだいたい2つのパターンに分かれるようでした。

  1. サーバーサイドで実ファイルを作成して、ファイルのパスを返しAjaxのdone内でwindow.location.href=file_pathとしてDLさせる
  2. Ajaxに返されたデータをBlob オブジェクト でファイル化してDLさせる

サーバーサイドに実ファイルを作成するのは、ファイル削除したり管理が面倒そうだったので、Blobでファイル化する方法を採用することにしました。

API側はCSVデータとファイル名をJSONで返すように変更します。
API (PHP)

<?php
// 略
function create_scvfile_by_array($data, $fileName = 'data.csv', $is_excel = false) {
  // 略
  header("Content-Type: text/javascript; charset={$charset}");
  echo json_encode([
    "csv" => $csv,
    "filename" => $fileName,
    "charset" => $charset,
  ]);
  exit();
}

JavaScript側はAjaxでデータが返されたあとの処理を追加します。
JS

function postJSONData(jsonData) {
  $.ajax({
    url: "api_url",
    type: "post",
    dataType: "json",
    data: {"form_data": jsonData}
  })
  .done(function(res) {
    console.log(res);
    const fileName = res.filename;
    const file = new Blob([res.csv], {
      type: `application/octet-stream; charset=${res.charset}`
    });

    if( window.navigator.msSaveOrOpenBlob ) {
      // IE
      navigator.msSaveBlob(file, fileName);
    } else {
      // window.location.href = URL.createObjectURL(file);
      // => DLできるけどファイル名の付け方が面倒そうだった。
      // ※ Blobの type: "text/csv;charset=utf-8" にすれば .csv 拡張子がついたファイルがDLされた
      const dlBtn = document.createElement('a')
      dlBtn.download = fileName;
      dlBtn.href = URL.createObjectURL(file);
      dlBtn.click();
    }
  })
  .fail(function(jqXHR, status, err) {
    console.log(status, err);
  });
}

HTML5を使えばaタグのdownload属性でファイル名を指定してDLさせることができます。(aタグ生成させる方法なら、IEで切り分ける必要無さそうな気もしますが...)
参考 HTML5 ファイルをダウンロードさせるリンクを作りたい。 - かもメモ

これでAjaxで受け取ったCSVファイルがダウンロードできるようになりました!

と、ここで完結すればよかったのですが...
データの中身が原因なのか不明な部分はありますが、Shift_JISに変換したCSVデータの入ったJSONが、Ajaxに戻ってきた時にパースエラーでエラーに流れてしまいました。(headerでcharsetも指定しているのですが...)

Shift_JIS 問題と戦う。

Shift_JIS対応ってのが、単に使ってる人がエクセルだとUTF-8が文字化けする(文字コードの概念を知らない)からって理由だけなのでShift_JISめ!! というかExcelめ!! という(# ゚Д゚) MSへのヘイトが高まる...

Ajaxで返されるデータの文字コードを指定してみる

AjaxbeforeSend 内でXMLHttpRequest.overrideMimeType文字コードを指定すれば文字化けが解消できるそうです。(ES2015なら直接XMLHttpRequest.overrideMimeTypeで良いんだろうけど)

Shift_JISに変更するチェックが有るときだけMineTypeを上書きするようにします。
JS

function postJSONData(jsonData) {
  $.ajax({
    url: "api_url",
    type: "post",
    dataType: "json",
    beforeSend: function(xhr) {
      const is_sjis = $('input[name="is_sjis"]').prop('checked');
      if( is_sjis ) {
        xhr.overrideMimeType("text/javascript; charset=shift_jis");
      }
    },
    data: {"form_data": jsonData}
  })
  .done(function(res) { /* 略 */})
  .fail(function(jqXHR, status, err) { /* 略 */})
}

こちらで試してみたのですが、Shif_JISに変換したCSVを組み込んだJSONだと相変わらずパースエラーになってしまいました。

APIからはCSVファイルだけを返すようにする

ファイル名をAPI側から渡したいという理由だけでJSONにしていたので、CSVのデータだけ返すようにしてしまえばShif_JISでもエラーにならないのでは?と思いAPIを元に戻すことに…

API (PHP)

<?php
// 略
function create_scvfile_by_array($data, $fileName = 'data.csv', $is_excel = false) {
  // 略
  header("Content-Type: text/csv; charset={$charset}");
  echo $csv;
  exit();
}

JavaScript側はJSONからファイル名や文字コードを取得していた部分を修正します
JS

function postJSONData(jsonData) {
  const const is_sjis = $('input[name="is_sjis"]').prop('checked');
  const charset = is_sjis? 'shift_jis' : 'utf-8';
  $.ajax({
    url: "api_url",
    type: "post",
    dataType: "text",
    beforeSend: function(xhr) {
      if( is_sjis ) {
        xhr.overrideMimeType("text/plan; charset=shift_jis");
      }
    },
    data: {"form_data": jsonData}
  })
  .done(function(res) {
    const fileName = "fileName.csv";
    const file = new Blob([res], {
      type: `application/octet-stream; charset=${charset}`
    });

    const dlBtn = document.createElement('a')
    dlBtn.download = fileName;
    dlBtn.href = URL.createObjectURL(file);
    dlBtn.click();
  })
  .fail(function(jqXHR, status, err) { /* 略 */})
}

こんかいはShift_JISに変換したデータでもAjaxでエラーにならずファイルがダウンロードされました!

... しかし ...

ダウロードされたファイルの中身がShif_JISになってない!! UTF-8CSVでしたw
原因は定かではない部分があるのですが、HTML・JS側の文字コードUTF-8なので、Ajaxに渡されたファイルがShif_JISでもUTF-8に解釈され直しているのではないかという推察です。

JavaScript文字コードを変換する。

字面にしただけで地雷そうな雰囲気がしているのですが、調べてみると先人がおられました。

とても大変そう…
ということで別の方法を模索することにしました。

解決 一方ロシアは鉛筆を使った作戦

JavaScript側で文字コードまで弄って頑張ってファイル生成をするのはtoo muchなように感じました。

Ajaxで受け取ったデータをファイルにしてDLさせる部分でaタグを生成してa.click()で動作させられるなら、JavaScriptでで、元のフォームデータをシリアライズ化したvalueを持ったinputタグをもった別のformを作ってform.submit()してしまえば、Ajaxすら不要で、JavaScript側でAjaxに渡されたデータからファイル作ってDLする機能すら不要になるのではないか?と閃いたのでした。
もともとサーバーの都合でmax_input_varsにかかってしまうと言うのがメインの問題だったのでJavaScriptで生成したformがそのまま送信できるのであれば、フロント側で頑張らなくても問題は解決する訳です!!
一方ロシアは鉛筆を使ったみたいなソリューション...

API側は当初のCSVを吐き出す状態に戻します
API (PHP)

<?php
if( isset($_POST)
 && isset($_POST['form_data'])
 && !empty($_POST['form_data']) ) {
  $data = [];
  // Shift_JIS化するかどうか
  // チェックボックスはチェックされないとそもそもデータが送られないのでデフォルト値を設定しておく
  $is_excel = false;
  // JSONを配列に戻す
  $postData = json_decode( $_POST['form_data'], true );
  // data[n][n] 文字列から連想配列に戻す
  foreach( $postData as $row ) {
    $key = $row['name'];
    $val = $row['value'];
    if( $key == 'is_sjis' ) {
      $is_excel = (bool) intval($val);
    } else {
      preg_match('/^data\[(\d+)\]\[(\d+)\]$/', $key, $matches);
      if( count($matches) == 3 ) {
        $dRow = intval($matches[1]);
        $dCol = intval($matches[2]);
        if( !isset( $data[ $dRow ] ) || !is_array( $data[ $dRow ] ) ) {
          $data[ $dRow ] = [];
        }
        $data[ $dRow ][$dCol] = $val;
      }
    }
  }
  if( count(data) > 0 ) {
    $fileName = "MY_DATA-" . date("Y-m-d") . ".csv"; 
    create_scvfile_by_array($data, $fileName, $is_excel);
  }
}

function create_scvfile_by_array($data, $fileName = 'data.csv', $is_excel = false) {
  $csv = '';
  foreach($data as $row) {
    if( is_array($row) ) {
      $csv .= join(',', $row);
    } else {
      $csv .= $row;
    }
    $csv .= "\n";
  }

  $charset = 'UTF-8';
  if( $is_excel ) {
    $csv = mb_convert_encoding($csv, 'SJIS', 'UTF-8');
    $charset = 'sjis-win';
  }

  header("Content-Type: application/octet-stream; charset={$charset}");
  header("Content-Disposition: attachment; filename={$fileName}");
  echo $csv;
  exit();
}
exit();

JS側はinputの数がmax_input_varsを超えそうなら新しいフォームを作成して、APIform.submit()するように変更します。

const maxInputVars = 1000;
const $form = $("#js-form");

function createNewFormAndPost(data) {
  cont $form = $('<form></form>')
       .attr('action', "api_url")
       .attr('method', 'post')
       .attr('enctype', 'multipart/form-data');
  cont $input = $('<input type="text" name="form_data">').val(data);
  $form.append($input).appendTo("body");
  $form.submit().remove();
}

$form.on('submit', function() {
  const postNum = $('input').length;
  if( (postNum + 50) >= maxPostVars ) {
    const params = JSON.stringify( $form.serializeArray() );
    createNewFormAndPost( params );
    return false;
  } else {
    // max_input_vars にかからそうな場合は通常にformを送信
    return true;
  }
});

これで試した所…
JavaScriptで生成したフォームをそのままJSでform.submitで送ることができ、Shift_JISCSVもそのままダウンロードですることができました!!
JSでフォーム生成してsubmitするのその内セキュリティの問題とかで規制されそうな気がしなくもないですが、とりあえず要件を満たすことが出来たので解決としました。

まとめ

すごい長い大作記事になったけど、ホント中身がありません。
文字コード問題はホント面倒なんで、Shift_JISとかEUC-JPとか滅んでUTFに統一されて欲しいって感想しか持てませんでした。JavaScriptのBlobオブジェクトでファイルが生成できるとか、頑張れば文字コードにも対応できるとか、JSで生成したaタグをクリックさせたり、form.submit()問題なく出来てしまうとか、色々と学びは有りました。 一つだけグローバル企業なMSがなぜ日本語版(?)のEXCELShift_JISを採用しているのかはとっても謎いです。(最近のはUTF-8で、このシステム使ってる人のEXCELが古いだけとかって可能性もありそうですが...


[参考]

Ajaxでファイルダウンロード

文字コード関係

Spreadsheetはイイゾ...