知識(shí)點(diǎn)
- window.postMessage 跨頁(yè)消息
- Promise 使用技巧 async/await
- 保護(hù)數(shù)據(jù)的三種方式和應(yīng)用場(chǎng)景 defineProperty,Object.freeze 和 Proxy
- es5中的類(lèi) 和 繼承
- rpc 實(shí)現(xiàn)原理
- 其他知識(shí)點(diǎn)
參數(shù)歸一化,解構(gòu),展開(kāi),惰性函數(shù),立即執(zhí)行函數(shù),箭頭函數(shù),閉包,async/await
2. 原始設(shè)計(jì)
參考RPC框架的思路,設(shè)計(jì)一個(gè)前端跨頁(yè)通訊組件 LinkCom - 領(lǐng)航

3. 類(lèi)模塊設(shè)計(jì)(UML)

4. 時(shí)序圖

5. 類(lèi)模塊功能解釋
提示:后續(xù)內(nèi)容中示例代碼目的是展示核心邏輯,會(huì)刪除例如參數(shù)校驗(yàn),文檔注釋等代碼
5.1 幫助函數(shù)
5.1.1 createId
const id = createId();
用于創(chuàng)建一個(gè)隨機(jī)id
const createId = (function () {
let num = 0;
return () => (++num) + ":" + Math.random().toString(36).substring(2);
})();
- 名詞解釋
閉包:是前端開(kāi)發(fā)中一種保護(hù)私有參數(shù)的手段,但是如果濫用可能會(huì)引起內(nèi)存泄露
立即執(zhí)行函數(shù):它是一種設(shè)計(jì)模式,也被稱為 自執(zhí)行匿名函數(shù),它可以有效的避免污染全局命名空間
5.1.2 freezeDeep
freezeDeep(obj)
用于深度凍結(jié)對(duì)象 普通的對(duì)象凍結(jié)只能保證當(dāng)前對(duì)象不被修改,但無(wú)法保證對(duì)象包含的子對(duì)象也無(wú)法修改 使用遞歸的方式將對(duì)象和對(duì)象中的屬性都凍結(jié)掉
const freezeDeep = (function () {
function freeze(obj) {
Object.freeze(obj);
for (const key in obj) {
if (typeof obj[key] === "object" && obj[key] != null) {
freeze(obj[key]);
}
}
}
return (obj) => {
freeze(obj);
return obj;
};
})();
- 名詞解釋
對(duì)象凍結(jié):這項(xiàng)功能一般用在需要保護(hù)的數(shù)據(jù)對(duì)象上,被凍結(jié)的對(duì)象將無(wú)法修改對(duì)中的任何屬性,也無(wú)法追加屬性,如果是數(shù)組,也同樣無(wú)法向數(shù)組中添加元素,對(duì)象凍結(jié)可以有效防止數(shù)據(jù)被其他代碼篡改。
5.1.3 log(message, ...args)
log && log("打印日志");
用于打印日志
let log = function (message, ...args) {
if (window.location.hostname === "localhost") {
log = function (message, ...args) {
if (message instanceof Error) {
console.trace();
}
console.log(`${window.document.title} - ${new Date().toLocaleTimeString()} LinkCom.${message}`, ...args);
};
log(message, ...args);
} else {
log = null;
}
};
- 名詞解釋:
惰性函數(shù):這是一種開(kāi)發(fā)技巧,表示函數(shù)執(zhí)行的分支只會(huì)在函數(shù)第一次調(diào)用的時(shí)候判斷,在第一次調(diào)用過(guò)程中,該函數(shù)會(huì)被覆蓋為另一個(gè)按照合適方式執(zhí)行的函數(shù),這樣任何對(duì)原函數(shù)的調(diào)用就不用再經(jīng)過(guò)執(zhí)行的分支了
參數(shù)展開(kāi):在這個(gè)例子中參數(shù)...args表示在調(diào)用時(shí),第1個(gè)之后的參數(shù)會(huì)被包裝為一個(gè)數(shù)組,例如調(diào)用log(1,2,3)后message=1,args=[2,3];參數(shù)展開(kāi)還有更多用法,可以自行查閱文檔
5.2 幫助類(lèi)
5.2.1 信號(hào)量 Semaphores
const ss = new Semaphores(); ss.resolve("完成");
表示一個(gè)信號(hào)量, 繼承自 Promise, 自身包含 resolve 和 reject 函數(shù), 可以直接解決或拒絕 Promise
class Semaphores extends Promise {
constructor(...args) {
if (args.length === 0 || (args.length === 1 && typeof args[0] === "object")) {
const hander = Object.assign({}, args[0]);
super((resolve, reject) => {
hander.resolve = resolve;
hander.reject = reject;
});
Object.assign(this, hander);
freezeDeep(this);
} else {
super(...args);
}
}
}
- 名詞解釋:
Promise:通常用來(lái)表示一個(gè)可以由人為控制的異步操作,可以通過(guò)主動(dòng)調(diào)用
Promise構(gòu)造函數(shù)中的resolve和reject來(lái)結(jié)束異步等待;
調(diào)用resolve并傳入一個(gè)任意對(duì)象表示異步完成,調(diào)用reject并傳入一個(gè)Error表示異步操作出現(xiàn)異常。
同時(shí)Promise還可以由[async/await](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function)關(guān)鍵觸發(fā)異步等待操作。
5.2.2 異步隊(duì)列 AsyncQueue
const queue = new AsyncQueue(); queue.push(123); const value = await queue.shift();
表示一個(gè)先進(jìn)先出的異步隊(duì)列; 使用push(data)存入數(shù)據(jù), 使用異步方法async shift()取出數(shù)據(jù), 當(dāng)沒(méi)有數(shù)據(jù)時(shí), 會(huì)等待, 直到數(shù)據(jù)被push方法存入
由于 class 的私有屬性兼容性不好, 希望對(duì)外隱藏list的操作,所以使用閉包代替class
首先初始化會(huì)在內(nèi)部列表中放入一個(gè)等待中的信號(hào)
-
push:獲取到列表中最后一個(gè)信號(hào),并將其標(biāo)識(shí)為已解決,然后將一個(gè)新的等待中信號(hào)放入列表尾部 -
shift:等待列表中一個(gè)信號(hào)被處理(解決或拒絕),然后將信號(hào)從列表第一位移除,這里需要注意,shift可以多次調(diào)用,可能存在多個(gè)等待中的操作,當(dāng)?shù)谝粋€(gè)等待結(jié)束后,會(huì)將信號(hào)移除,之后的等待操作結(jié)束必須判斷目前列表第一位的數(shù)據(jù)是否還是等待前取出的信號(hào),如果不是,則說(shuō)明已經(jīng)由其他操作移除了,不用再進(jìn)行任何操作 -
destroy:銷(xiāo)毀列表時(shí),向所有列表中的信號(hào)發(fā)送拒絕,并附上原因
function AsyncQueue() {
const list = [new Semaphores()];
this.shift = async () => {
const first = list[0];
const data = await first;
if (first === list[0]) {
list.shift();
}
return data;
}
this.push = data => {
const last = list[list.length - 1];
last.resolve(data);
list.push(new Semaphores());
}
this.count = () => list.length;
this.destroy = reason => {
const err = reason instanceof Error ? reason : new Error(reason || "queue is destroyed");
list.forEach(x => x.reject(err));
list.length = 0;
}
}
- 名詞解釋:
參數(shù)歸一化:是將不同類(lèi)型的數(shù)據(jù)變?yōu)橄嗤?lèi)型,以方便后續(xù)處理的一種技巧
例如:const err = reason instanceof Error ? reason : new Error(reason || "queue is destroyed");
以上操作就是無(wú)論reason現(xiàn)在是什么類(lèi)型,都轉(zhuǎn)為Error類(lèi)型,后續(xù)操作只需要操作Error就可以了
5.2.3 任務(wù)板 TaskBoard
const tasks = new TaskBoard(); tasks.put(message.id, new Semaphores({ message, targetToken: handler.targetToken }), timeout); tasks.finish(message.id, message.result);
表示一個(gè)任務(wù)板, 任務(wù)為 Semaphores 類(lèi)型
每個(gè)存入的任務(wù)都有一個(gè)超時(shí)時(shí)間, 超時(shí)后自動(dòng)刪除, 并將 Semaphores 解決為錯(cuò)誤
每個(gè)存入的任務(wù)可在超時(shí)前被完成(完成可以是解決或拒絕), 完成后自動(dòng)刪除
同樣的為了隱藏 tasks 使用閉包
- 方法:
-
put:添加任務(wù)時(shí),構(gòu)造一個(gè)setTimeout用于控制超時(shí)時(shí)間,超時(shí)后直接調(diào)用完成方法傳入Error -
finish:完成任務(wù)方法中判斷結(jié)果值,如果是Error執(zhí)行reject -
take:取出任務(wù)的時(shí),將任務(wù)從tasks中刪除,并刪除定時(shí)器
-
function TaskBoard() {
const tasks = {};
this.put = (id, task, timeout) => {
const timeoutId = setTimeout(() => this.finish(id, new Error("timeout")), timeout);
tasks[id] = { content: task, timeoutId };
};
this.take = id => {
const data = tasks[id];
clearTimeout(data.timeoutId);
delete tasks[id];
return data.content;
};
this.has = id => id in tasks;
this.count = () => Object.keys(tasks).length;
this.clear = () => Object.keys(tasks).forEach(id => this.take(id));
this.get = id => this.has(id) ? tasks[id].content : null;
this.finish = (id, result) => {
const task = this.take(id);
const exec = result instanceof Error ? task.reject : task.resolve;
exec(result);
};
}
5.3 消息類(lèi)
要實(shí)現(xiàn)類(lèi)似RPC的通信功能,首先要設(shè)計(jì)的就是消息類(lèi)結(jié)構(gòu) 消息類(lèi)分為 請(qǐng)求(RequestMessage)和響應(yīng)(ResponseMessage) 兩種類(lèi)型的消息會(huì)有一些共同的特點(diǎn),所以抽象出消息基類(lèi)(Message)
5.3.1 消息基類(lèi) Message
消息作為一個(gè)DTO(Data Transfer Object 數(shù)據(jù)傳輸對(duì)象),創(chuàng)建后就不應(yīng)再被更改,所以執(zhí)行freezeDeep將其保護(hù)起來(lái) 考慮到繼承的問(wèn)題,子類(lèi)也無(wú)法對(duì)其進(jìn)行更改,所以在構(gòu)造函數(shù)中需要傳入子類(lèi)的屬性,由基類(lèi)執(zhí)行屬性綁定操作
-
屬性:
-
type:消息類(lèi)型 ("request"或"response") -
token:用于標(biāo)識(shí)發(fā)送消息的程序的身份 -
id:每個(gè)請(qǐng)求消息都會(huì)生成唯一的id,而響應(yīng)消息的id取自請(qǐng)求消息id,表示響應(yīng)的是哪個(gè)請(qǐng)求 -
*:任意擴(kuò)展屬性,用于綁定子類(lèi)屬性
-
-
方法:
-
buildMessage():構(gòu)建可用于發(fā)送的消息體,默認(rèn)實(shí)現(xiàn)Object.assign({}, this);可由子類(lèi)重寫(xiě) -
static parse(Object):靜態(tài)方法,用于將任意對(duì)象轉(zhuǎn)為Message
-
class Message {
constructor(token, type, id, props) {
Object.assign(this, props, { token, type, id });
freezeDeep(this);
}
buildMessage(){
return Object.assign({}, this);
}
static parse(message) {
const { token, type, id } = message;
return new Message(token, type, id, message);
}
}
- 名詞解釋:
類(lèi) :前端 ES5 之后也提供了與后端類(lèi)似的
class聲明的方式,語(yǔ)法和代碼更為純粹
5.3.2 請(qǐng)求消息 RequestMessage
const message = new RequestMessage(handler.token, "call", { method, args })
請(qǐng)求消息,用于 主動(dòng)調(diào)用方 發(fā)給 被動(dòng)接收方 時(shí)的消息
- 屬性:
-
token:生成消息的處理程序token,用于接收方對(duì)消息進(jìn)行來(lái)源認(rèn)證 -
command:消息命令,用于接收方確定使用何種方式處理 -
datadata:消息數(shù)據(jù)
-
class RequestMessage extends Message {
constructor(token, command, data) {
super(token, "request", createId(), { command, data });
}
}
- 名詞解釋
繼承:extends 關(guān)鍵字用于表示類(lèi)之間的繼承關(guān)系,在構(gòu)造函數(shù)中使用
super關(guān)鍵字來(lái)調(diào)用父類(lèi)的構(gòu)造函數(shù)
5.3.3 響應(yīng)消息 ResponseMessage
const message = new ResponseMessage(handler.token, id, result);
響應(yīng)消息,用于 被動(dòng)接收方 收到消息并處理完成后將返回值發(fā)送給 主動(dòng)調(diào)用方 時(shí)的消息
- 屬性:
-
id:請(qǐng)求消息id,用于調(diào)用方確定當(dāng)前響應(yīng)是回復(fù)哪一個(gè)請(qǐng)求消息 -
result:響應(yīng)結(jié)果
-
class ResponseMessage extends Message {
constructor(token, id, result) {
super(token, "response", id, { result });
}
}
5.4 消息處理程序 MessageHandler
const handler = new MessageHandler(methods, url, window);
handler.onmessage = (message, event) => { };
handler.send(message);
handler.apply(data.method, handler, [...data.args, event]);
提供最基礎(chǔ)的發(fā)送消息方法和接收消息回調(diào)
處理程序與Message不同,并不是所有屬性都是凍結(jié)的 其中targetUrl,targetUrl,targetWindow都需要后期設(shè)置,但設(shè)置時(shí)需要驗(yàn)證值的類(lèi)型,所以使用屬性劫持(defineProperty)來(lái)處理,其中targetUrl也使用了參數(shù)歸一化的技巧,將String和URL都轉(zhuǎn)為URL,方便后續(xù)使用;
- 屬性:
-
token:消息處理程序token,該屬性自動(dòng)生成只讀,身份標(biāo)識(shí),在發(fā)送消息時(shí)需要設(shè)置到 -
Message,用于接收方識(shí)別身份 -
methods:注冊(cè)為允許遠(yuǎn)程調(diào)用方法 -
targetToken:目標(biāo)token,該屬性用于接收消息時(shí)驗(yàn)證消息來(lái)源 -
targetUrl:目標(biāo)url -
targetWindow:目標(biāo)window
-
- 函數(shù):
-
send(Message):發(fā)送消息 -
onmessage:接收消息回調(diào),模擬window.onmessage的行為,將事件參數(shù)改為[Message, Event]方便語(yǔ)義理解apply(methodName, that, args):執(zhí)行已注冊(cè)的方法,如果方法不存在則返回 undefined
-
class MessageHandler {
constructor(methods, url, win) {
Object.defineProperties(this, {
token: { value: createId() },
methods: { value: methods },
targetUrl: {
get: () => url && url.origin,
set(value) {
if (url instanceof URL) {
url = value;
} else if (typeof value === "string") {
url = new URL(value);
} else {
throw new Error("url is not URL");
}
}
},
targetWindow: {
get: () => win,
set(value) => win = value;
},
});
if (win != null) {
this.targetWindow = win;
}
if (url != null) {
this.targetUrl = url;
}
window.addEventListener("message", event => {
const message = Message.parse(event.data);
this.onmessage(message, event);
}, false);
}
send(message) {
const data = message.buildMessage();
this.targetWindow.postMessage(data, this.targetUrl);
}
targetToken = null;
onmessage = null;
apply(that, method, args) {
const fn = this.methods[method];
if (typeof fn !== "function") {
return undefined;
}
const result = fn.apply(that, args);
return result === undefined ? null : result;
}
}
- 名詞解釋
defineProperty:屬性劫持,與對(duì)象凍結(jié)一樣,也是一種保護(hù)數(shù)據(jù)的手段,他們的區(qū)別在于,屬性劫持可以更靈活的控制那些屬性需要保護(hù),需要怎樣的保護(hù);而對(duì)象凍結(jié)是將整個(gè)對(duì)象所有的屬性都保護(hù)起來(lái)
5.5 收發(fā)客戶端 Client
const client = new Client(handler, timeout);
const token = await client.request("$ask", data);
const { message, event } = await client.receive(); const result = commands[message.command](message.data, event); client.response(message.id, result);
const invoker = client.build();
對(duì)消息處理程序進(jìn)行封裝,對(duì)外提供封裝后的新函數(shù)
- 私有屬性:
-
handler:消息處理程序(MessageHandler),用于發(fā)出和接收消息 -
inbox:先進(jìn)先出隊(duì)列(AsyncQueue), 表示一個(gè)收件箱,用于存放收到的情況,由另一個(gè)線程取出數(shù)據(jù)操作 -
tasks:任務(wù)板(TaskBoard),用于存放發(fā)出的請(qǐng)求,收到回復(fù)或超時(shí)后從任務(wù)板中移除
-
- 方法:
-
async request(command, data, unresponse):發(fā)送請(qǐng)求消息,第三個(gè)參數(shù)表示是否忽略響應(yīng) -
response(id, result):發(fā)送響應(yīng)消息 -
async receive():等待接收消息 -
close():關(guān)閉客戶端 -
build():編譯一個(gè)用于調(diào)用遠(yuǎn)程方法的代理對(duì)象
-
function Client(handler, timeout) {
timeout = Math.max(100, parseInt(timeout) || 1000);
const inbox = new AsyncQueue();
const tasks = new TaskBoard();
const beforeunload = () => handler.targetWindow && this.request("close", null, true);
window.addEventListener("beforeunload", beforeunload, false);
handler.onmessage = (message, event) => {
switch (message.type) {
case "request":
inbox.push({ message, event });
break;
case "response":
tasks.finish(message.id, message.result);
break;
default:
return;
}
};
this.close = function (reason) {
window.removeEventListener("beforeunload", beforeunload);
window.removeEventListener("message", onmessage, false);
// 清理資源
handler.onmessage = null;
tasks.clear();
inbox.destroy(reason || "主動(dòng)斷開(kāi)");
}
this.request = async function (command, data, unresponse) {
const message = new RequestMessage(handler.token, command, data);
const content = new Semaphores({ message, targetToken: handler.targetToken });
if (unresponse !== true) {
tasks.put(message.id, content, timeout);
}
handler.send(message);
return await content;
}
this.response = (id, result) => handler.send(new ResponseMessage(handler.token, id, result));
this.builder = () => {
const invoker = new Proxy({}, {
get: (target, prop) => {
// 如果是 MessageHandler 屬性, 則直接返回
if (["targetUrl", "targetWindow", "token", "targetToken", "then", "catch", "finally"].includes(prop)) {
return handler[prop];
}
return (...args) => this.request("call", { method: prop, args });
}
});
const methods = handler.methods;
const commands = { /* 命令部分代碼省略... */ }
async function execRequestMessage(message, event) {
const { command, data } = message;
const fn = commands[command];
try {
return fn(data, event);
} catch (error) {
return new Error(error.message || "error");
}
}
// 循環(huán)接收消息
(async () => {
while (true) {
const { message, event } = await inbox.shift();
window.requestIdleCallback(async () => {
const result = await execRequestMessage(message, event);
if (result !== undefined) {
this.response(message.id, result);
}
});
}
})();
return invoker;
}
}
- 名詞解釋:
箭頭函數(shù):箭頭函數(shù)可以忽略函數(shù)中的this,直接使用當(dāng)前作用的this對(duì)象,在閉包中使用,可以忽略調(diào)用者對(duì)作用域的影響
Proxy:動(dòng)態(tài)代理與defineProperty最大的不同點(diǎn)在于他可以劫持一個(gè)對(duì)象中的所有操作,而不僅僅是已知屬性,但會(huì)產(chǎn)生一個(gè)新對(duì)象,而defineProperty不會(huì)產(chǎn)生新的對(duì)象; 在builder方法里對(duì)Messagehandler進(jìn)行動(dòng)態(tài)代理,可以讓用戶方便的調(diào)用遠(yuǎn)端的方法。
5.6 領(lǐng)航組件主入口 LinkCom
// 主動(dòng)連接
const invoker = await new LinkCom(methods).connect(win, url, {}); // 創(chuàng)建跨站調(diào)用器, 主動(dòng)發(fā)出連接申請(qǐng)
const res = await invoker.hello("world");
invoker.close()
// 被動(dòng)連接
const methods = {
connect() {
this.hello("world");
},
hello(msg){
alert(msg);
return "你也好";
},
close(){
// 已斷開(kāi)
}
};
new LinkCom(methods).wait(); // 創(chuàng)建跨站調(diào)用器, 并等待連接
主入口,用戶使用組件從該對(duì)象開(kāi)始
function LinkCom(methods) {
this.connect = async function (window, url, data) {
try {
const handler = new MessageHandler(methods, url, window);
const client = new Client(handler);
const invoker = client.builder();
const result = await client.request("connect", handler.token);
if (result) {
return invoker;
}
} catch (error) {
throw new Error("拒絕連接:" + error.message);
}
throw new Error("拒絕連接");
}
this.wait = function () {
const handler = new MessageHandler(methods);
new Client(handler).builder();
return this;
}
}
6. 最終效果


