Vue架构自定义Hook钩子库 spa-custom-hooks 源码解析

661 阅读3分钟

spa-custom-hooks是什么

一个可以定制页面钩子的东西,你可以注册全局的异步任务,自己定义钩子的触发条件,满足条件时即可自动执行页面里相关的钩子。 支持和vue的原生钩子created,mounted等随意搭配使用。 支持vue架构(包括uni-app、wepy、mpvue等)以及各种小程序。

目录结构

image.png 核心代码都在lib文件夹中的spa-custom-hooks文件夹中:

image.png

// 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钩子就是由onCreatedonLogin钩子构成的,然后初始化钩子函数:

// 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完美的解决了某些页面依赖相同异步数据的问题,我们在使用框架完成业务需求,除了使用基本的框架能力,我们更应该根据平时业务的痛点,结合框架或者工具,沉淀出合理的解决方案。