spa-custom-hooks是什么
一个可以定制页面钩子的东西,你可以注册全局的异步任务,自己定义钩子的触发条件,满足条件时即可自动执行页面里相关的钩子。 支持和vue的原生钩子created,mounted等随意搭配使用。 支持vue架构(包括uni-app、wepy、mpvue等)以及各种小程序。
目录结构
核心代码都在lib文件夹中的
spa-custom-hooks文件夹中:
// index.js
import * as init from './init.js'
import { setHit } from './hooks.js'
import * as polyfill from './mini-polyfill.js'
const install = function(){
if(arguments.length < 3){
// 小程序架构,使用垫片
init.install(polyfill.vue,arguments[0],polyfill.store,arguments[1] || 'globalData')
}else{
// vue架构
init.install(...arguments)
}
}
export default{
install,
setHit
}
可以看到index.js入口文件中install函数做了参数判断区分是Vue还是小程序,然后调用init初始化函数,可以看到对于小程序参数用polyfill做了兼容处理。
// init.js
const install = (vue,params,store,storeKey)=>{
//基于mpvue框架特殊处理,避免created的bug
if(vue.mpvueVersion){
BASE.initHook = 'onLoad'
}else if(vue.userAgentKey){
// 垫片里指定userAgentKey,根据参数来判断vue架构||原生小程序
BASE = userAgentKeys[vue.userAgentKey]
}
setStore(store)
hooks.init(params)
vue.mixin({
......
})
}
init函数中主要做三件事将传入的store保存,初始化自定义钩子hooks.init(params),以及混入自定义钩子Vue.mixin,我们主要看核心的代码:
// hooks.js
// 所有支持的原生钩子
const nativeHooks = ['onLaunch','created','beforeMount','mounted','activated','deactivated','beforeDestroy','destroyed','onLoad','attached','detached', 'onShow','onHide','onReady','onUnload']
.....
const init = (hooks)=> diyHooks = hooks
....
// init.js
const userAgentKeys = {
......
'vue-miniprogram': {
hooksKey: '$options',
initHook: 'beforeCreate',
supportComponent: true,
isPage(){
return this.supportComponent
}
},
......
}
let BASE = userAgentKeys['vue-miniprogram']
vue.mixin({
// 监听所有原生钩子,改变对应状态
...hooks.nativeHooks.reduce((obj,key) => (obj[key] = function(options){
//没有创建customHook不作处理
if(typeof this.customHook != 'object' && typeof this.customHook != null) return
//customHook里没有自定义钩子不作处理
if(!this.customHook.customHookArr.length) return
if(options){
this.customHook.options = options
}
const hooks = this.customHook.hook
for(let k in hooks){
const hook = hooks[k]
if(hook.name == key){
hook.cycleStart()
}else if(hook.destroy == key){
hook.cycleEnd()
}
}
}) && obj,{}),
[BASE.initHook](options) {
hookInit.call(this,options)
},
[BASE.initHookApp](options) {
hookInit.call(this,options)
}
})
上面代码init将自定义钩子对象赋值给diyHooks, 然后利用Vue.mixin在原生的钩子初始化自定义钩子this.customHook对象以及处理相关逻辑。初始化customHook对象在代码:
[BASE.initHook](options) {
hookInit.call(this,options)
}
BASE.initHook是页面初始化的第一个触发的钩子Vue就是beforeCreate小程序就是onLoad,会在这里调用hookInit.call(this,options)初始化页面的自定义钩子customHook对象:
// 入口文件及普通页面初始化
function hookInit(options){
// 入口文件特殊处理
let pageHooks = getVal(this,BASE['hooksKey'])
// 过滤掉非业务组件
if(BASE.isPage(pageHooks)){
// 兼容非vuex环境
if(!store?.state && storeKey){
store.state = this[storeKey] || storeKey
}
this.customHook = new customHook(this,options,pageHooks)
}
}
每次初始化页面时,会先过滤非业务组件,兼容非vuex环境,然后new customHook(this,options,pageHooks)创建自定义钩子对象。
// custom-hooks.js
export default class customHook {
constructor(page,options,pageHooks) {
// 页面实例、原生钩子对象
this.pageInstance = page;
// 页面内需要处理的所有钩子构成和函数执行状态
this.customHooks = {};
// 页面内需要处理的所有钩子数组
this.customHookArr = [];
// 所有钩子对象,注册的所有钩子的hookEntity类集合
this.hook = {};
// url里的参数
this.options = options;
// 钩子对象
this.pageHooks = pageHooks;
// page的hooks对象
this.init();
}
init() {
// 提出需要注入的自定义钩子
let hook = Hooks(this);
this.hook = hook;
let pageHooks = this.pageHooks;
// 钩子对象在自身还是原型链
let oneself = pageHooks.hasOwnProperty('beforeCreate') || pageHooks.hasOwnProperty('onReady');
// 过滤钩子对象、分析钩子构成
let {customHookArr,hookInscape} = this.filterHooks(oneself ? pageHooks : pageHooks['__proto__']);
this.customHookArr = customHookArr;
// 单独处理每个钩子
customHookArr.forEach((e) => {
this.customHooks[e] = {
// 此钩子实体函数
callback: pageHooks[e].bind(this.pageInstance),
// 此钩子构成
inscape: hookInscape[e],
// 此钩子是否已执行
execute: false
};
//启用需要的钩子
hookInscape[e].forEach(hookStr => hook[hookStr].need = true);
});
// await Promise.resolve();
customHookArr.length && Object.keys(hook).forEach(e => hook[e].need && hook[e].init());
}
}
上面代码会在初始化时在构造函数中执行init函数,会提出注入的自定义钩子hook:
const Hooks = (customhook) => ({
...
'Created': new hookEntity({
customhook,
name:'created',
destroy: 'destroyed',
hit: BASE.initHook == 'created'
}),
'Load': new hookEntity({
customhook,
name:'onLoad',
destroy: 'onUnload',
hit: BASE.initHook == 'onLoad'
}),
....
...(Object.keys(diyHooks).reduce((hooks,key)=>{
const item = diyHooks[key]
item.customhook = customhook
return (hooks[key] = new hookEntity(item)) && hooks
},{}))
})
所以hook钩子都经过hookEntity的处理:
// hook-entity.js
export default class hookEntity {
constructor({customhook,name,destroy,hit = false,watchKey,onUpdate}) {
// 钩子名
this.name = name;
// 相反钩子名
this.destroy = destroy;
// hit是在是生命周期钩子的情况下才有用,属性监听钩子是检测时(triggerHook)实时判断的,没有用hit属性
this.hit = hit;
// 是否采用
this.need = false;
// 是否已经初始化
this.initFlag = false;
// 属性监听key
if(watchKey){
//兼容1.1.1以及之前的版本
this.watchKey = watchKey.replace('$store.state.','');
}
// 属性监听回调
this.onUpdate = onUpdate;
this.__customhook = customhook;
}
...
}
hookEntity对象会保存钩子一系列属性比如初始化状态initFlag,属性监听回调onUpdate等。
在提取出自定义的Hook钩子之后,会判断初始化钩子beforeCreate或者onReady是属于$options对象自身还是属于原型链上,然后过滤出所有钩子:
// 钩子对象在自身还是原型链
let oneself = pageHooks.hasOwnProperty('beforeCreate') || pageHooks.hasOwnProperty('onReady');
// 过滤钩子对象、分析钩子构成
let {customHookArr,hookInscape} = this.filterHooks(oneself ? pageHooks : pageHooks['__proto__']);
在过滤钩子的方法this.filterHooks中会过滤掉未注册的钩子以及不符合规则的钩子:
filterHooks(option){
// 各钩子和包含的所有已注册单独钩子构成
let hookInscape = {};
// 筛选出自定义钩子
return {
customHookArr: Object.keys(option).filter(e => {
// 不符合规则的钩子不予处理
let hookArr = this.getHookArr(e);
if(hookArr.length){
//过滤掉未注册的钩子
hookInscape[e] = hookArr.filter((h)=>{
if(this.hook[h]){
return true;
}
console.warn(`[custom-hook 错误声明警告] "${h}"钩子未注册,意味着"${e}"可能永远不会执行,请先注册此钩子再使用,文档:https://github.com/1977474741/spa-custom-hooks#-diyhooks对象说明`);
return false;
});
//格式 + 是否注册的效验
return e == 'on' + hookArr.join('') && hookInscape[e].length == hookArr.length;
}
return false;
}),
hookInscape
}
}
上面过滤后会返回customHookArr自定义钩子数组以及自定义钩子的组成对象hookInscape,比如onCreatedLogin钩子就是由onCreated和onLogin钩子构成的,然后初始化钩子函数:
// custion-hook.js
customHookArr.length && Object.keys(hook).forEach(e => hook[e].need && hook[e].init());
// hook-entity.js
init() {
if (this.initFlag) return;
if(this.watchKey){
this.watchAttr((success) => {
this[success ? 'cycleStart' : 'cycleEnd']();
});
}
this.initFlag = true;
}
在初始化函数init中,如果存在需要监听的异步数据会使用store.watch中对应的数据变化,根据数据变化确定是否触发对应的钩子事件:
watchAttr(cb) {
try{
const that = this;
const store = getStore(this.__customhook.pageInstance)
store.watch((state) => {
return getVal(state,that.watchKey)
},(val,oldval)=>{
cb(that.onUpdate ? that.onUpdate(val, oldval) : val);
},{
//兼容mini-polyfill
watchKey: that.watchKey
})
}catch(err){}
}
在页面初始化钩子函数中完成自定义钩子对象的处理之后,在页面其他原生钩子中混入对于自定义钩子的处理:
// 监听所有原生钩子,改变对应状态
...hooks.nativeHooks.reduce((obj,key) => (obj[key] = function(options){
...
//没有创建customHook不作处理
if(typeof this.customHook != 'object' && typeof this.customHook != null) return
//customHook里没有自定义钩子不作处理
if(!this.customHook.customHookArr.length) return
if(options){
this.customHook.options = options
}
const hooks = this.customHook.hook
for(let k in hooks){
const hook = hooks[k]
if(hook.name == key){
hook.cycleStart()
}else if(hook.destroy == key){
hook.cycleEnd()
}
}
}) && obj,{}),
会先判断当前页面是否存在自定义的钩子,没有不做任何处理,有的话会将遍历所有钩子对象,后触发对应的hook.cycleStart()函数,每个钩子cycleStart的只会触发一次,通过this.hit判断,如果是激活钩子重置状态则会调用hook.cycleEnd:
// hook-entity.js
cycleStart() {
if(this.hit) return;
this.hit = true;
this.__customhook && this.__customhook.triggerHook(this.name);
}
后触发customhook.triggerHook(this.name);,会遍历钩子触发需要的钩子函数callback,并将对应的参数options传入:
triggerHook(hitKey) {
this.customHookArr.forEach((name) => {
let customHook = this.customHooks[name];
let meet = customHook.inscape.every(e => this.hook[e].need && this.checkHookHit(this.hook[e]))
if (meet && !customHook.execute) {
customHook.execute = true;
this.customHooks[name]['callback'](this.options);
}
});
}
总结思考
spa-custom-hooks库完美的利用了Vue的生命周期特性结合VueX的能力,实现了开发者自定义钩子的功能,思路并不复杂,难点在于对自定义钩子状态的控制以及对复合钩子的处理,spa-custom-hooks完美的解决了某些页面依赖相同异步数据的问题,我们在使用框架完成业务需求,除了使用基本的框架能力,我们更应该根据平时业务的痛点,结合框架或者工具,沉淀出合理的解决方案。