一、背景
我们公司的官网因为种种原因,是phpcms做的,模板生成的SEO静态页
某天产品:我希望在官网添加一套登录逻辑,我看这套登录逻辑已经在X项目实现了,你直接复制粘贴就好了,很简单吧!
我:@%#......¥
二、思考一番
已知条件:一个静态页面、一个已有的登录功能
需求:把这个登录功能加入到静态页面中
我们先把这个需求拆解成:
- 实现一个通信机制,让页面和登录模块能够交流,进行数据的同步
- 通信机制写成通用的形式,保不准后面还要插上其他功能也需要用到通讯
三、通讯系统的实现
因为是在同一个页面的实现,不需要考虑跨域的问题,所以考虑使用SDK
的方式实现,这样就可以挂载到window
上,直接使用了
1. 实现事件系统
我们先完成通信中不可缺少的事件模块,包含简单的 on
、off
和emit
js
export const SDK = {
globalNameSpace: 'sdk',
listeners: {},
on(event, callback, namespace = this.globalNamespace) {
const eventKey = `${namespace}:${event}`;
if (!this.listeners[eventKey]) {
this.listeners[eventKey] = [];
}
this.listeners[eventKey].push(callback);
},
off(event, callback, namespace = this.globalNamespace) {
const eventKey = `${namespace}:${event}`;
if (this.listeners[eventKey]) {
const index = this.listeners[eventKey].indexOf(callback);
if (index > -1) {
this.listeners[eventKey].splice(index, 1);
}
}
},
emit(event, data, namespace = this.globalNamespace) {
const eventKey = `${namespace}:${event}`;
if (this.listeners[eventKey]) {
this.listeners[eventKey].forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(error);
}
});
}
}
}
2. 实现模块系统
通讯系统应该是一个管理的地方,不涉及具体的业务实现。所以登录相关的业务逻辑应该是一个可插拔的模块去实现,那么我们可以在SDK
中添加一个模块系统去管理这些插拔的业务模块
js
export const SDK = {
// 省略已有的代码
modules: {},
registerModule(moduleName, module) {
this.modules[moduleName] = module;
this.emit('moduleRegistered', { name: moduleName, module });
},
unregisterModule(moduleName) {
if (this.modules[moduleName]) {
if (typeof this.modules[moduleName].destroy === 'function') {
this.modules[moduleName].destroy();
}
delete this.modules[moduleName];
this.emit('moduleUnregistered', { name: moduleName });
}
},
getModule(moduleName) {
return this.modules[moduleName] || null;
},
waitForModules(moduleNames) {
const names = Array.isArray(moduleNames) ? moduleNames : [moduleNames];
return new Promise((resolve) => {
const checkModules = () => {
const allLoaded = names.every(name => this.getModule(name));
if (allLoaded) {
resolve(names.map(name => this.getModule(name)));
} else {
setTimeout(checkModules, 100);
}
};
checkModules();
});
},
}
if (typeof window !== 'undefined') {
window.SDK = SDK
window.dispatchEvent(new CustomEvent('SDKMounted'))
}
export default SDK
3. 实现登录模块
登录模块更多的是实现业务逻辑,包含注册自身到SDK中、监听登录数据、登录、退出登录等等
js
import { SDK } from './sdk.js';
const sdkEmit = (event, data) => {
SDK.emit(event, data)
}
export const loginModule = {
initialized: false
init () {
if (this.initialized) return;
this.initialized = true;
this.setupWatchers();
},
setupWatchers () {
// 监听 token 变化
watch(() => store.token, (newToken, oldToken) => {
if (newToken !== oldToken) {
sdkEmit('loginStateChange', { token: newToken });
}
});
// 监听 userInfo 变化
watch(() => store.userInfo, (newUserInfo) => {
sdkEmit('userInfoChange', { userInfo: Object.assign({}, newUserInfo) });
}, { deep: true });
},
getToken () {},
getUserInfo () {},
openLogin (data) {
store.openLogin(data)
},
logout() {
store.logout()
}
}
export default const initLoginModule () {
SDK.registerModule('login', LoginModule)
LoginModule.init()
}
四、静态页面接入SDK
在静态页面中,我们需要完成以下几个实现
- 完成和SDK的连接
- 获取登录模块
- 监听登录数据的改变
下面我们一一实现
html
<script>
const init = (SDK, [loginModule]) => {
// 主动获取数据
// 获取token
loginModule.getToken().then((token) => {
console.log('getToken', token)
})
// 获取用户信息
loginModule.getUserInfo().then((userInfo) => {
console.log('getUserInfo', userInfo)
})
// 数据变更监听
// 登录状态变更
SDK.on('loginStateChange', (res) => {
console.log('loginStateChange', res)
})
// 用户信息变更
SDK.on('userInfoChange', (res) => {
console.log('userInfoChange', res)
})
}
const obLoginModule = () => {
window.SDK.waitForModules(['login]).then(modules => {
init(window.SDK, modules)
})
}
if (window.SDK) {
obLoginModule()
} else {
window.addEventListener('SDKMounted', () => {
obLoginModule()
})
}
</script>
五、总结
对于常见的页面模块通信处理,可以参考上面的思路
因为有模块系统在,后续产品再插入其他功能,新增一个模块的业务代码即可,不需要更改已有的通信机制,还是很方便的