瀏覽代碼

refactor: RouterTab 组件代码按功能拆分子模块后混入

zhaihaoyi 6 年之前
父節點
當前提交
5ba25963c7

+ 3 - 10
src/components/RouterAlive.js

@@ -1,17 +1,12 @@
 import { emptyObj } from '../util'
-import { getAliveId } from '../util/alive'
 import { getFirstComponentChild } from '../util/dom'
 import { isAlikeRoute, isSameComponentRoute } from '../util/route'
 
+import rule from './RouterTab/rule'
+
 export default {
   name: 'RouterAlive',
-  props: {
-    // 缓存id,如果为函数,则参数为route
-    aliveId: {
-      type: [ String, Function ],
-      default: 'path'
-    }
-  },
+  mixins: [ rule ],
 
   beforeCreate () {
     Object.assign(this, {
@@ -82,8 +77,6 @@ export default {
   },
 
   methods: {
-    getAliveId,
-
     // 设置缓存项
     set (key, item) {
       const { cache } = this

+ 0 - 486
src/components/RouterTab.js

@@ -1,486 +0,0 @@
-import Vue from 'vue'
-
-// 方法
-import { emptyObj, emptyArray, debounce } from '../util'
-import { getAliveId } from '../util/alive'
-import { scrollTo } from '../util/dom'
-import { isAlikeRoute, getPathWithoutHash } from '../util/route'
-
-// 语言配置
-import langs from '../lang'
-
-// 子组件
-import RouterAlive from './RouterAlive'
-
-// 功能混入
-import iframe from '../mixins/routerTab/iframe'
-
-export default {
-  name: 'RouterTab',
-  components: { RouterAlive },
-  mixins: [ iframe ],
-  props: {
-    // 缓存id,如果为函数,则参数为route
-    aliveId: RouterAlive.props.aliveId,
-
-    // 语言配置
-    // - 为字符串时,可以设置为内置的语言 'zh-CN' (默认) 和 'en'
-    // - 为对象时,可设置自定义的语言
-    i18n: {
-      type: [ String, Object ],
-      default: 'zh-CN'
-    },
-
-    // 初始页签数据
-    tabs: {
-      type: Array,
-      default: () => []
-    },
-
-    // router-view组件配置
-    routerView: Object,
-
-    // 页签过渡效果
-    tabTransition: {
-      type: [ String, Object ],
-      default: 'router-tab-zoom'
-    },
-
-    // 页面过渡效果
-    pageTransition: {
-      type: [ String, Object ],
-      default: () => ({
-        name: 'router-tab-swap',
-        mode: 'out-in'
-      })
-    }
-  },
-
-  data () {
-    return {
-      loading: false, // 路由页面loading
-      items: [], // 页签项
-      activedTab: null, // 当前激活的页签
-      isRouterAlive: true,
-      // 右键菜单
-      contextmenu: {
-        id: null,
-        index: -1,
-        left: 0,
-        top: 0
-      }
-    }
-  },
-
-  computed: {
-    // 语言内容
-    lang () {
-      let lang = null
-      let i18n = this.i18n
-
-      if (typeof i18n === 'string') {
-        lang = langs[i18n]
-      } else if (typeof i18n === 'object') {
-        lang = i18n
-      }
-
-      // 找不到语言配置,则使用英文
-      if (!lang) lang = langs['en']
-
-      return lang
-    },
-
-    // 右键菜单是否当前页签
-    isContextTabActived () {
-      return this.contextmenu.id === this.activedTab
-    },
-
-    // 右键页签是否允许关闭
-    isContextTabCanBeClosed () {
-      let { items, contextmenu } = this
-      let contextTab = items[contextmenu.index]
-      return items.length > 1 && contextTab && contextTab.closable !== false
-    },
-
-    // 左侧可关闭的页签
-    tabsLeft () {
-      let { items, contextmenu: { id, index } } = this
-      return id ? items.slice(0, index).filter(({ closable }) => closable !== false) : emptyArray
-    },
-
-    // 左侧可关闭的页签
-    tabsRight () {
-      let { items, contextmenu: { id, index } } = this
-      return id ? items.slice(index + 1).filter(({ closable }) => closable !== false) : emptyArray
-    },
-
-    // 其他可关闭的页签
-    tabsOther () {
-      let { items, contextmenu: { id } } = this
-      return id ? items.filter(({ closable, id: tid }) => closable !== false && id !== tid) : emptyArray
-    }
-  },
-
-  watch: {
-    // 路由切换更新激活的页签
-    $route ($route) {
-      this.loading = false
-      this.hideContextmenu()
-      this.updateActivedTab()
-      this.fixCommentPage()
-    },
-
-    async activedTab () {
-      // 激活页签时,如果当前页签不在可视区域,则滚动显示页签
-      await this.$nextTick()
-
-      let $cur = this.$el.querySelector('.router-tab-nav .actived')
-      let $scr = this.$el.querySelector('.router-tab-scroll')
-      if ($cur) {
-        let cLeft = $cur.offsetLeft
-        let sLeft = $scr.scrollLeft
-        if (cLeft < sLeft || cLeft + $cur.clientWidth > sLeft + $scr.clientWidth) {
-          this.adjust()
-        }
-      }
-    },
-
-    // 监听右键菜单显示关闭
-    'contextmenu.id' (val, old) {
-      if (!old && val) {
-        // 显示右键菜单,绑定点击关闭事件
-        document.addEventListener('click', this.onClick = (e) => {
-          if (e.target !== this.$el.querySelector('.router-tab-contextmenu')) {
-            this.hideContextmenu()
-          }
-        })
-      } else if (old && !val) {
-        // 隐藏右键菜单,移除点击关闭事件
-        document.removeEventListener('click', this.onClick)
-      }
-    }
-  },
-
-  beforeCreate () {
-    // 添加到原型链
-    Vue.prototype.$routerTab = this
-
-    // 获取跟路径
-    let matched = this.$route.matched
-    this.basePath = (matched[matched.length - 2] || {}).path
-  },
-
-  created () {
-    this.getTabItems()
-    this.updateActivedTab()
-
-    this.$router.beforeEach(this.routerPageLeaveGuard)
-    this.$nextTick(this.adjust)
-  },
-
-  mounted () {
-    // 页面载入和浏览器窗口大小改变时调整Tab滚动显示
-    window.addEventListener('resize', this.onResize = debounce(this.adjust))
-  },
-
-  destroyed () {
-    // 销毁后移除监听事件
-    window.removeEventListener('resize', this.onResize)
-  },
-
-  methods: {
-    getAliveId,
-
-    // 页面离开导航守卫
-    routerPageLeaveGuard (to, from, next) {
-      if (this._isDestroyed) {
-        let hooks = this.$router.beforeHooks
-        let idx = hooks.indexOf(this.routerPageLeaveGuard)
-
-        // 移除已销毁的RouterTab实例注册的导航守卫
-        if (idx > -1) hooks.splice(idx, 1)
-
-        next()
-      } else {
-        const id = this.getAliveId(to)
-        const $alive = this.$refs.routerAlive
-        const { route: cacheRoute } = ($alive && $alive.cache[id]) || emptyObj
-
-        // 如果不是相同路由则检查beforePageLeave
-        if (cacheRoute && !isAlikeRoute(to, cacheRoute)) {
-          this.pageLeavePromise(id, 'replace')
-            .then(() => next())
-            .catch(() => next(false))
-        } else {
-          next()
-        }
-      }
-    },
-
-    // 根据初始页签数据生成页签列表
-    getTabItems () {
-      let { tabs, $router } = this
-      let ids = {}
-
-      this.items = tabs.map((item, index) => {
-        let { to, closable, title, tips } = typeof item === 'string'
-          ? { to: item }
-          : (item || emptyObj)
-        let route = to && $router.match(to)
-
-        if (route) {
-          let tab = this.getRouteTab(route)
-          let id = tab.id
-
-          // 根据id去重
-          if (!ids[id]) {
-            // 初始 tab 数据
-            if (title) tab.title = title
-            if (tips) tab.tips = tips
-            tab.closable = closable !== false
-
-            return (ids[id] = tab)
-          }
-        }
-      }).filter(item => !!item)
-    },
-
-    // 更新激活的页签
-    updateActivedTab () {
-      this.activedTab = this.getAliveId()
-    },
-
-    // 更新tab数据
-    updateTab (key, { route, tab }) {
-      let { items } = this
-      let matchIdx = items.findIndex(({ id }) => id === key)
-
-      let item = Object.assign(this.getRouteTab(route), tab)
-
-      if (matchIdx > -1) {
-        let matchTab = items[matchIdx]
-        item.closable = matchTab.closable !== false
-        this.$set(items, matchIdx, item)
-      } else {
-        items.push(item)
-      }
-    },
-
-    // 从路由地址获取 aliveId
-    getIdByLocation (location, fullMatch = true) {
-      if (!location) return
-
-      let $route = this.$router.match(location, this.$router.currentRoute)
-
-      // 路由地址精确匹配页签
-      if (fullMatch) {
-        let matchPath = getPathWithoutHash($route)
-        let matchTab = this.items.find(({ to }) => to.split('#')[0] === matchPath)
-
-        if (matchTab) {
-          return matchTab.id
-        }
-      } else {
-        return this.getAliveId($route)
-      }
-    },
-
-    // 从route中获取tab数据
-    getRouteTab (route) {
-      let id = this.getAliveId(route)
-      let { fullPath: to, meta } = route
-      let { title, icon, tips } = meta
-
-      return { id, to, title, icon, tips }
-    },
-
-    // 页面离开Promise
-    pageLeavePromise (id, type) {
-      return new Promise((resolve, reject) => {
-        let $alive = this.$refs.routerAlive
-        let tab = this.items.find(item => item.id === id) // 当前页签
-        let { vm } = $alive.cache[id] || emptyObj // 缓存数据
-        let beforePageLeave = vm && vm.$vnode.componentOptions.Ctor.options.beforePageLeave
-
-        if (typeof beforePageLeave === 'function') {
-          // 页签关闭前
-          beforePageLeave.bind(vm)(resolve, reject, tab, type)
-        } else {
-          resolve()
-        }
-      })
-    },
-
-    // 移除tab项
-    async removeTab (id) {
-      let { items } = this
-      let $alive = this.$refs.routerAlive
-      const idx = items.findIndex(item => item.id === id)
-
-      if (items.length === 1) {
-        throw new Error(this.lang.msg.keepOneTab)
-      }
-
-      try {
-        await this.pageLeavePromise(id, 'close')
-
-        // 承诺关闭后移除页签和缓存
-        $alive.remove(id)
-        idx > -1 && items.splice(idx, 1)
-      } catch (e) {}
-    },
-
-    // 通过路由地址关闭页签
-    close (location, fullMatch = true) {
-      if (location) {
-        let id = this.getIdByLocation(location, fullMatch)
-        if (id) {
-          this.closeTab(id)
-        }
-      } else {
-        this.closeTab()
-      }
-    },
-
-    // 通过页签id关闭页签
-    async closeTab (id = this.activedTab) {
-      let { activedTab, items, $router } = this
-      const idx = items.findIndex(item => item.id === id)
-
-      try {
-        await this.removeTab(id)
-
-        // 如果关闭当前页签,则打开后一个页签
-        if (activedTab === id) {
-          let nextTab = items[idx] || items[idx - 1]
-          $router.replace(nextTab.to)
-        }
-      } catch (e) {
-        console.warn(e)
-      }
-    },
-
-    // 关闭多个页签
-    async closeMulti (tabs) {
-      let { items, $router, contextmenu } = this
-      let nextTab = items.find(({ id }) => id === contextmenu.id)
-
-      for (let { id } of tabs) {
-        try {
-          await this.removeTab(id)
-        } catch (e) {}
-      }
-
-      // 当前页签如已关闭,则打开右键选中页签
-      if (items.findIndex(({ id }) => id === this.activedTab) === -1) {
-        $router.replace(nextTab.to)
-      }
-    },
-
-    // 通过路由地址刷新页签
-    refresh (location, fullMatch = true) {
-      if (location) {
-        let id = this.getIdByLocation(location, fullMatch)
-        if (id) {
-          this.refreshTab(id)
-        }
-      } else {
-        this.refreshTab()
-      }
-    },
-
-    // 刷新指定页签
-    async refreshTab (id = this.activedTab) {
-      try {
-        await this.pageLeavePromise(id, 'refresh')
-        this.$refs.routerAlive.clear(id)
-        if (id === this.activedTab) this.reloadRouter()
-      } catch (e) {}
-    },
-
-    /**
-     * 刷新所有页签
-     * @param {boolean} [force=false] 是否强制刷新,如果强制则忽略页面beforePageLeave
-     */
-    async refreshAll (force = false) {
-      const $alive = this.$refs.routerAlive
-      const { cache } = $alive
-      for (const id in cache) {
-        if (!force) {
-          try {
-            await this.pageLeavePromise(id, 'refresh')
-            $alive.clear(id)
-          } catch (e) {}
-        } else {
-          $alive.clear(id)
-        }
-      }
-      this.reloadRouter()
-    },
-
-    // 重载路由组件
-    async reloadRouter (ignoreTransition = false) {
-      this.isRouterAlive = false
-
-      // 默认在页面过渡结束后会设置 isRouterAlive 为 true
-      // 如果过渡事件失效,则需传入 ignoreTransition 为 true 手动更改
-      if (ignoreTransition) {
-        await this.$nextTick()
-        this.isRouterAlive = true
-      }
-    },
-
-    // 页签过渡结束
-    onTabTransitionEnd () {
-      this.adjust()
-    },
-
-    // 页面过渡结束
-    onPageTransitionEnd () {
-      if (!this.isRouterAlive) this.isRouterAlive = true
-    },
-
-    // 显示页签右键菜单
-    showContextmenu (id, index, e) {
-      // 菜单定位
-      let { clientY: top, clientX: left } = e || emptyObj
-      Object.assign(this.contextmenu, { id, index, top, left })
-    },
-
-    // 关闭页签右键菜单
-    hideContextmenu () {
-      this.showContextmenu(null, -1)
-    },
-
-    // Tab滚动
-    tabScroll (direction) {
-      let $tab = this.$el.querySelector('.router-tab-header')
-      let $scr = $tab.querySelector('.router-tab-scroll')
-      let space = $tab.clientWidth - 110
-
-      scrollTo($scr, $scr.scrollLeft + (direction === 'left' ? -space : space))
-    },
-
-    // 调整Tab滚动显示
-    adjust () {
-      let $tab = this.$el.querySelector('.router-tab-header')
-      let $scr = $tab.querySelector('.router-tab-scroll')
-      let $nav = $scr.querySelector('.router-tab-nav')
-      let $cur = $nav.querySelector('.actived')
-      let isScroll = $nav.clientWidth > $scr.clientWidth // 判断是否需要滚动
-
-      $tab.classList[isScroll ? 'add' : 'remove']('is-scroll')
-
-      if ($cur && isScroll) {
-        scrollTo($scr, $cur.offsetLeft + ($cur.clientWidth - $scr.clientWidth) / 2)
-      }
-    },
-
-    // 修复:当快速频繁切换页签时,旧页面离开过渡效果尚未完成,新页面内容无法正常mount,内容节点为comment类型
-    fixCommentPage () {
-      if (this.$refs.routerAlive.$el.nodeType === 8) {
-        this.reloadRouter(true)
-      }
-    }
-  }
-}

+ 3 - 3
src/components/RouterTab.vue → src/components/RouterTab/RouterTab.vue

@@ -155,7 +155,7 @@
   </div>
 </template>
 
-<script src="./RouterTab.js"></script>
+<script src="./index.js"></script>
 
-<style lang="scss" src="../scss/routerTab.scss"></style>
-<style lang="scss" src="../scss/transition.scss"></style>
+<style lang="scss" src="../../scss/routerTab.scss"></style>
+<style lang="scss" src="../../scss/transition.scss"></style>

+ 101 - 0
src/components/RouterTab/contextmenu.js

@@ -0,0 +1,101 @@
+import { emptyObj, emptyArray } from '../../util'
+
+// 右键菜单
+export default {
+  data () {
+    return {
+      // 右键菜单
+      contextmenu: {
+        id: null,
+        index: -1,
+        left: 0,
+        top: 0
+      }
+    }
+  },
+
+  computed: {
+    // 右键菜单是否当前页签
+    isContextTabActived () {
+      return this.contextmenu.id === this.activedTab
+    },
+
+    // 右键页签是否允许关闭
+    isContextTabCanBeClosed () {
+      let { items, contextmenu } = this
+      let contextTab = items[contextmenu.index]
+      return items.length > 1 && contextTab && contextTab.closable !== false
+    },
+
+    // 左侧可关闭的页签
+    tabsLeft () {
+      let { items, contextmenu: { id, index } } = this
+      return id ? items.slice(0, index).filter(({ closable }) => closable !== false) : emptyArray
+    },
+
+    // 左侧可关闭的页签
+    tabsRight () {
+      let { items, contextmenu: { id, index } } = this
+      return id ? items.slice(index + 1).filter(({ closable }) => closable !== false) : emptyArray
+    },
+
+    // 其他可关闭的页签
+    tabsOther () {
+      let { items, contextmenu: { id } } = this
+      return id ? items.filter(({ closable, id: tid }) => closable !== false && id !== tid) : emptyArray
+    }
+  },
+
+  watch: {
+    // 路由切换更新激活的页签
+    $route ($route) {
+      this.hideContextmenu()
+    },
+
+    // 监听右键菜单显示关闭
+    'contextmenu.id' (val, old) {
+      if (!old && val) {
+        // 显示右键菜单,绑定点击关闭事件
+        document.addEventListener('click', this.onClick = (e) => {
+          if (e.target !== this.$el.querySelector('.router-tab-contextmenu')) {
+            this.hideContextmenu()
+          }
+        })
+      } else if (old && !val) {
+        // 隐藏右键菜单,移除点击关闭事件
+        document.removeEventListener('click', this.onClick)
+      }
+    }
+  },
+
+  methods: {
+    // 显示页签右键菜单
+    showContextmenu (id, index, e) {
+      // 菜单定位
+      let { clientY: top, clientX: left } = e || emptyObj
+      Object.assign(this.contextmenu, { id, index, top, left })
+    },
+
+    // 关闭页签右键菜单
+    hideContextmenu () {
+      this.showContextmenu(null, -1)
+    },
+
+    // 关闭多个页签
+    async closeMulti (tabs) {
+      let { items, $router, contextmenu } = this
+      let nextTab = items.find(({ id }) => id === contextmenu.id)
+
+      for (let { id } of tabs) {
+        try {
+          await this.removeTab(id)
+        } catch (e) {}
+      }
+
+      // 当前页签如已关闭,则打开右键选中页签
+      if (items.findIndex(({ id }) => id === this.activedTab) === -1) {
+        $router.replace(nextTab.to)
+      }
+    }
+  }
+}

+ 34 - 0
src/components/RouterTab/i18n.js

@@ -0,0 +1,34 @@
+// 语言配置
+import langs from '../../lang'
+
+// 国际化
+export default {
+  props: {
+    // 语言配置
+    // - 为字符串时,可以设置为内置的语言 'zh-CN' (默认) 和 'en'
+    // - 为对象时,可设置自定义的语言
+    i18n: {
+      type: [ String, Object ],
+      default: 'zh-CN'
+    }
+  },
+
+  computed: {
+    // 语言内容
+    lang () {
+      let lang = null
+      let i18n = this.i18n
+
+      if (typeof i18n === 'string') {
+        lang = langs[i18n]
+      } else if (typeof i18n === 'object') {
+        lang = i18n
+      }
+
+      // 找不到语言配置,则使用英文
+      if (!lang) lang = langs['en']
+
+      return lang
+    }
+  }
+}

+ 1 - 1
src/mixins/routerTab/iframe.js → src/components/RouterTab/iframe.js

@@ -1,4 +1,4 @@
-// iframe 页签功能混入
+// iframe 页签
 export default {
   data () {
     return {

+ 278 - 0
src/components/RouterTab/index.js

@@ -0,0 +1,278 @@
+import Vue from 'vue'
+
+// 方法
+import { emptyObj } from '../../util'
+import { getPathWithoutHash } from '../../util/route'
+
+// 子组件
+import RouterAlive from '../RouterAlive'
+
+// 功能模块混入
+import contextmenu from './contextmenu'
+import i18n from './i18n'
+import iframe from './iframe'
+import pageLeave from './pageLeave'
+import rule from './rule'
+import scroll from './scroll'
+
+// RouterTab 组件
+export default {
+  name: 'RouterTab',
+  components: { RouterAlive },
+  mixins: [ contextmenu, i18n, iframe, pageLeave, rule, scroll ],
+  props: {
+    // 初始页签数据
+    tabs: {
+      type: Array,
+      default: () => []
+    },
+
+    // router-view组件配置
+    routerView: Object,
+
+    // 页签过渡效果
+    tabTransition: {
+      type: [ String, Object ],
+      default: 'router-tab-zoom'
+    },
+
+    // 页面过渡效果
+    pageTransition: {
+      type: [ String, Object ],
+      default: () => ({
+        name: 'router-tab-swap',
+        mode: 'out-in'
+      })
+    }
+  },
+
+  data () {
+    return {
+      loading: false, // 路由页面loading
+      items: [], // 页签项
+      activedTab: null, // 当前激活的页签
+      isRouterAlive: true
+    }
+  },
+
+  watch: {
+    // 路由切换更新激活的页签
+    $route ($route) {
+      this.loading = false
+      this.updateActivedTab()
+      this.fixCommentPage()
+    }
+  },
+
+  beforeCreate () {
+    // 添加到原型链
+    Vue.prototype.$routerTab = this
+
+    // 获取跟路径
+    let matched = this.$route.matched
+    this.basePath = (matched[matched.length - 2] || {}).path
+  },
+
+  created () {
+    this.getTabItems()
+    this.updateActivedTab()
+  },
+
+  methods: {
+    // 根据初始页签数据生成页签列表
+    getTabItems () {
+      let { tabs, $router } = this
+      let ids = {}
+
+      this.items = tabs.map((item, index) => {
+        let { to, closable, title, tips } = typeof item === 'string'
+          ? { to: item }
+          : (item || emptyObj)
+        let route = to && $router.match(to)
+
+        if (route) {
+          let tab = this.getRouteTab(route)
+          let id = tab.id
+
+          // 根据id去重
+          if (!ids[id]) {
+            // 初始 tab 数据
+            if (title) tab.title = title
+            if (tips) tab.tips = tips
+            tab.closable = closable !== false
+
+            return (ids[id] = tab)
+          }
+        }
+      }).filter(item => !!item)
+    },
+
+    // 更新激活的页签
+    updateActivedTab () {
+      this.activedTab = this.getAliveId()
+    },
+
+    // 更新tab数据
+    updateTab (key, { route, tab }) {
+      let { items } = this
+      let matchIdx = items.findIndex(({ id }) => id === key)
+
+      let item = Object.assign(this.getRouteTab(route), tab)
+
+      if (matchIdx > -1) {
+        let matchTab = items[matchIdx]
+        item.closable = matchTab.closable !== false
+        this.$set(items, matchIdx, item)
+      } else {
+        items.push(item)
+      }
+    },
+
+    // 从路由地址获取 aliveId
+    getIdByLocation (location, fullMatch = true) {
+      if (!location) return
+
+      let $route = this.$router.match(location, this.$router.currentRoute)
+
+      // 路由地址精确匹配页签
+      if (fullMatch) {
+        let matchPath = getPathWithoutHash($route)
+        let matchTab = this.items.find(({ to }) => to.split('#')[0] === matchPath)
+
+        if (matchTab) {
+          return matchTab.id
+        }
+      } else {
+        return this.getAliveId($route)
+      }
+    },
+
+    // 从route中获取tab数据
+    getRouteTab (route) {
+      let id = this.getAliveId(route)
+      let { fullPath: to, meta } = route
+      let { title, icon, tips } = meta
+
+      return { id, to, title, icon, tips }
+    },
+
+    // 移除tab项
+    async removeTab (id) {
+      let { items } = this
+      let $alive = this.$refs.routerAlive
+      const idx = items.findIndex(item => item.id === id)
+
+      if (items.length === 1) {
+        throw new Error(this.lang.msg.keepOneTab)
+      }
+
+      try {
+        await this.pageLeavePromise(id, 'close')
+
+        // 承诺关闭后移除页签和缓存
+        $alive.remove(id)
+        idx > -1 && items.splice(idx, 1)
+      } catch (e) {}
+    },
+
+    // 通过路由地址关闭页签
+    close (location, fullMatch = true) {
+      if (location) {
+        let id = this.getIdByLocation(location, fullMatch)
+        if (id) {
+          this.closeTab(id)
+        }
+      } else {
+        this.closeTab()
+      }
+    },
+
+    // 通过页签id关闭页签
+    async closeTab (id = this.activedTab) {
+      let { activedTab, items, $router } = this
+      const idx = items.findIndex(item => item.id === id)
+
+      try {
+        await this.removeTab(id)
+
+        // 如果关闭当前页签,则打开后一个页签
+        if (activedTab === id) {
+          let nextTab = items[idx] || items[idx - 1]
+          $router.replace(nextTab.to)
+        }
+      } catch (e) {
+        console.warn(e)
+      }
+    },
+
+    // 通过路由地址刷新页签
+    refresh (location, fullMatch = true) {
+      if (location) {
+        let id = this.getIdByLocation(location, fullMatch)
+        if (id) {
+          this.refreshTab(id)
+        }
+      } else {
+        this.refreshTab()
+      }
+    },
+
+    // 刷新指定页签
+    async refreshTab (id = this.activedTab) {
+      try {
+        await this.pageLeavePromise(id, 'refresh')
+        this.$refs.routerAlive.clear(id)
+        if (id === this.activedTab) this.reloadRouter()
+      } catch (e) {}
+    },
+
+    /**
+     * 刷新所有页签
+     * @param {boolean} [force=false] 是否强制刷新,如果强制则忽略页面beforePageLeave
+     */
+    async refreshAll (force = false) {
+      const $alive = this.$refs.routerAlive
+      const { cache } = $alive
+      for (const id in cache) {
+        if (!force) {
+          try {
+            await this.pageLeavePromise(id, 'refresh')
+            $alive.clear(id)
+          } catch (e) {}
+        } else {
+          $alive.clear(id)
+        }
+      }
+      this.reloadRouter()
+    },
+
+    // 重载路由组件
+    async reloadRouter (ignoreTransition = false) {
+      this.isRouterAlive = false
+
+      // 默认在页面过渡结束后会设置 isRouterAlive 为 true
+      // 如果过渡事件失效,则需传入 ignoreTransition 为 true 手动更改
+      if (ignoreTransition) {
+        await this.$nextTick()
+        this.isRouterAlive = true
+      }
+    },
+
+    // 页签过渡结束
+    onTabTransitionEnd () {
+      this.adjust()
+    },
+
+    // 页面过渡结束
+    onPageTransitionEnd () {
+      if (!this.isRouterAlive) this.isRouterAlive = true
+    },
+
+    // 修复:当快速频繁切换页签时,旧页面离开过渡效果尚未完成,新页面内容无法正常mount,内容节点为comment类型
+    fixCommentPage () {
+      if (this.$refs.routerAlive.$el.nodeType === 8) {
+        this.reloadRouter(true)
+      }
+    }
+  }
+}

+ 54 - 0
src/components/RouterTab/pageLeave.js

@@ -0,0 +1,54 @@
+import { emptyObj } from '../../util'
+import { isAlikeRoute } from '../../util/route'
+
+// 页面离开
+export default {
+  created () {
+    this.$router.beforeEach(this.routerPageLeaveGuard)
+  },
+
+  methods: {
+    // 页面离开导航守卫
+    routerPageLeaveGuard (to, from, next) {
+      if (this._isDestroyed) {
+        let hooks = this.$router.beforeHooks
+        let idx = hooks.indexOf(this.routerPageLeaveGuard)
+
+        // 移除已销毁的RouterTab实例注册的导航守卫
+        if (idx > -1) hooks.splice(idx, 1)
+
+        next()
+      } else {
+        const id = this.getAliveId(to)
+        const $alive = this.$refs.routerAlive
+        const { route: cacheRoute } = ($alive && $alive.cache[id]) || emptyObj
+
+        // 如果不是相同路由则检查beforePageLeave
+        if (cacheRoute && !isAlikeRoute(to, cacheRoute)) {
+          this.pageLeavePromise(id, 'replace')
+            .then(() => next())
+            .catch(() => next(false))
+        } else {
+          next()
+        }
+      }
+    },
+
+    // 页面离开Promise
+    pageLeavePromise (id, type) {
+      return new Promise((resolve, reject) => {
+        let $alive = this.$refs.routerAlive
+        let tab = this.items.find(item => item.id === id) // 当前页签
+        let { vm } = $alive.cache[id] || emptyObj // 缓存数据
+        let beforePageLeave = vm && vm.$vnode.componentOptions.Ctor.options.beforePageLeave
+
+        if (typeof beforePageLeave === 'function') {
+          // 页签关闭前
+          beforePageLeave.bind(vm)(resolve, reject, tab, type)
+        } else {
+          resolve()
+        }
+      })
+    }
+  }
+}

+ 40 - 0
src/components/RouterTab/rule.js

@@ -0,0 +1,40 @@
+// 内置规则
+const rules = {
+  // 地址,例如:"/page/1?type=a#title" 则取 "/page/1"
+  path (route) {
+    return route.path
+  },
+
+  // 完整地址 (忽略hash),例如:"/page/1?type=a#title" 则取 "/page/1?type=a"
+  fullpath (route) {
+    return route.fullPath.replace(route.hash, '')
+  }
+}
+
+// 页签缓存规则
+export default {
+  props: {
+    // 缓存id,如果为函数,则参数为route
+    aliveId: {
+      type: [ String, Function ],
+      default: 'path'
+    }
+  },
+
+  methods: {
+    // 获取缓存 id
+    getAliveId (route = this.$route) {
+      let rule = (route.meta && route.meta.aliveId) || this.aliveId
+
+      if (typeof rule === 'string') {
+        rule = rules[rule.toLowerCase()]
+      }
+
+      if (typeof rule !== 'function') {
+        rule = rules.path
+      }
+
+      return rule.bind(this)(route)
+    }
+  }
+}

+ 62 - 0
src/components/RouterTab/scroll.js

@@ -0,0 +1,62 @@
+import { debounce } from '../../util'
+import { scrollTo } from '../../util/dom'
+
+// 页签滚动
+export default {
+  watch: {
+    async activedTab () {
+      // 激活页签时,如果当前页签不在可视区域,则滚动显示页签
+      await this.$nextTick()
+
+      let $cur = this.$el.querySelector('.router-tab-nav .actived')
+      let $scr = this.$el.querySelector('.router-tab-scroll')
+      if ($cur) {
+        let cLeft = $cur.offsetLeft
+        let sLeft = $scr.scrollLeft
+        if (cLeft < sLeft || cLeft + $cur.clientWidth > sLeft + $scr.clientWidth) {
+          this.adjust()
+        }
+      }
+    }
+  },
+
+  created () {
+    this.$nextTick(this.adjust)
+  },
+
+  mounted () {
+    // 页面载入和浏览器窗口大小改变时调整Tab滚动显示
+    window.addEventListener('resize', this.onResize = debounce(this.adjust))
+  },
+
+  destroyed () {
+    // 销毁后移除监听事件
+    window.removeEventListener('resize', this.onResize)
+  },
+
+  methods: {
+    // Tab滚动
+    tabScroll (direction) {
+      let $tab = this.$el.querySelector('.router-tab-header')
+      let $scr = $tab.querySelector('.router-tab-scroll')
+      let space = $tab.clientWidth - 110
+
+      scrollTo($scr, $scr.scrollLeft + (direction === 'left' ? -space : space))
+    },
+
+    // 调整Tab滚动显示
+    adjust () {
+      let $tab = this.$el.querySelector('.router-tab-header')
+      let $scr = $tab.querySelector('.router-tab-scroll')
+      let $nav = $scr.querySelector('.router-tab-nav')
+      let $cur = $nav.querySelector('.actived')
+      let isScroll = $nav.clientWidth > $scr.clientWidth // 判断是否需要滚动
+
+      $tab.classList[isScroll ? 'add' : 'remove']('is-scroll')
+
+      if ($cur && isScroll) {
+        scrollTo($scr, $cur.offsetLeft + ($cur.clientWidth - $scr.clientWidth) / 2)
+      }
+    }
+  }
+}

+ 1 - 1
src/index.js

@@ -1,4 +1,4 @@
-import RouterTab from './components/RouterTab.vue'
+import RouterTab from './components/RouterTab/RouterTab.vue'
 import routerPage from './mixins/routerPage'
 import routes from './util/routes'
 

+ 6 - 0
src/page/Iframe.vue

@@ -3,18 +3,22 @@
 </template>
 
 <script>
+// iframe 页签页面
 export default {
   name: 'Iframe',
+
   props: {
     src: String,
     title: String,
     icon: String
   },
+
   data () {
     return {
       routeTab: null
     }
   },
+
   mounted () {
     let { src, title, icon, $routerTab: $tab } = this
     let { iframes } = $tab
@@ -24,6 +28,7 @@ export default {
     if (!iframes.includes(src)) {
       iframes.push(src)
     }
+
     $tab.currentIframe = src
   },
 
@@ -35,6 +40,7 @@ export default {
     this.$routerTab.currentIframe = null
   },
 
+  // 组件销毁后移除 iframe
   destroyed () {
     let { src } = this
     let { iframes } = this.$routerTab

+ 0 - 16
src/util/alive.js

@@ -1,16 +0,0 @@
-import rules from './rules'
-
-// 获取缓存 id
-export function getAliveId (route = this.$route) {
-  let rule = (route.meta && route.meta.aliveId) || this.aliveId
-
-  if (typeof rule === 'string') {
-    rule = rules[rule.toLowerCase()]
-  }
-
-  if (typeof rule !== 'function') {
-    rule = rules.path
-  }
-
-  return rule.bind(this)(route)
-}

+ 0 - 12
src/util/rules.js

@@ -1,12 +0,0 @@
-// 页签规则
-export default {
-  // 地址,例如:"/page/1?type=a#title" 则取 "/page/1"
-  path (route) {
-    return route.path
-  },
-
-  // 完整地址 (忽略hash),例如:"/page/1?type=a#title" 则取 "/page/1?type=a"
-  fullpath (route) {
-    return route.fullPath.replace(route.hash, '')
-  }
-}