お知らせ

現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。
投稿日:
言語::JavaScript言語::HTML

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-Typemultipart/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')を利用する。$_POSTContent-Typeapplication/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);

// {}

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.jsontypemoduleにする
  • jest.config.jstransformIgnorePatternsにESMモジュールのパスだけ除外する設定を書く
  • 上記に加えてtransform.jsc.pathpkg-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の組み合わせであればテストはできるが肝心の実行ができず無意味だった

undefinedの判定方法が複数あるということでundefined判定の処理速度比較をしてみたのでその結果。

端的に言うと、hoge === undefinedtypeof hoge === 'undefined'の二方式がある。後者は原則考慮不要だが、言語仕様上存在しているので比較したが、現実的に見た場合、どちらで記述した場合でも処理速度に有意な差はないように感じた。

確認環境

Env Ver
Node.js 20.1.0
TypeScript 4.9.5
@swc/core 1.3.8

比較結果

hoge === undefinedの方が早く見えるが実行するタイミングで変わるので誤差の範疇だと思う。

方式 ms
hoge === undefined 4,514
typeof hoge === 'undefined' 4,515

確認コード

const tyof = (param?: string) => {
  return typeof param === 'undefined';
};

const undef = (param?: string) => {
  return param === undefined;
};

const tyStart = +new Date();
for (let i = 0; i < 10000000000; i++) {
  tyof();
}
console.log('typeof', +new Date() - tyStart);

const unStart = +new Date();
for (let i = 0; i < 10000000000; i++) {
  undef();
}
console.log('undefined', +new Date() - unStart);

TSから生成されたJS

"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
const tyof = (param)=>{
    return typeof param === 'undefined';
};
const undef = (param)=>{
    return param === undefined;
};
const tyStart = +new Date();
for(let i = 0; i < 10000000000; i++){
    tyof();
}
console.log('typeof', +new Date() - tyStart);
const unStart = +new Date();
for(let i = 0; i < 10000000000; i++){
    undef();
}
console.log('undefined', +new Date() - unStart);

あとがき

MDNを読む限りtypeof hoge === 'undefined'は該当変数が存在しない場合に有用なようであるが、TypeScriptで書いている場合、通常このようなコードが生まれることがなく、仮に起きるとした場合、次のようなコードになるため現実的に考慮する必要はない。なおMDNにも「こんなことはしないこと」と書いてあるので、一般的なコードでないことは客観的にも伺えるだろう。

(() => {
  const undefined = 123;
  const hoge = undefined;

  if (typeof hoge === 'undefined') {
    console.log('hoge is undefined');
  } else {
    console.log('hoge is not undefined');
  }
})();

上記コードの実行結果としてはhoge is not undefinedが出力される。

このコードの主な問題点

  1. const undefined = 123;というコードは予約語を変数名にしているため、混乱を招くコードであり、書かないことが好ましい
    1. MDNには予約語ではないとあるが、一般的には予約語の一つとして解釈して支障ないと考える
  2. このコードはESLintのeslint:recommendedで検知されるため、通常であれば書かれることはない
    1. no-shadow-restricted-namesに引っかかる

なお、このコードは例示のために即時実行関数形式で記述しているが、必要がない限りこの形式での実装は避けたほうが問題が少なくなると思う。これは不必要なネストが生まれたり、スコープの混乱を生むためである。

投稿日:
言語::JavaScriptジャンル::調査

関数の仮引数の書き方でオブジェクトの参照がどう変わるか気になったので試したメモ。別に仮引数でなくても分割代入なら何でも結果は同じになると思う。

これはプロジェクトのコーディング規約に定めがなく、無秩序な状態になっていると不具合を引き起こす因子になると思うので、どの方法を取るのか決めたほうが品質に寄与すると考える。個人的には引数由来であることが明示的であり、必ず参照を引きずるオブジェクト代入方式(後述)が好みだ。

結果

分割代入した結果プリミティブになると元オブジェクトとの参照が切れる

仮引数方式 参照
分割代入
スプレッド代入
オブジェクト代入

分割代入した結果、オブジェクトのままだと元オブジェクトとの参照が残る(Shallow copy)
再代入は参照が切れるので影響しない(再代入で参照アドレスが変わるためと思われる)

仮引数方式 参照
オブジェクト再代入
オブジェクト書き換え

確認コード

// 分割代入
const foo = ({ id, name }) => {
  id = 2;
  name = 'aaaa';
  console.log(id, name);
};

// スプレッド代入
const bar = ({ ...props }) => {
  props.id = 2;
  props.name = 'aaaa';
  console.log(props.id, props.name);
};

// オブジェクト代入
const baz = (props) => {
  props.id = 2;
  props.name = 'aaaa';
  console.log(props.id, props.name);
};

// オブジェクト再代入
const hoge = ({ id, name, dat }) => {
  id = 2;
  name = 'aaaa';
  dat = {
    value: 3,
  };
  console.log(id, name, dat);
};

// オブジェクト書き換え
const piyo = ({ id, name, dat }) => {
  id = 2;
  name = 'aaaa';
  dat.value = 3;
  console.log(id, name, dat);
};


const test1 = { id: 1, name: 'test' };
const test2 = { id: 1, name: 'test' };
const test3 = { id: 1, name: 'test' };
const test4 = { id: 1, name: 'test', dat: { value: 1 } };
const test5 = { id: 1, name: 'test', dat: { value: 1 } };


foo(test1);
console.log(test1);

bar(test2);
console.log(test2);

baz(test3);
console.log(test3);

hoge(test4);
console.log(test4);

piyo(test5);
console.log(test5);

結果

// 分割代入では呼び出し元が影響を受けていない
2 'aaaa'
{id: 1, name: 'test'}

// スプレッド代入では呼び出し元が影響を受けていない
2 'aaaa'
{id: 1, name: 'test'}

// オブジェクト代入では呼び出し元が影響を受ける
2 'aaaa'
{id: 2, name: 'aaaa'}

// オブジェクト再代入では呼び出し元が影響を受けていない
2 'aaaa' {value: 3}
{id: 1, name: 'test', dat: { value: 1}}

// オブジェクト書き換えでは呼び出し元が影響を受ける
2 'aaaa' {value: 3}
{id: 1, name: 'test', dat: { value: 3}}