开发者社区> 问答> 正文

如何在Django管理mixin中合并功能?

我正在使用两个Django插件,Django -import-export和Django -safedelete。两者都提供了Django管理下拉菜单的功能。如: django-import-export django-safedelete 有没有一种简单的方法可以将这两个插件的特性结合在一起,并且代码更改最少? 问题来源StackOverflow 地址:/questions/59467133/how-to-merge-functionalities-in-django-admin-mixins

展开
收起
kun坤 2019-12-25 09:32:32 567 0
1 条回答
写回答
取消 提交回答
  • 在vue里面,混入(mixin)是一种特殊的使用方式。一个混入对象可以包含任意的组件选项,可根据需求随意“封装”组件的可复用单元,并在使用时根据一定的策略合并到组件的选项当中,使用时与组件自身选项无异。 官方文档对mixin介绍比较少,不能了解甚少,于是便想研究下源码对它混入做个研究和总结

    本文基于Vue源码2.x版本

    一、说在前面 在分析mixin之前,先看看两个方法,它们在混入的合并过程中扮演着重要的角色 (1) Vue.extend() Vue.extend()是基础Vue构造器,参数是一个包含组件选项的对象,可用于显式的扩展组件和混入对象,如

    var Component = Vue.extend({ mixins: [myMixin] }) 其实,在组件内部的混入合并也是通过它来完成的 (2) extend() extend()是对象合并方法,参数是源和目标两个对象,用于对象的合并操作

    extend(target, source) 其中source对象将合并到target对象,如果source和target的key值相同,则直接覆盖,否则属性添加;作用类似于Object.assgin()和underscore的_.extend(destination, *sources)

    二、合并图示 通过对vue源码的研究,我发现混入对于选项的“合并”并不是一步到位的,而是两两合并,并通过合并策略和优先级向一定的方向逐步进行合并操作,最终才得到合并的结果,就像默认的选项合并策略:

    const defaultStrat = function (parentVal: any, childVal: any): any { return childVal === undefined ? parentVal : childVal } 其中childVal和parentVal是每一次合并中的两个源和目标对象,比如:vm.options.xx和mixin.options.xx,下面用一个例子和图示进行说明 示例代码:

    import Vue from 'vue'

    Vue.mixin({ data () { return { msg: '全局混入-msg', msg1: '全局混入1-msg-1', msg2: '全局混入1-msg-2', msg4: '全局混入1-msg4', msg5: '全局混入1-msg5', site: { name: '腾讯', url: 'www.tenant.com', hahaList: [3, 4], city: 'shenzhen' } } }, created: function () { console.log('全局混入1 --- created') }, methods: { startMix: function () { console.log('全局混入1 --- startMix') } }, computed: { getMsg: function () { return 'getMsg 全局混入1!!' }, getMsg2: function () { return 'getMsg2 全局混入1!!' }, getMsg5: function () { return 'getMsg5 全局混入1!!' } } })

    Vue.mixin({ data () { return { msg: '全局混入2-msg', msg1: '全局混入2-msg-1', msg4: '全局混入2-msg4', numList: [4, 5], site: { name: 'alibaba', hahaList: [3, 4], url: 'www.alibaba.com', people: 30000 } } }, created: function () { console.log('全局混入2 --- created') }, methods: { startMix: function () { console.log('全局混入2 --- startMix') }, hello: function () { console.log('全局混入2 --- hello') } }, computed: { getMsg: function () { return 'getMsg 全局混入2!!' }, getMsg2: function () { return 'getMsg2 全局混入2!!' } } })

    var localMix3 = { data () { return { msg: '实例混入3', msg1: '实例混入3-msg1', msg2: '实例混入3-msg2', msg3: '实例混入3-msg3', site: { name: '淘宝111', url: 'www', country: 'China' } } }, created: function () { console.log('实例混入3 --- created') }, methods: { startMix: function () { console.log('实例混入3 --- startMix') }, hello: function () { console.log('实例混入3 --- hello') } }, computed: { getMsg: function () { return 'getMsg 实例混入3!!' }, getMsg2: function () { return 'getMsg2 实例混入3!!' }, getMsg3: function () { return 'getMsg3 实例混入3!!' }, getMsg4: function () { return 'getMsg4 实例混入3!!' } } } var localMix2 = { data () { return { msg: '实例混入2', msg1: '实例混入2-msg1', msg2: '实例混入2-msg2', site: { name: '淘宝', url: 'www.taobao.com', title: 'I am 淘宝' } } }, created: function () { console.log('实例混入2 --- created') }, methods: { startMix: function () { console.log('实例混入2 --- startMix') }, hello: function () { console.log('实例混入2 --- hello') } }, computed: { getMsg: function () { return 'getMsg 实例混入2!!' }, getMsg2: function () { return 'getMsg2 实例混入2!!' }, getMsg3: function () { return 'getMsg3 实例混入2!!' } } } var localMix1 = { data () { return { msg: '实例混入1', msg1: '实例混入1-msg1', numList: [2, 3], site: { name: '淘宝', url: 'www.taobao.com', hahaList: [3, 4] } } }, mixins: [localMix2], created: function () { console.log('实例混入1 --- created') }, methods: { startMix: function () { console.log('实例混入1 --- startMix') }, hello: function () { console.log('实例混入1 --- hello') } }, filters: { capitalize2: function (value) { if (!value) return '' return value + ' 实例混入1!!' } }, computed: { getMsg: function () { return 'getMsg 实例混入1!!' }, getMsg2: function () { return 'getMsg2 实例混入1!!' } } }

    export default { name: 'basicExtend', data () { return { msg: 'basicExtend', numList: [1, 2], site: { name: '百度', hahaList: [1, 2] } } }, mixins: [localMix3, localMix1], created: function () { console.log('basicExtend --- created') }, methods: { startMix: function () { console.log('basicExtend --- startMix') } }, computed: { showData: function () { return JSON.stringify(this.$data) }, getMsg: function () { return this.msg + ' !!!' } } } 其中合并后的data和输出如下,

    vm.$data: { "msg":"basicExtend", "numList":[1,2], "site":{ "name":"百度", "hahaList":[1,2], "url":"www.taobao.com", "title":"I am 淘宝", "country":"China", "people":30000, "city":"shenzhen" }, "msg1":"实例混入1-msg1", "msg2":"实例混入2-msg2", "msg3":"实例混入3-msg3", "msg4":"全局混入2-msg4", "msg5":"全局混入1-msg5" }

    computed计算属性: getMsg: basicExtend !!! getMsg2: getMsg2 实例混入1!! getMsg3: getMsg3 实例混入2!! getMsg4: getMsg4 实例混入3!! getMsg5: getMsg5 全局混入1!! 下面根据这个例子整理的“合并父子图示”,根据后面的讲解再反过来看为什么合并的结果会是这样。

    vue-mixins.png 图示描述了源代码中选项合并方法的入参"父子"关系,其中,

    全局注册的混入最先完成混入,并按注册的顺序来逐个合并,先注册的先完成混入合并,依次类推 局部注册的混入次之,并按mixins数组里声明的顺序依次完成合并 每个混入也可以包含mixins局部混入数组,mixins先完成合并,本混入的options再进行合并 组件options最后完成混入合并 先合并的"优先级"低,后合并的"优先级"高,也就是组件的options合并优先级最高 不同的选项根据自身的混入策略合并方向不一样,这个在下面会有说 三、选项合并 混入对象的混入是每个对象选项和组件选项的“混入合并”,比如:options.data、options.props等,根据不同选项合并策略的不同,各类选项会以不同的方式和方向进行“合并”操作 (1) el,propsData

    合并方向: parent --> child

    源码:

    src/core/util/options.js

    const strats = config.optionMergeStrategies

    if (process.env.NODE_ENV !== 'production') { strats.el = strats.propsData = function (parent, child, vm, key) { if (!vm) { warn( option "${key}" can only be used during instance + 'creation with the new keyword.' ) } return defaultStrat(parent, child) } } /** * Default strategy. */ const defaultStrat = function (parentVal: any, childVal: any): any { return childVal === undefined ? parentVal : childVal } 可见,el , propsData使用的是默认合并策略,默认策略比较简单干脆,以child选项为主,若无则使用parent选项

    (2) 生命周期钩子

    合并方向: parent --> child

    源码:

    export const LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured', 'serverPrefetch' ] LIFECYCLE_HOOKS.forEach(hook => { strats[hook] = mergeHook })

    function mergeHook ( parentVal: ?Array , childVal: ?Function | ?Array ): ?Array { const res = childVal ? parentVal ? parentVal.concat(childVal) // 合并为一个数组 : Array.isArray(childVal) ? childVal : [childVal] : parentVal return res ? dedupeHooks(res) : res } 可见,LIFECYCLE_HOOKS会将每个hook合并成一个数组,按照图示从父到子开始一步步链接合并成数组,parent在前,child在后。在钩子触发时,按照数组从头顺序调用触发,所以我们看到调用顺序是这样的,与图示一致:

    全局混入hook --> 实例混入hook ... --> 组件实例hook (3) data

    合并方向: parent --> child

    源码:

    strats.data = function ( parentVal: any, childVal: any, vm?: Component ): ?Function { if (!vm) { if (childVal && typeof childVal !== 'function') { process.env.NODE_ENV !== 'production' && warn( 'The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm ) return parentVal } return mergeDataOrFn(parentVal, childVal) }

    return mergeDataOrFn(parentVal, childVal, vm) }

    export function mergeDataOrFn ( parentVal: any, childVal: any, vm?: Component ): ?Function { if (!vm) { // in a Vue.extend merge, both should be functions if (!childVal) { return parentVal } if (!parentVal) { return childVal } // when parentVal & childVal are both present, // we need to return a function that returns the // merged result of both functions... no need to // check if parentVal is a function here because // it has to be a function to pass previous merges. return function mergedDataFn () { // parentVal作为from,childVal作为to,parent->child方向合并 return mergeData( typeof childVal === 'function' ? childVal.call(this, this) : childVal, typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal ) } } else { return function mergedInstanceDataFn () { // instance merge const instanceData = typeof childVal === 'function' ? childVal.call(vm, vm) : childVal const defaultData = typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal if (instanceData) { // parentVal作为from,childVal作为to,parent->child方向合并 return mergeData(instanceData, defaultData) } else { return defaultData } } } }

    function mergeData (to: Object, from: ?Object): Object { if (!from) return to let key, toVal, fromVal

    const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from)

    for (let i = 0; i < keys.length; i++) { key = keys[i] // in case the object is already observed... if (key === 'ob') continue toVal = to[key] // child fromVal = from[key] // parent if (!hasOwn(to, key)) { set(to, key, fromVal) //若to没有此key,添加它 } else if ( toVal !== fromVal && isPlainObject(toVal) && isPlainObject(fromVal) ) { //若to有此key,且不等 //若to有此key,值非对象,否则进行深度合并 mergeData(toVal, fromVal) } } return to } 这里set()方法操作添加key并创建响应属性值 可以看到,data的合并比较复杂,按照图示从父到子开始递归合并,以child为主,比较key规则如下:

    若child无此key,parent有,直接合并此key 若child和parent都有此key,且非object类型,忽略不作为 若child和parent都有此key,且为object类型,则递归合并对象 (4) components,directives,filters

    合并方向: parent <-- child

    源码:

    export const ASSET_TYPES = [ 'component', 'directive', 'filter' ] ASSET_TYPES.forEach(function (type) { strats[type + 's'] = mergeAssets })

    function mergeAssets ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): Object { const res = Object.create(parentVal || null) // 原型委托 if (childVal) { process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm) return extend(res, childVal) // child合并到parent } else { return res } } 可以看到,components, directives,filters的合并策略比较简单,使用extend方法合并为一个对象,按 照图示从子到父进行合并。 其实这是采用原型链委托的方式在合并时把child的属性委托在parent上,这样在使用的时候,在child上查找,没有的再从parent上找,以此类推,所以child的优先级的更高的。

    (5) watch

    合并方向: parent --> child

    源码:

    strats.watch = function ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object { // work around Firefox's Object.prototype.watch... if (parentVal === nativeWatch) parentVal = undefined if (childVal === nativeWatch) childVal = undefined /* istanbul ignore if */ if (!childVal) return Object.create(parentVal || null) if (process.env.NODE_ENV !== 'production') { assertObjectType(key, childVal, vm) } if (!parentVal) return childVal const ret = {} extend(ret, parentVal) //获取parent选项 for (const key in childVal) { let parent = ret[key] //获取parent选项值 const child = childVal[key] //获取child选项值 if (parent && !Array.isArray(parent)) { parent = [parent] } //每个wather选项合并为数组 ret[key] = parent

    2021-02-16 12:19:05
    赞同 展开评论 打赏
问答分类:
问答标签:
问答地址:
问答排行榜
最热
最新

相关电子书

更多
ui-model,跨框架复用 立即下载
函数计算最佳实践:快速开发一个分布式 Puppeteer 网页截图服务 立即下载
利用编译将 Vue 组件转成 React 组件 立即下载