Posts for: #Javascript

svelte コンポーネントの実装は簡単

1時に寝て何度か起きて7時に起きた。日曜日の夜に業務スーパーへ行ったら生鮮系は売り切れているのが多かった。日持ちするようなものを購入した。呪術廻戦ゲーム の初心者ミッションをクリアしたのでゲームの時間を減らしていく。

kit/vite アプリケーションのデバッグ

先日の続き の続き。

ある kit アプリケーションの svelte コンポーネントから外部の kit アプリのコンポーネントやモジュールを埋め込むことができるかどうかを調査した。ドキュメントの Loading data をみながらコンポーネントを書いてみる。フロントエンドの開発はすべてメンバーに委譲しているので私はほとんど開発していない。ドキュメントみないとまったくどう実装していよいかわからない。

svelte コンポーネントをレンダリングするときにサーバー側で動かすのは +page.server.ts に、クライアント側で動かすのは +page.ts に実装する。今回の場合、外部の node.js プロセスに起動したサーバーに対してリクエストして index.html に相当するものを取得するのでサーバー側で取得したレスポンスから html を取り出して、それをコンポーネント側でレンダリングする。+page.server.ts は次のように実装する。

import type { PageServerLoad } from './$types';
import { apps } from '$lib/index';

export const load: PageServerLoad = async ({ params }) => {
	const res = await fetch(apps['kit-demo1'].entrypoint);
	const html = await res.text();
	return { html };
};

この html をクライアント側の +page.svelte から参照してレンダリングする。

<script lang="ts">
	import type { PageData } from './$types';
	export let data: PageData;
</script>

<div>{@html data.html}</div>

これで一応は意図した kit アプリケーションを埋め込むことはできるが、実際にはスクリプトなどはなにかが競合して動かないようだ。これは node.js から取得するスクリプトやスタイルなどが複数の kit アプリケーションで競合してしまうからではないかと推測する。

これが ssg ならば adapter-static を使ってビルドして、その成果物を static ディレクトリ配下に置くだけでそのまま動く。これは特別ななにかではなく、kit アプリケーションとして意図した振る舞いにはなる。これが出来て嬉しいことはあまり思いつかないが、想像した通りに動くかどうかの検証のために確認した。

次のリポジトリに調査した内容のサンプルコードを作った。ここまでの調査内容でまたテックブログを書いてみようと思う。

複数の kit アプリケーションを共存させる仕組みの考察

1時に寝て3時に起きて5時に起きて7時半に起きた。なんとなく布団に入らずにベッドの上でそのまま寝てた。それでもあまり寒くはなかった。

kit/vite アプリケーションのデバッグ

先日の続き の続き。

kit アプリケーションを kit アプリケーションに埋め込むといったことができないかどうかの調査をしている。いろいろ調べている中で kit の discussions でもそういった議論はいくつか行われている。マイクロフロントエンドというキーワードも出てくる。

これらの議論をみていても kit の ssr はそれ自体が1つのアプリケーションとして動かすことを前提にビルドされているため、kit アプリケーション内に別の kit アプリケーションを埋め込んだり、一部のコンポーネントを外部のアプリケーションと組み合わせて動かすことはなかなか難しいようにみえる。マイクロフロントエンドのような思想で設計されていない。しかし、既存のアプリケーションを動かしつつ、少しずつ kit アプリケーションへ移行するといった運用をしたいという世の中のニーズも根強いことが伺える。

ここで svelte.config.js でエントリーポイントを置き換えるぐらいはできる。デフォルトは / がエントリーポイントになるのを /myapp に置き換えるには次のように設定する。relative は es モジュールのインポートを相対パスで行うか、絶対パスにするかの設定も変更できる。これもデプロイ先のインフラの都合にあわせて調整できるようになっている。この設定を切り替えられるのだからエンドポイントをハックすること自体はそう難しくないのかもしれない。

kit: {
  paths: {
    relative: false,
    base: '/myapp'
  },
}

さらに調査していて、adapter-node を使ってビルドするとデフォルトでは polka というアプリケーションサーバーが起動するコードが生成される。

function polka (opts) {
	return new Polka(opts);
}

const path = env('SOCKET_PATH', false);
const host = env('HOST', '0.0.0.0');
const port = env('PORT', !path && '3000');

const server = polka().use(handler);

server.listen({ path, host, port }, () => {
	console.log(`Listening on ${path ? path : host + ':' + port}`);
});

ここで任意のアプリケーションサーバーを使いたいという issue があって、それに対する回答から adapter-node のドキュメントにカスタムサーバーについて書かれていることに気付く。

アダプタは、ビルドディレクトリにindex.jsとhandler.jsの2つのファイルを作成します。index.js を実行すると (デフォルトのビルドディレクトリを使用している場合は node ビルドなど)、設定されたポートでサーバが起動します。

あるいは、Express、Connect、Polka(あるいは組み込みのhttp.createServer)に適したハンドラをエクスポートするhandler.jsファイルをインポートして、独自のサーバをセットアップすることもできます。

handler.js さえインポートすればそのまま動くことはデバッグしていて知ってはいたのだけど、この自前のアプリケーションサーバーを hooks を使って起動すれば任意のサーバーに置き換えできると issue の中で回答されていた。kit アプリケーションは1つのサーバーが1つのシステムとして動かすことを前提に設計されているが、サーバーを複数起動することでそれらを共存できるのではないか?と考えた。検証のために node.js から子プロセスを生成するには次のようなコードで起動する。些事だけど adapter-node の生成したコードが shell を介しないとポート番号を設定できなかったので shell: true もセットしている。

import { spawn } from 'child_process';

export function start_server() {
  console.log('called start_server');
  const opts = {
    shell: true,
    env: {
      ...process.env,
      PORT: '3005',
        ORIGIN: 'http://localhost:5174',
      NODE_ENV: 'production'
    }
  };
  const node = spawn('node', ['apps/myapp/build/index.js'], opts);
  node.stdout.on('data', (data) => {
    console.log(data.toString());
  });

  node.stderr.on('data', (data) => {
    console.error(data.toString());
  });

  node.on('exit', (code) => {
    console.log(`Child exited with code ${code}`);
  });
}

この node.js の子プロセスを起動する処理を hooks で呼び出すことである kit アプリケーションを起動したときに、別の kit アプリケーションを提供するアプリケーションサーバーの node.js プロセスも起動できる。そしてパスを解決できるようにするため、さらに es モジュールのインポートパスにあわせたプロキシを実装する。

import { start_server } from '$lib/index';

start_server();

import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  if (event.url.pathname.startsWith('/myapp')) {
    if (event.request.method == 'GET') {
      return fetch('http://localhost:3005' + event.url.pathname);
    } else if (event.request.method == 'POST') {
      const data = await event.request.formData();
      const endpoint = 'http://localhost:3005' + event.url.pathname + event.url.search;
      return fetch(endpoint, { method: 'POST', body: data });
    }
  }
  const response = await resolve(event);
  return response;
};

これは kit のデモアプリが動くことを確認するためだけに実装したプロキシで GET/POST のリクエストを localhost:3005 に起動した node.js のプロセスへプロキシしている。これで2つの kit アプリケーションが1つのサーバーで共存しているかのように振る舞うことは確認できた。この延長上に私のやりたいことが実現できるかどうかをさらに調査する必要がある。

pure go の javascript 処理系を使ってみた

23時に寝て2時に起きてだらだらして寝て7時に起きた。テンカイチ 日本最強武芸者決定戦 という漫画を読んでた。

pure go の javascript 処理系

ユーザー定義スクリプトを実行する要件がある。システム管理者がスクリプトを書くなら javascript がいいかなと次のリファレンスを調べたりしていた。

これらを読んだ結果として dop251/goja がよいだろうと判断した。README だけ読めばすぐに実装できてめっちゃ簡単で感心した。こういうライブラリを私も会社のプロダクトとして作ってみたい。万人が使うものではなくても、必要なときがたまにあって、すごく使いやすいみたいな。こんな感じで goja の機能を使ってライブラリ実装してみた。

type FunctionCaller func(funcName string, args ...interface{}) (interface{}, error)

func NewFunctionCaller(script string) (FunctionCaller, error) {
	vm := goja.New()
	_, err := vm.RunString(script)
	if err != nil {
		return nil, err
	}
	return func(funcName string, args ...interface{}) (interface{}, error) {
		f, ok := goja.AssertFunction(vm.Get(funcName))
		if !ok {
			return nil, fmt.Errorf("failed to get function: %s", funcName)
		}

		funcArgs := make([]goja.Value, 0, len(args))
		for _, arg := range args {
			funcArgs = append(funcArgs, vm.ToValue(arg))
		}
		value, err := f(goja.Undefined(), funcArgs...)
		if err != nil {
			return nil, fmt.Errorf("failed to call javascript function: %w", err)
		}
		return value.Export(), nil
	}, nil
}

Is it goroutine-safe? によると、goroutine-safe ではないので毎回 js の runtime を生成する必要がある。用途によっては、実行したいスクリプトが大きくなるとオーバーヘッドを無視できない状況もあるかもしれない。うちの要件は小さいユーザー定義スクリプトを実行するだけなのでおそらくそれほど問題にはならないだろうという想定。