- 投稿日:
adiaryの改造でやったのでメモがてら
環境
Env | Ver |
---|---|
Microsoft Edge | 120.0.2210.144 |
PHP | 8.2.10 |
サンプルコード
FORM形式での送信
Web標準でサポートされているため実装が非常に容易。特に理由がなければこれでいい
HTMLコード
<textarea onpaste="handlePasteForm(event)"></textarea>
JSコード
fetch()
のヘッダ指定がないが、 fetch()
を使う場合はFormDataを使うと勝手に生えるので気にしなくていい。他にもXMLHttpRequest.send()
やNavigator.sendBeacon()
でも生えるらしい。
今回試したEdgeではバウンダリー文字列もちゃんと生えていた。
/** @param {ClipboardEvent} e */
const handlePasteForm = (e) => {
if (e.clipboardData.files.length) {
const pasteFile = e.clipboardData.files[0];
const form = new FormData();
form.append('image', pasteFile);
fetch('./test.php', {
method: 'POST',
body: form
});
} else {
// 何もしない
}
};
PHPコード
Content-Type
がmultipart/form-data
の時だけ中身が入ってくる
参考:https://www.php.net/manual/ja/reserved.variables.files.php
<?php
$file_name = $_FILES['image']['name'];
move_uploaded_file($_FILES['image']['tmp_name'], './'. $file_name);
>
$_FILES
の中身はvar_dumpした限りこんな感じだった
array(1) {
["image"]=>
array(6) {
["name"]=>
string(9) "image.png"
["full_path"]=>
string(9) "image.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(29) "C:\env\msys64\tmp\php3B90.tmp"
["error"]=>
int(0)
["size"]=>
int(3988)
}
}
JSON形式での送信
Web標準ではないので、これといったやり方もなく、正直面倒だが、どうしてもJSONで送らないといけないときに。
HTMLコード
<textarea onpaste="handlePasteJson(event)"></textarea>
JSコード
HTTPではバイナリを送ることができないため、Base64にデコードして送る。Base64デコードに含まれる情報はファイルバイナリのみ。もしFORM送信のようにファイル名も送りたければ、別途送ってやる必要がある。
/** @param {File} file */
const encodeBase64 = (file) => {
return new Promise ((resolve, reject) => {
const fr = new FileReader();
fr.onload = () => {
const data = fr.result.split(',')[1];
resolve(fr.result.split(',')[1]);
};
fr.onerror = (ev) => {
reject(ev);
};
fr.readAsDataURL(file);
});
};
/** @param {ClipboardEvent} e */
const handlePasteJson = async (e) => {
if (e.clipboardData.files.length) {
const pasteFile = e.clipboardData.files[0];
const base64File = await encodeBase64(pasteFile);
fetch('./test.php', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ image: base64File })
});
} else {
// 何もしない
}
};
PHPコード
JSONリクエストを取るときはfile_get_contents('php://input')
を利用する。$_POST
はContent-Type
がapplication/x-www-form-urlencoded
ないしmultipart/form-data
の時しか取れないからだ。つまりFORMリクエスト以外はfile_get_contents('php://input')
で取ると覚えておけばよいだろう。
参考:https://www.php.net/manual/ja/wrappers.php.php#wrappers.php.input
以下のコードはかなりいい加減なので余りアテにしてはいけない(特に拡張子取得の下りがガバガバすぎる)が、取り合えずpngかjpegかgifが送られてきた場合は動いてくれる筈だ。
<?php
// 連想配列にするため、json_decode()の第二引数をtrueにする(指定しないとstdClassになる
$json = json_decode(file_get_contents('php://input'), true);
// Base64エンコードを解除する
$file = base64_decode($json['image']);
// 一時ファイルに落とす
file_put_contents('json_upload', $file);
// 拡張子判別
$mime = mime_content_type('json_upload');
// 拡張子取得
$ext = explode('/', $mime);
// 拡張子付きにリネーム
rename('json_upload', 'image.'. $ext[1]);
参考
JSONでファイルを投げる方式での実装状況については以下の記事が参考になる。
WebAPI でファイルをアップロードする方法アレコレ #API - Qiita
一気通貫で動くサンプルコード
今までの実装が全部動かせるサンプル
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if(preg_match('/^multipart\/form-data;/', $_SERVER['CONTENT_TYPE'])) {
// form
$file_name = $_FILES['image']['name'];
move_uploaded_file($_FILES['image']['tmp_name'], './'. $file_name);
} else {
// json
$json = json_decode(file_get_contents('php://input'), true);
$file = base64_decode($json['image']);
file_put_contents('json_upload', $file);
$mime = mime_content_type('json_upload');
$ext = explode('/', $mime);
rename('json_upload', 'image.'. $ext[1]);
}
} else {
?>
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Upload test</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<script>
/** @param {File} file */
const encodeBase64 = (file) => {
return new Promise ((resolve, reject) => {
const fr = new FileReader();
fr.onload = () => {
const data = fr.result.split(',')[1];
resolve(fr.result.split(',')[1]);
};
fr.onerror = (ev) => {
reject(ev);
};
fr.readAsDataURL(file);
});
};
/** @param {ClipboardEvent} e */
const handlePasteForm = (e) => {
if (e.clipboardData.files.length) {
const pasteFile = e.clipboardData.files[0];
const form = new FormData();
form.append('image', pasteFile);
fetch('./test.php', {
method: 'POST',
body: form
});
} else {
// 何もしない
}
};
/** @param {ClipboardEvent} e */
const handlePasteJson = async (e) => {
if (e.clipboardData.files.length) {
const pasteFile = e.clipboardData.files[0];
const base64File = await encodeBase64(pasteFile);
fetch('./test.php', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ image: base64File })
});
} else {
// 何もしない
}
};
</script>
</head>
<body>
<div>Form:<textarea onpaste="handlePasteForm(event)"></textarea></div>
<div>Json:<textarea onpaste="handlePasteJson(event)"></textarea></div>
</body>
</html>
<?php
}
?>
- 投稿日:
EdgeとChromeは同じだったがNode.jsでは異なる挙動をしていたのでそのメモ
確認環境
Env | ver |
---|---|
Microsoft Edge | 120.0.2210.9 |
Google Chrome | 120.0.6099.130 |
Node.js | 20.0.0 |
環境別の確認結果
Edge
DevToolsで確認
const err = new Error('test');
Object.getPrototypeOf(err);
// {name: 'Error', message: '', constructor: ƒ, toString: ƒ}
Google Chrome
DevToolsで確認
const err = new Error('test');
Object.getPrototypeOf(err);
// {name: 'Error', message: '', constructor: ƒ, toString: ƒ}
Node.js
node -i
で確認
const err = new Error('test');
Object.getPrototypeOf(err);
// {}
- 投稿日:
TypeScriptで関数を書いているときに戻り値の型を書くケースがあるが、個人的にはあれは基本書かないほうがいいと思っているので、その理由を書いていく。
コード記述が冗長になる
まず型を書くと記述が冗長になる。以下のコードを見ると戻り値型が長く読みづらく、書くのも面倒だ。まずこんな長い命名をやめたほうが…というのはあれど、現実問題として長い命名は存在するので仕方ない。
export const getCampanyAndDepartmentAndEmployeeFromPrefectureCode = (
param: VeryveryLoooongestParamTypeNaming,
): VeryveryLoooongestReturnTypeNaming => {
return param.db.con.execQuery(
"SELECT * FROM foo WHERE id = :?",
param.dbParam.foo.id,
);
};
しかし以下であれば戻り値型がない分すっきりしていて見やすいし、書く手間も掛からない。更に型推論によって正しい型が返るので理想的だ。
export const getCampanyAndDepartmentAndEmployeeFromPrefectureCode = (
param: VeryveryLoooongestParamTypeNaming,
) => {
return param.db.con.execQuery(
"SELECT * FROM foo WHERE id = :?",
param.dbParam.foo.id,
);
};
実装と異なる戻り値型を暗黙的に記述できる
例えば以下のように書けば戻り値の型は'foo' | 'not foo'
となり、常に正しい型が返る。
export const foo = (isFoo: boolean) => {
return isFoo ? 'foo' : 'not foo';
};
しかし以下のように戻り値の型を定義すると実装上存在しない'bar'
が返る。これは実装時に無用な混乱を生む。一般的にこのようなケースは仕様削除やリファクタなどで生まれることがあると思うが、そういうメンテ漏れにもなるので書かないほうが安全だといえる。
export const foo2 = (isFoo: boolean): 'foo' | 'not foo' | 'bar' => {
return isFoo ? 'foo' : 'not foo';
};
他にも次のケースでは戻り値がstring
となり、何が返ってくるのかが実装を見ないと解らなくなる。特に理由がないなら書かないほうがよい。
// 'foo' | 'not foo'になるはずだがstring扱いになる
export const foo3 = (isFoo: boolean): string => {
return isFoo ? 'foo' : 'not foo';
};
型記述が混乱する
ここは基本的に前項の内容と重複する内容となる。
例えば次の二つの実装は同じだが、戻り値の型だけが異なる。こういう実装が混在すると実装の一貫性が失われ無用な混乱を生むので、指定しないほうが望ましい。
const IndexPage = (): JSX.Element => {
return (
<Layout title={'Hello Next.js'}>
<>
<h1>Hello Next.js 👋</h1>
</>
</Layout>
);
};
const IndexPage2 = (): ReactElement => {
return (
<Layout title={'Hello Next.js'}>
<>
<h1>Hello Next.js 👋</h1>
</>
</Layout>
);
};
書いてもよいと思うケース
例えば依存関係を持たせたい時など、インターフェースとして型を共通化したい場合は書いてもよいと思う。これはどこでそれを使うのかが自明になるからだ。改修時にも型によって関連処理が見出しやすくなるので意識しやすくなる。
export const createPostMessage = (
channel: string,
username: string,
message: string,
): PostMessage => {
return {
channel,
username,
message,
};
};
export const postMessage = async (param: PostMessage) => {
try {
return await fetch('https://example.com/api/postMessage', param);
} catch (err) {
return err;
}
};
但しこのようなケースではUnit Testを書いて、実装された戻り値型を満たす値が返ることを確認するのが望ましい。
戻り値型を書かないことによるデメリット
TypeScriptの公式リポジトリによると、型推論の速度に悪影響を及ぼすとあるので、型推論の速度が落ちるという点だ。
もし型推論の速度が非常に遅いと感じた場合は書いてみてもよいと思うが、公式でも以下のように案内があり、和訳すると「型推論は非常に便利なので、これを普遍的に行う必要はありませんが、コードの遅いセクションを特定した場合に試してみると便利です。」とあるので、余程複雑なことをしていない限り不要だとは思うし、そんな複雑な型を返すような処理は必要がなければ書かないほうがいいだろう。
Type inference is very convenient, so there's no need to do this universally - however, it can be a useful thing to try if you've identified a slow section of your code.
少なくとも私は実務上、型推論の速度に困ったことがないのと、tsc
でビルドすることも稀であるため、ビルドに影響することもない。よって基本書いていない。
- 投稿日:
ローカルで動作するNode.jsのライブラリ(node_modules
)欲しくないですか?欲しいですよね?という訳で作ってみました。
要件としてはTypeScriptで実装出来、Jestでテスト可能で、ESLintでLintが可能というところです。
確認環境
Env | Ver |
---|---|
@swc/cli | 0.1.62 |
@swc/core | 1.3.93 |
@swc/jest | 0.2.29 |
@types/jest | 29.5.5 |
@types/node | 20.8.6 |
@typescript-eslint/eslint-plugin | 6.7.5 |
@typescript-eslint/parser | 6.7.5 |
eslint | 8.51.0 |
eslint-config-prettier | 9.0.0 |
eslint-plugin-jest | 27.4.2 |
jest | 29.7.0 |
jest-watch-typeahead | 2.2.2 |
prettier | 3.0.3 |
ts-jest | 29.1.1 |
typescript | 5.2.2 |
Node.js | 20.8.0 |
npm | 10.1.0 |
サンプル実装
https://github.com/Lycolia/ts-library-example
実装について
フォルダ構成
monorepoでmainからlibrary/singleやlibrary/multi/hoge, piyoを参照するような構成です。
root
└─packages
├─library // ライブラリ側
│ └─packages
│ ├─multi // 複数ファイルのライブラリ
│ │ └─src
│ │ └─utils
│ │ ├─hoge
│ │ └─piyo
│ └─single // 単一ファイルのライブラリ
│ └─src
└─main // ライブラリを使う側
└─src
└─libs
実装時のポイント
これが全てというわけではないと思いますが、一旦今回作ったもののポイントを解説していきます。利用側がtsc
を利用しない場合、ライブラリ側と利用側で構成は別々になります。
またこの実装はエディタにVSCodeを利用し、import
するパスが相対パスであることを前提に説明しています。
ライブラリ側のポイント
ビルドに関して
まずビルドに関してはtsc
でやります。
これはTypeScriptで開発するためにはビルド成果物として.d.ts
ファイルが必要になるのと、Jestを通すためにビルド成果物がCommonJS形式(以下CJS)である必要があるためです。CJSが吐けるなら何でもいいとは思いますが、.d.ts
も必要になるので、tsc
を使うのが無難な選択肢だと思います。
テストファイルを出力したくないので、tsconfig.json
はビルドと開発で分けます。
またtsconfig.json
は各ワークスペースのルートに置いて置く必要があります。これはTypeScriptがtsconfig.json
のパスを起点として動作するためです。
ビルド用の設定ポイントとしては以下の通りです。
"compilerOptions"
"module": "NodeNext"
- これがないとパス解決が上手く行きません
"moduleResolution": "NodeNext"
- これがないとパス解決が上手く行きません
"declaration": true,
.d.ts
の出力に使います
"outDir": "./dist",
- ビルド成果物の出力先です
"exclude": ["src/**/*.spec.ts"]
- ビルド時にテストファイルは不要なので無視します
"include": ["src/**/*"]
- 参照するソースコードです
Jestに関して
ビルドにtsc
を使うため、Jestのローダーとしてもts-jestを利用します。公式ではbabelが推奨されているようですが、構成方法が不明だったので諦めました。BabelはJest公式の案内通りにやっても多分上手く行きません。
jest.config.js
には以下の設定を追加します。
preset: 'ts-jest'
開発用の設定について
開発とビルドでtsconfig.json
を分けるので、開発用のも必要です。これはビルド用から"exclude": ["src/**/*.spec.ts"]
を抜くだけです。
ライブラリを外部参照させる方法
基本的にはpackage.json
に外部参照させるための定義を書くことによって行います。
この辺りの仕様はdocs.npmjs.comにはなく、nodejs.orgにあります。
単一ファイルの外部参照
単一ファイルを外部参照(import出来るように)するときに使う手法です。
import
をimport { hoge } from '@my-lib/example'
みたいな書き方をしたい時に必要になるやつです。
以下の様にビルド成果物の.js
のパスをpackage.json
に追加してやると出来るようになります。
"main": "./dist/index.js",
以下の書き方でも同様に可能です。
"exports": {
".": {
"default": "./dist/index.js"
}
},
上記には他にtypes
というフィールドがあり、本来ここに.d.ts
を追加するのですが、TypeScriptが解決してくれるので、なくても動きます。一応ない場合はファイル探索を行うようなので、あった方が少しだけパフォーマンスが上がるかもしれません。
複数ファイルの外部参照
複数ファイルを外部参照(import出来るように)するときに使う手法です。
基本的に何もしなくてよいですが、import { hoge } from '@my-lib/example'
みたいな書き方もしたい場合は以下の記述が必要です。
"main": "./dist/core/index.js"
ここで指定していないものはimport { hoge } from '@my-lic/example/dist/hoge'
みたいにして参照します。dist
がダサくて嫌な場合は適当な名前に変えます。
参考までに@actions/githubはバージョン6.0.0時点でdist
に相当する部分をlib
にしており、import { Context } from '@actions/github/lib/context';
の様にして参照するようになっています。
exports
を使ってdist
を隠すことも出来るとは思うのですが面倒なので試してません。
ライブラリ側を利用する側のポイント
今回利用する側はビルドにswcを使う想定ですが、たぶんなんでも動くと思います。参考までにswc-loader + webpackでも動きました。
開発用の設定について
tsconfig.json
に以下の設定があれば恐らく最低限大丈夫だと思います。
"compilerOptions"
"module": "NodeNext"
- これがないとパス解決が上手く行きません
"moduleResolution": "NodeNext"
- これがないとパス解決が上手く行きません
"include": ["src/**/*"]
- 参照するソースコードです
ライブラリのインストール方法について
以下のように相対パスを指定するとインストールできます。
npm i ../package/library/package/single
消すときはパッケージ名を指定すれば消せます。
npm un @lycolia/library-example-single
試したけどダメだったやつら
色々してる過程で試行錯誤した名残。
ライブラリ側のJestでbabelを使おうとやったこと
以下の二通りの設定は試しましたが、どっちもダメだったので諦めました。何よりtscを使うならts-jestの方が楽なのは確定的に明らかですし…。
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
presets: [
['@babel/preset-env', {targets: {node: 'current'}, modules: 'commonjs'}],
'@babel/preset-typescript',
],
ライブラリ側をswcでビルド
.swcrc
を以下の設定にしても
"module": {
"type": "commonjs"
},
以下の出力がされるため、jest.spyOn()
が上手く動かない(get
が邪魔でhello
が見れない
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "hello", {
enumerable: true,
get: function() {
return hello;
}
});
const hello = (param)=>{
console.log(param);
};
どの道これでは.d.ts
が出せないので、あんま意味ないなと…。
参考にしたもの
実装方法は@actions/githubが一番単純で参考になると思います。
- 仕様
- 実装
- Jest周り
実装はビルド成果物とビルド前のコード、package.json
やtsconfig.json
辺りがどうなっているのかを参考にしました。
- 投稿日:
ESM化が叫ばれて久しいですが、未だにJestはESMとCJSが混在したコードを処理してくれません。
Getting StartedにもSWCに対する言及がないので、きっともう忘れられているのでしょう。swc-project/jestの方も特にやる気はなさそうだし、やりたければ自分でPR書きましょうって感じだと思います。きっと。
確認環境
node_modules配下にESMで作られたモジュールが存在し、コードはTypeScript、トランスパイルにはSWCを利用する。
Env | Ver |
---|---|
@swc/cli | 0.1.62 |
@swc/core | 1.3.92 |
@swc/jest | 0.2.29 |
@swc/register | 0.1.10 |
jest | 29.7.0 |
typescript | 5.2.2 |
やったけど意味がなかったこと
package.json
のtype
をmodule
にするjest.config.js
のtransformIgnorePatterns
にESMモジュールのパスだけ除外する設定を書く- 上記に加えて
transform.jsc.path
にpkg-name: ['node_modules/pkg-name']
を追記する node --experimental-vm-modules node_modules/jest/bin/jest.js
で実行する- 多少マシになったがコケるものはコケる
- ESMで書かれたモジュールを丸ごとモックする
- 一切効果なし
所感
多分Webpackでバンドルしてnode_modules
の中身も外も関係ない状態にするのが一番無難なのではないかと思いました。
Node.jsの組み込みテストランナーにすれば解決するかな?と思ったものの、こちらは現状SWCでは使えそうにないので諦めました。
参考までに以下のコマンドで走らさせられます。
node --require @swc/register --test ./src/**/*.spec.ts
取り敢えずESMに引っかったモジュールはCJS時のバージョンを維持しておくことにしましたが、このままだとSWC使えないし、なんとかなって欲しいですね。Webpack使えば解決できるのはわかるんですが、このために使いたくないので、テストを重視する場合、Vitestを持つViteが有力候補になって来そうです。
2024-02-17追記
esbuild + Node.js built-in test runnerの組み合わせであればテストはできるが肝心の実行ができず無意味だった