LoginSignup
3
2

JavaScriptから使いやすいWebAPIをRPCでお手軽に

Last updated at Posted at 2022-11-07

JavaScriptから使いやすいWebAPIを考える回。
WebAPIといえばRESTが有名ですが、今回はRPCを実装してみます。

基本的な使い方

wfn.jsはローカル関数を実行するような感覚で、サーバ側の関数を実行できます。

基本的な使い方
import API from './wfn.js'

const result = await API.plus(1, 2) //足し算するAPI
//結果は3

ここでAPIのplusという関数が実行されてますが、これは幻です。
裏で関数名引数がHTTP送信されて、サーバ側で同名の関数が実行され、戻り値が返ってくる仕組みです。

基本的にGET送信ですが、関数名が大文字から始まるとPOST送信になります。

POST送信
const result = await API.Save(file) //ファイルを保存するAPI
//結果はtrue or false

以上がJavaScriptからWebAPIにアクセスする最善の書き方だと思います。
サーバ側も同名の関数定義+αで済むので、全体を通してシンプルで分かりやすいかと。

主な仕様

クライアント側の仕様

  • クライアント側は、APIのURLを1つ決めるだけの簡単設定
  • HTTPメソッドはGETPOSTの2つ。関数名の先頭が大文字だとPOSTになる
  • 引数には特別にファイルとform要素が渡せる

内部的な話

  • 関数名はPATH_INFOとして送る
  • 引数は個別にJSONにして、クエリパラメータ0=引数1&1=引数2のように送る
  • 送信方法はformと同じapplication/x-www-form-urlencoded
    • ファイルを送る時はmultipart/form-dataになる
    • 単純なリクエストなのでCORS時にプリフライトが発生しない
  • サーバからの戻り値はJSON
  • 応答コードが200以外なら例外が発生する

クライアント側のコード

クライアント側のコードは、APIのURLを設定するだけです。

wfn.js
const apiurl = 'http://localhost/wfn'

export default new Proxy(wfn, {get: (_,fn) => wfn.bind(fn)})

async function wfn(...args){
    const url    = new URL(apiurl + '/' + encodeURIComponent(this))
    const option = {credentials:'include'}

    if(this.match(/^[A-Z]/)){
        option.method = 'POST'
        option.body   = new FormData()
        args.forEach((v, i) => {
            if(v instanceof HTMLFormElement){
                v = Object.fromEntries(new FormData(v))
            }
            if(!(v instanceof Blob)){
                v = JSON.stringify(v)
            }
            option.body.append(i, v)
        }
    }
    else{
        args.forEach((v, i) => url.searchParams.set(i, JSON.stringify(v)))
    }

    const response = await fetch(url, option)

    if(response.status !== 200){
        throw response
    }

    return response.json()
}
  • 幻メソッドの仕組みはProxy

サーバ側のコード

サーバ側のコードは自作する必要がありますが、仕様は単純なので実装も簡単です。
言語はなんでもいいですが、参考までにPHPのサンプルを紹介。

wfn.php
class API{ // これが公開される関数
    static function plus(int $v1, int $v2): int{
        return $v1 + $v2;
    }

    static function Save(array $file): bool{
        return move_uploaded_file($file['tmp_name'], "./a.txt");
    }
}

function wfn(string $api){
    $fn     = basename($_SERVER['PATH_INFO']);
    $method = preg_match('/^[A-Z]/', $fn) ? 'POST' : 'GET'

    if($fn !== (new ReflectionMethod($api, $fn))->name){ //関数の存在と大文字小文字を確認
        throw new Exception('関数名の大文字小文字が違います');
    }
    if($method !== $_SERVER['REQUEST_METHOD']){
        throw new Exception('不正なメソッドです');
    }

    if($method === 'POST'){
        $args = [];
        for($i=0; $i<count($_POST)+count($_FILES); $i++){
            $args[] = $_FILES[$i] ?? json_decode($_POST[$i]);
        }
    }
    else{
        $args = array_map(fn($v) => json_decode($v), $_GET);
    }

    header('Content-Type: application/json');
    print json_encode( $api::$fn(...$args) );
}


try{
    wfn('API');
}
catch(Throwable $e){
    http_response_code(400);
}

セキュリティ面での話

  • 任意の関数が実行できるのが一番マズイので注意
    • 上記の例だと、実行できる関数は指定クラスの静的メソッドに限定している
  • POST時は許可オリジンか確認しましょう
  • 一般的な話ですが、ネットから送られてくるデータは信用できないので注意しましょう
  • [PHP限定] PHPは関数/メソッドの大文字小文字を区別しないので、きちんと確認する
  • [PHP限定] アップされたファイルは必ずmove_uploaded_file()を通してください

メモ

//複数送信の構想。Proxyのapplyを使う
API({関数1:[引数1]}, {関数2:[引数2]})

//引数に渡せるオブジェクトを汎用的に対処
v[Symbol.iterator]?.name === 'entries'
//ArrayとSetは'values'

//別サイトからのアクセスをブロック(same-origin, same-siteは許可)
if($_SERVER['HTTP_SEC_FETCH_SITE'] === 'cross-site' or $_SERVER['HTTP_SEC_FETCH_SITE'] === 'none'){ 
    exit;
}

//クロスオリジン対応。Cookieにも対応
if(isset($_SERVER['HTTP_ORIGIN'])){ //同一オリジンでGETの場合は送られてこない
    header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}"); //'cross-site'をブロックしていればこれでOK
    header("Access-Control-Allow-Credentials: true");
}

//is_ajax()
$_SERVER['HTTP_SEC_FETCH_DEST'] === 'empty'

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2