24.3K Views
May 24, 24
スライド概要
2024.5.24 JavaScript祭りでの資料です
Webフロントエンドエンジニア
JavaScriptのProxy(Reflect) JavaScriptのProxy(Reflect)と 昨日より仲良くなる20分 2024.05.24 JavaScript祭り hiroko_ino ※掲載させていただいているOSSのコードは 発表現在のものです
自己紹介 猪野 浩子 / hiroko_ino 所属:RUN.EDGE株式会社 Web Front-end Engineer, Designe Vue.jsやFlutterでの開発や、UIデザインを時にはやっ たりしま 神奈川在 趣味はゲームのRTA
みなさんは JSのProxyを使っていますか? ?
今回のお話はプロキシサーバのことではありません
JavaScriptのProxy ECMAScript 2015から追加されたメタプログラミングを可能とする機 メタプログラミングとは、言語要素を実行時に操作するコードを記述するこ とである(メタプログラミングRuby 第2版より) Ruby on RailsのActive Recordなどもメタプログラミングの一 for/inループを使ってプロパティを巡回する機能すら、もっと静的な言語 に慣れている人にとってはメタとされる(JavaScript第7版 Proxy オブジェクトにより別なオブジェクトのプロキシーを作成することがで き、そのオブジェクトの基本的な操作に介入したり再定義したりすることができ ます。(MDNより)
MDNで紹介されているProxyの例 let obj = {}; let handler = { get: function (target, name) { return name in target ? target[name] : 42; }, }; let p = new Proxy(obj, handler); p.a = 1; console.log(p.a, p.b); // 1, 42
改めて質問 JavaScriptのProxyを使っていますか? ?
どこにあてはまりましたか?
この発表のアジェンダ JavaScriptのProxyとReflectについ Proxy in real worl 身近なライブラリ、FWにもProx まとめ
このスライドの想定読者 JavaScriptにはある程度精通しているつもりだけれど、Proxyについてはあまり 知らない Proxyについては概要は知っているが、実際の使われ方についてはあまり浮かば ない FWやライブラリでの使用例を知りたい方
知っている方も知らない方も… Proxy(とReflect)について簡単に !
const target = {
message1: "hello",
message2: "everyone",
};
const handler3 = {
get(target, prop, receiver) {
if (prop === "message2") {
return "world";
}
return Reflect.get(...arguments);
},
};
const proxy3 = new Proxy(target, handler3);
console.log(proxy3.message1); // hello
console.log(proxy3.message2); // world
const target = {
ターゲット
message1: "hello",
message2: "everyone",
};
ハンドラー(トラップ)
const handler3 = {
get(target, prop, receiver) {
if (prop === "message2") {
return "world";
}
return Reflect.get(...arguments);
},
};
const proxy3 = new Proxy(target, handler3);
console.log(proxy3.message1); // hello
console.log(proxy3.message2); // world
オブジェクトの保持している内部メソッドに対応するトラップ 内部メソッド [[GetPrototypeOf]] [[SetPrototypeOf]] [[IsExtensible]] [[PreventExtensions]] [[GetOwnProperty]] [[DefineOwnProperty]] [[HasProperty]] [[Get]] [[Set]] [[Delete]] [[OwnPropertyKeys]] 関数オブジェクトには[[Call]] : apply()、[[Construct]] : construct()もある 対応するトラップ getPrototypeOf() setPrototypeOf() isExtensible() preventExtensions() getOwnPropertyDescriptor() defineProperty() has() get() set() deleteProperty() ownKeys()
Reflect Reflectオブジェクトはクラスではありません。Mathオブジェクトと同じよう に、単に関連する関数をプロパティにまとめたものです(JavaScript第7版より 新しい機能ではなく便利な関数 Reflectの関数のセットはProxyのハンドラーのセットと一対 Reflect.has()の例 Reflect.has(Object, "assign"); // true
デフォルトの操作と同様のものを規定する const proxyObject = new Proxy(normalObject, { get(target, prop, receiver) { return Reflect.get(...arguments); }, set(target, prop, value, receiver) { return Reflect.set(...arguments); }, deleteProperty(target, prop) { return Reflect.deleteProperty(...arguments); }, apply(target, thisArg, argumentsList) { return Reflect.apply(...arguments); }, construct(target, args, newTarget) { return Reflect.construct(...arguments); } ...
Proxy in real world ?
Real world…? ?
実際どうですか?
直接的ではないが、フレームワークを通じて使っている Proxyを使うのは一般的にいい考えだと思わないが… 直接的には使っていない。immer.jsを通じて使っている
弊プロダクトに入ったコード(簡略例) !
Vue.use(VueI18n)
export const messages = { en, ja, specialJa: deepmerge(ja, special), specialEn: en, }
export const vueI18n = new VueI18n({ locale: ‘en’, fallbackLocale: [‘en’], messages,})
const _handler: ProxyHandler<Record<string, unknown>> = {
get(target: Record<string, unknown>, prop: string) {
if (prop === 'locale') {
const _locale = target.locale as string
let returnLocale = _locale
if (_locale === 'specialJa') { returnLocale = 'ja' }
if (_locale === 'specialEn') { returnLocale = 'en' }
return returnLocale
// ...関数だった場合の処理が入る
}}}
const i18n = new Proxy(
vueI18n as unknown as Record<string, unknown>, _handler,
) as unknown as VueI18n
export default function createMockClass( MockClass: new (addProps?: Partial<T> | undefined) => T, initialState: Partial<{ [key in keyof T]: T[key] }>, mockState: Partial<{ [key in keyof T]: T[key] }>, ) : T { const handler = { get(target: any, prop: string) { if (prop in mockState) { return mockState[prop as keyof T] } return target[prop] }, set(target: any, prop: string, value: any) { if (prop in mockState) { mockState[prop as keyof T] = value } target[prop] = value return true } } const _mockInstance = new MockClass(initialState) return new Proxy(_mockInstance, handler) }
その他の例は オープンソースのサービスから 拝借します !
supabase / apps / studio / __mocks__ / hooks.js
Jestのマニュアルモッ
useIsFeatureEnabledをモックしている
export const useIsFeatureEnabled = jest.fn().mockImplementation((arg) =>
typeof arg === 'string'
? true
: new Proxy(
{},
{
get() {
// Always return true, regardless of the property name
return true
},
}
)
)
supabase / apps / studio / hooks / misc / useIsFeatureEnabled.ts
function useIsFeatureEnabled<T extends Feature[]>(
features: T
): { [key in FeatureToCamelCase<T[number]>]: boolean }
function useIsFeatureEnabled(features: Feature): boolean
function useIsFeatureEnabled<T extends Feature | Feature[]>(features: T) {
const { profile } = useProfile()
if (Array.isArray(features)) {
return Object.fromEntries(
features.map((feature) => [
featureToCamelCase(feature),
checkFeature(feature, profile?.disabled_features),
])
)
}
}
return checkFeature(features, profile?.disabled_features)
const webhookEvents = {
ENTRY_CREATE: 'entry.create',
ENTRY_UPDATE: 'entry.update',
...
};
/**
* TODO V5: remove this file
* @deprecated
*/
const deprecatedWebhookEvents = new Proxy<WebhookEvents>(webhookEvents, {
get(target, prop: string) {
console.warn(
'[deprecated] @strapi/utils/webhook will no longer exist in the next major release of
Strapi. ' +
'Instead, the webhookEvents object can be retrieved from
strapi.webhookStore.allowedEvents'
);
return target[prop];
},
});
strapi / packages / core / utils / src / webhook.ts
grafana / packages / grafana-data / src / utils / makeClassES5Compatible.ts
/**
* @beta
* Proxies a ES6 class so that it can be used as a base class for an ES5 class
*/
export function makeClassES5Compatible<T extends abstract new (...args:
ConstructorParameters<T>) => InstanceType<T>>(
ES6Class: T
): T {
return new Proxy(ES6Class, {
// ES5 code will call it like a function using super
apply(target, self, argumentsList) {
if (typeof Reflect === 'undefined' || !Reflect.construct) {
alert('Browser is too old');
}
return Reflect.construct(target, argumentsList, self.constructor);
}})}
grafana / public / app / plugins / sdk.ts import { makeClassES5Compatible } from '@grafana/data'; import { loadPluginCss } from '@grafana/runtime'; import { MetricsPanelCtrl as MetricsPanelCtrlES6 } from 'app/angular/panel/ metrics_panel_ctrl'; import { PanelCtrl as PanelCtrlES6 } from 'app/angular/panel/panel_ctrl'; import { QueryCtrl as QueryCtrlES6 } from 'app/angular/panel/query_ctrl'; const PanelCtrl = makeClassES5Compatible(PanelCtrlES6); const MetricsPanelCtrl = makeClassES5Compatible(MetricsPanelCtrlES6); const QueryCtrl = makeClassES5Compatible(QueryCtrlES6); export { PanelCtrl, MetricsPanelCtrl, QueryCtrl, loadPluginCss }; 一般のリポジトリの使用例 import { MetricsPanelCtrl } from 'app/plugins/sdk' class HogeHogePanelCtrl extends MetricsPanelCtrl {
! 個人的には直観に反するものの、継承することは普通に可能 class Dog { constructor() { console.log('bow wow!') } } const ProxyClass = new Proxy(Dog, { apply(target, self, args) { console.log('this is my child!') return Reflect.construct(target, args, self.constructor) } }) class Corgi extends ProxyClass { constructor() { super() console.log('corgi!') } }
function decorateFlattenedWrapper(
hit: Record<string, unknown[]>,
metaFields: Record<string, string>
) {
return function (flattened: Record<string, unknown>) {
...
// Force all usage of Object.keys to use a predefined sort order,
// instead of using insertion order
return new Proxy(flattened, {
ownKeys: (target) => {
return Reflect.ownKeys(target).sort((a, b) => {
const aIsMeta = _.includes(metaFields, a);
const bIsMeta = _.includes(metaFields, b);
if (aIsMeta && bIsMeta) { return String(a).localeCompare(String(b));}
if (aIsMeta) { return 1; }
if (bIsMeta) { return -1; }
return String(a).localeCompare(String(b));
});
},
})}}
kibana / src / plugins / data_views / common / data_views / flatten_hit.ts
FW・ライブラリでの 使用例 !
見たことある? 左:ref 右:reactive
https://ja.vuejs.org/guide/extras/reactivity-in-depth#how-reactivity-works-in-vue
reactiveの擬似コード function reactive(obj) { return new Proxy(obj, { get(target, key) { track(target, key) return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) } }) }
track, triggerの疑似コード let activeEffect function track(target, key) { if (activeEffect) { const effects = getSubscribersForProperty(target, key) effects.add(activeEffect) } } function trigger(target, key) { const effects = getSubscribersForProperty(target, key) effects.forEach((effect) => effect()) }
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean,) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}
core / packages / reactivity / src / ref.ts
refのコードも面白いから見て(強欲)
更に深堀りたいならchibivueがオススメ 小さくVue.jsを作るブック https://ubugeeei.github.io/chibivue/
export function createProxyProxy<T extends Objectish>( base: T, parent?: ImmerState ):
Drafted<T, ProxyState> {
const isArray = Array.isArray(base)
const state: ProxyState = {
type_: isArray ? ArchType.Array : (ArchType.Object as any),
scope_: parent ? parent.scope_ : getCurrentScope()!,
modified_: false,
...
}
let target: T = state as any
let traps: ProxyHandler<object | Array<any>> = objectTraps
if (isArray) {
target = [state] as any
traps = arrayTraps
}
const {revoke, proxy} = Proxy.revocable(target, traps)
state.draft_ = proxy as any
state.revoke_ = revoke
return proxy as any
immer / src / core / proxy.ts
}
revocableの機能を実装している 前述のコードの一部 const {revoke, proxy} = Proxy.revocable(target, traps) state.draft_ = proxy as any state.revoke_ = revoke function revokeDraft(draft: Drafted) { const state: ImmerState = draft[DRAFT_STATE] if (state.type_ === ArchType.Object || state.type_ === ArchType.Array) state.revoke_() else state.revoked_ = true }
hono / src / client / client.ts
const createProxy = (callback: Callback, path: string[]) => {
const proxy: unknown = new Proxy(() => {}, {
get(_obj, key) {
if (typeof key !== 'string' || key === 'then') {
return undefined
}
return createProxy(callback, [...path, key])
},
apply(_1, _2, args) {
return callback({
path,
args,
})
},
})
return proxy
}
例:hc().posts.$post()
hono / src / client / client.ts
export const hc = <T extends Hono<any, any, any>>(
baseUrl: string,
options?: ClientRequestOptions
) =>
createProxy(function proxyCallback(opts) {
const parts = [...opts.path]
// allow calling .toString() and .valueOf() on the proxy
if (parts[parts.length - 1] === 'toString') {
if (parts[parts.length - 2] === 'name') {
// e.g. hc().somePath.name.toString() -> "somePath"
return parts[parts.length - 3] || ''
}
// e.g. hc().somePath.toString()
return proxyCallback.toString()
}
...valueOfのコードもあり
...
let method = ''
if (/^\$/.test(parts[parts.length - 1])) {
const last = parts.pop()
if (last) { method = last.replace(/^\$/, '') }
}
const path = parts.join('/')
const url = mergePath(baseUrl, path)
if (method === 'url') {
if (opts.args[0] && opts.args[0].param) {
return new URL(replaceUrlParam(url, opts.args[0].param))
}
return new URL(url)
}
if (method === 'ws') {
const targetUrl = replaceUrlProtocol(
opts.args[0] && opts.args[0].param ? replaceUrlParam(url, opts.args[0].param) : url,
'ws'
)
return new WebSocket(targetUrl)
hono / src / client / client.ts
} ...
const req = new ClientRequestImpl(url, method) if (method) { options ??= {} const args = deepMerge<ClientRequestOptions>(options, { ...(opts.args[1] ?? {}) }) return req.fetch(opts.args[0], args) } return req }, []) as UnionToIntersection<Client<T>> hono / src / client / client.ts
ここまで紹介してきましたが とはいえ…
プロキシを配置してもアプリの結果にそれほど影響しないように、プロキシと非プ ロキシ オブジェクトを区別できないようにするのが理想的です。これが、プロキ シ API にオブジェクトがプロキシかどうかをチェックする手段が含まれず、オブ ジェクトに対するすべての操作に対してトラップも提供されない理由の一つです。 (https://developer.chrome.com/blog/es2015-proxies?hl=ja) とくにアプリケーションでは、他の実装者がProxyの実装を意識しなくてもバグが起き ないようにしたほうが 用法用量を守って つかってね
参考になる資料:Proxyについて知りたいなら発表当時あたりの記事が正直一番詳しい [es6]research on ES6 `Proxy` https://gist.github.com/bellbind/8f33d81458dd454b430d4cd949076b30 ES2015 プロキシの概要 https://developer.chrome.com/blog/es2015-proxies?hl=ja Meta programming with ECMAScript 6 proxies https://2ality.com/2014/12/es6-proxies.html Proxy - JavaScript | MDN https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/ Global_Objects/Proxy
今日をきっかけにProxyに親近感を持ってくれるとうれし 使用は慎重にですが、「この場合Proxyが使えるかも?」と思い浮かべてくれると うれしい
ご清聴ありがとうございました! End.