Implementing Multi‑Instance Tab Navigation in Vue‑Router for Backend Admin Systems
This article analyzes the challenges of implementing multi‑instance tab navigation in mobile‑friendly web pages using Vue‑router, explains why standard router components are single‑instance, and presents two solutions—dynamic route addition and route‑change hijacking—complete with code examples and considerations for memory management.
Background : Mobile web pages usually have simple routing needs, but backend admin systems (e.g., logistics, merchant, customer management) require a browser‑like multi‑tab navigation because users stay on the page for a long time and need to switch between multiple detail pages.
Current situation : Common UI libraries such as Ant Design (AntD) and iView do not provide a multi‑instance tab component. Even admin frameworks like antd‑pro and iview‑admin lack this core feature, so developers must implement it themselves.
Why a single instance occurs : In vue‑router the router‑view component caches a component instance by its name . When a new route with the same component is pushed, the previous instance is replaced because the router renders a one‑to‑one component from the matched record.
Solution Overview : Two approaches are proposed to achieve multi‑instance tabs.
Solution 1 – Dynamically add routes
By creating a component factory that receives a unique id (e.g., order ID) and returns a component definition, we can generate a distinct component for each detail page. The factory‑generated component is cached globally, and a dynamic route object is built and added via router.addRoutes .
function OrderDetailFactory(params = {}) {
const { orderId } = params;
const extName = `-${orderId}`;
const Comp = {
name: `order-info${extName}`,
props: [...],
data() { /* ... */ },
methods: { /* ... */ },
render() { /* ... */ },
mounted() { /* ... */ },
beforeDestroy() {
const orderTimeStatus = setTimeout(() => {
window.ticketCache[orderId] = null;
clearTimeout(ticketTimeStatus);
});
}
};
return Comp;
}
async function orderDetail(orderId = '0') {
const _orderId = orderId !== '0' ? orderId : '0';
const postTitle = orderId !== '0' ? orderId : '新建';
if (!window.ticketCache) { window.ticketCache = {}; }
const { OrderDetailFactory } = await import('@/views/orderDetail/index.js');
const OrderDetail = OrderDetailFactory({ orderId: _orderId });
const { name: compName } = OrderDetail;
window.orderCache[_orderId] = OrderDetail;
return {
path: '/order/:orderId',
title: `订单详情-${postTitle}`,
name: compName,
component: window.orderCache[_orderId],
// ...other route fields
};
}
function addRouter(context, currRouter) {
const router = context.$router;
// other logic ...
router.addRoutes && router.addRoutes(currRouter);
}
async function jumpToDyRouter(context, orderId) {
const currRouter = await orderDetail(orderId);
const { name: routerName } = currRouter;
addRouter(context, currRouter);
const timeStatus = setTimeout(function () {
context.$router.push({ name: routerName });
context = null;
clearTimeout(timeStatus);
}, 0);
}This method creates a new route for each id , allowing the router to render a separate component instance. However, the dynamically added routes remain in memory because vue‑router does not provide a way to remove them, which may cause memory leaks.
Solution 2 – Hijack route changes
Instead of relying on router‑view , we watch the route change, keep a list of opened tabs ( tagList ), and render the corresponding component manually, controlling its visibility with CSS. This avoids the one‑to‑one limitation.
export default {
data() {
return {
tagList: [], // opened tabs
activeKey: ''
};
},
watch: {
$route(to) { this.handlePathChange(to); }
},
methods: {
handlePathChange(route) {
const { fullPath, name, params, query = {}, matched = [] } = route;
const [currMatch] = matched.slice(-1);
const { components } = currMatch || {};
const { default: Comp } = components || {};
const uniqueName = this.createName(name, params);
const findRes = this.tagList.find(item => item.uniqueName === uniqueName);
if (findRes) { this.activeKey = uniqueName; return; }
const currRoute = { fullPath, uniqueName, component: Comp, params, query };
this.tagList = [...this.tagList, currRoute];
this.activeKey = uniqueName;
},
createName(name, params) {
const uniqueKey = Object.values(params).reduce((dist, item) => `${dist ? dist + '-' : ''}${item}`, '');
return `${name}-${uniqueKey}`;
}
},
render() {
return (
{this.tagList.map(item => {
const { component: Component, uniqueName, query, params } = item;
const isActive = uniqueName === this.activeKey;
const otherProps = { props: { ...params, ...query } };
return (
);
})}
);
}
};By storing each component instance in tagList and toggling its display style, we can keep multiple detail pages alive simultaneously and release them manually when a tab is closed or when a maximum tab count is exceeded.
Value of multi‑instance routing : The first solution leverages addRoutes to bypass the router’s one‑to‑one restriction, while the second gives full control over component lifecycle and memory usage. Both improve user efficiency in complex backend systems where users need to work with several detail pages at once.
Conclusion : Backend systems have diverse business scenarios; on mobile they focus on load performance, but on PC they must also consider memory consumption. When implementing business code, developers should choose an appropriate multi‑instance tab strategy and manage resources carefully. The article thanks the Zhuanzhuan FE team for their practical experience.
转转QA
In the era of knowledge sharing, discover 转转QA from a new perspective.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.