vue-nnn-router

File-based SPA routing for Vue Router 4–5 — build RouteRecordRaw from Vite import.meta.glob (index.vue, _layout.vue, _redirect.ts, [param], _middleware cascade).
1,575
Install
npm install vue-nnn-router
Latest Version:0.1.1
Vue:3
License:MIT
Last Updated:Oct 20, 2026
Links: GitHub  ·  npm
Maintainer: theanhit96

vue-nnn-router

File-based routing cho SPA dùng Vue Router. Chuyển map từ Vite import.meta.glob thành routes có layout, dynamic segment, middleware, redirect, tên route, scroll behavior, progress bar và tùy chọn smooth scrolling bằng Lenis.

English · Tiếng Việt · npm · Changelog

Tính năng

  • index.vue, page lồng nhau và _layout.vue
  • [id].vue hoặc [id]/index.vue → :id
  • _middleware.ts kế thừa theo thư mục
  • _redirect.ts làm page mặc định cho layout
  • Page mặc định eager-load, tùy chọn lazy-load; middleware và redirect eager-load
  • Tên route ổn định và có thể sinh file hằng số
  • Scroll khi chuyển route và cấu hình scroll theo từng page
  • Progress bar và smooth scrolling liên tục bằng Lenis

Cài đặt

npm install vue-nnn-router

Yêu cầu:

  • Vue ^3.3
  • Vue Router ^4.2 hoặc ^5.0
  • Vite, hoặc công cụ khác cung cấp map có cấu trúc như import.meta.glob

Chỉ cần cài Lenis khi dùng smooth scrolling liên tục:

npm install lenis

Bắt đầu nhanh

Tạo các page trong src/pages:

src/pages/
├── index.vue                 # /
├── about.vue                 # /about
└── users/
    ├── _layout.vue           # bọc /users/**; cần render <RouterView />
    ├── _middleware.ts        # chạy cho /users/**
    ├── index.vue             # /users
    ├── add.vue               # /users/add
    └── [id].vue              # /users/:id

Mặc định page, middleware và redirect được eager-load. Thêm plugin vào Vite một lần:

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueNnnRouterPlugin } from "vue-nnn-router/vite";

export default defineConfig({
  plugins: [vue(), vueNnnRouterPlugin()],
});

Sau đó khởi tạo router bằng đường dẫn thư mục:

// src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import { createNnnRoutes } from "vue-nnn-router";

const routes = createNnnRoutes("src/pages");

export const router = createRouter({
  history: createWebHistory(),
  routes,
});

Mount router như bình thường:

// src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";

createApp(App).use(router).mount("#app");

Khởi tạo bằng đường dẫn

createNnnRoutes("src/pages", options?) cần vueNnnRouterPlugin(). Đường dẫn phải là chuỗi literal truyền trực tiếp vào hàm và tính từ Vite root; "src/pages", "/src/pages" và "./src/pages" tương đương. Plugin tự đặt routesRoot; options vẫn nhận CreateNnnRoutesOptions:

const routes = createNnnRoutes("src/pages", {
  prefix: "app",
  silent: true,
});

Page/layout .vue, .tsx, .jsx mặc định được eager-load (lazy: false). Đặt lazy: true để chỉ tải component khi vào route:

const routes = createNnnRoutes("src/pages", { lazy: true });

lazy phải là boolean literal trong object options truyền trực tiếp. Các trường khác vẫn có thể dùng giá trị runtime. Nếu có options từ biến hoặc hàm, dùng { ...options, lazy: true } hoặc { ...options, lazy: false } để plugin xác định chế độ tải lúc build. Tùy chọn này chỉ áp dụng cho API đường dẫn; API glob map giữ nguyên cách tải do glob quyết định.

_middleware.{ts,js} và _redirect.{ts,js} luôn eager-load. Trong chế độ lazy, nếu page export middleware, thêm glob eager vào plugin:

vueNnnRouterPlugin({ eager: ["src/pages/users/add.vue"] });

Không dùng biến hay đường dẫn tính lúc runtime cho cú pháp này. Khi cần glob tùy chỉnh, page .ts/.js hoặc không dùng plugin, API glob map vẫn hoạt động:

import { createNnnModules, createNnnRoutes } from "vue-nnn-router";

const modules = createNnnModules({
  views: import.meta.glob("/src/pages/**/*.{vue,tsx,jsx}"),
  eager: import.meta.glob("/src/pages/**/_{middleware,redirect}.{ts,js}", {
    eager: true,
  }),
});
const routes = createNnnRoutes(modules, { routesRoot: "src/pages" });

Quy ước file

File Kết quả
index.vue URL của thư mục hiện tại
about.vue Thêm /about
[id].vue Thêm /:id
[id]/index.vue Cũng thêm /:id
_layout.vue Bọc các route bên dưới thư mục
_middleware.ts Chạy trước mọi route bên dưới thư mục
_redirect.ts Redirect một layout không có page index

Page component có thể dùng .vue, .tsx, .jsx, .ts hoặc .js. Hãy thêm extension bạn sử dụng vào glob views.

File _layout.vue phải render <RouterView />:

<template>
  <main class="users-layout">
    <RouterView />
  </main>
</template>

routesRoot

routesRoot loại bỏ phần không thuộc URL khỏi mỗi glob key:

Glob routesRoot
/src/pages/** src/pages
../pages/** ../pages
Key đã có dạng users/index.vue bỏ qua tùy chọn này

Khi truyền glob map /src/pages/**, hãy dùng routesRoot: "src/pages". Khi truyền đường dẫn chuỗi, plugin tự suy ra giá trị này.

Tùy chọn route

createNnnRoutes(modules, {
  routesRoot: "src/pages",
  prefix: "app",
  onDuplicate: "first-wins",
  verbose: false,
  silent: false,
});
Tùy chọn Mặc định Công dụng
lazy false Với API đường dẫn: đặt true để lazy-load page/layout
routesRoot — Loại prefix filesystem khỏi glob key
prefix — Thêm một URL prefix cho mọi route
onDuplicate "first-wins" Xử lý hai file sinh cùng URL; cũng nhận "last-wins" hoặc callback
verbose false In bảng path và file đã sinh
logger console.log Thay logger dùng cho verbose
silent false Ẩn warning và output của verbose

Middleware và redirect

Middleware thư mục chạy từ cha xuống con, sau đó mới tới middleware của page:

pages/_middleware.ts
→ pages/users/_middleware.ts
→ middleware export từ pages/users/add.vue

Export một guard hoặc một mảng guard:

// src/pages/users/_middleware.ts
import type { NavigationGuard } from "vue-router";

const requireUser: NavigationGuard = (to) => {
  if (!hasSession() && to.path !== "/login") return "/login";
};

export default requireUser;
// export default [logVisit, requireUser];

Luôn tải _middleware.ts ở chế độ eager. Export middleware nằm trong page chỉ khả dụng khi chính page đó được eager-load, vì vậy middleware thư mục là lựa chọn phù hợp với page lazy-load.

Dùng _redirect.ts cạnh _layout.vue nếu thư mục không có page index:

// src/pages/users/_redirect.ts
export default "add"; // /users → /users/add

Khi bật prefix, nên dùng target tương đối. Target tuyệt đối như "/users/add" được giữ nguyên.

Tên route

Tên được sinh từ URL:

URL route.name Key hằng số
/ home home
/users/add users-add usersAdd
/users/:id users-id usersId

Để lấy map tại runtime:

import { createNnnRoutesWithNames } from "vue-nnn-router";

const { routes, routeNames } = createNnnRoutesWithNames(modules, {
  routesRoot: "src/pages",
});

router.push({ name: routeNames.usersAdd });

Để tự động sinh file ROUTER_NAME, thêm vueNnnRouterNamesPlugin() vào danh sách plugin đang dùng. Không cần truyền tham số nếu pages nằm trong src/pages:

// vite.config.ts
import { defineConfig } from "vite";
import {
  vueNnnRouterPlugin,
  vueNnnRouterNamesPlugin,
} from "vue-nnn-router/vite";

export default defineConfig({
  plugins: [
    vueNnnRouterPlugin(),
    vueNnnRouterNamesPlugin(),
  ],
});
import { ROUTER_NAME } from "./router-name";

router.push({ name: ROUTER_NAME.usersAdd });

Mặc định plugin quét page/layout .vue, .tsx, .jsx và _redirect.{ts,js} trong src/pages, rồi tạo file src/router/router-name.ts.

Tùy chọn Mặc định Ý nghĩa
rootPath "src" Thư mục gốc, tính từ Vite root
folderPages "pages" Thư mục pages bên trong rootPath
outFile "router/router-name.ts" File đầu ra bên trong rootPath

Plugin tự ghép routesRoot = join(rootPath, folderPages) và đường dẫn đầu ra join(rootPath, outFile). Cả ba tùy chọn và object cấu hình đều có thể bỏ qua.

vueNnnRouterPlugin() hỗ trợ cú pháp createNnnRoutes("src/pages"); vueNnnRouterNamesPlugin() sinh file hằng số tên route. Khi cần cả hai chức năng, dùng cả hai plugin như trên.

Có thể chỉ truyền tùy chọn cần đổi:

vueNnnRouterNamesPlugin({ outFile: "constants/router-name.ts" });
// Xuất: src/constants/router-name.ts

Nếu dùng thư mục khác:

vueNnnRouterNamesPlugin({
  rootPath: "src/app",
  folderPages: "views",
  outFile: "constants/router-name.ts",
});
// Quét: src/app/views
// Xuất: src/app/constants/router-name.ts

rootPath: "" cho phép dùng thư mục ngay dưới Vite root. Nếu router dùng prefix, truyền cùng giá trị vào names plugin để tên route khớp nhau; silent vẫn được hỗ trợ và mặc định là true.

Khi chuyển từ API cũ, thay pages/routesRoot bằng rootPath/folderPages, và bỏ phần rootPath khỏi outFile cũ. Plugin dùng Vite root của dự án thay cho tùy chọn root trước đây.

Cuộn trang

Thư viện có hai chức năng riêng biệt:

Chức năng Dùng khi
createNnnScrollBehavior Chọn vị trí page sau khi chuyển route: đầu trang, vị trí cũ hoặc hash
createNnnSmoothScroll Làm mượt liên tục theo kiểu Lenis khi người dùng cuộn

Cuộn sau khi chuyển route

import { createNnnScrollBehavior } from "vue-nnn-router";

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior: createNnnScrollBehavior({
    restorePosition: true,
    scrollToHash: true,
    top: 0,
  }),
});

Các tùy chọn:

Tùy chọn Mặc định Công dụng
smooth false Animate khi cuộn sau chuyển route
restorePosition true Khôi phục vị trí khi Back/Forward
scrollToHash true Cuộn tới phần tử từ URL hash
top / left 0 Offset theo pixel
skipMetaKey "noScroll" Bỏ qua nếu route meta tương ứng là truthy; dùng false để tắt cơ chế này
scrollMap — Áp dụng cấu hình riêng đã sinh cho từng page

Cấu hình scroll theo từng page

Thêm scroll plugin một lần:

// vite.config.ts
import { defineConfig } from "vite";
import { vueNnnRouterScrollPlugin } from "vue-nnn-router/vite";

export default defineConfig({
  plugins: [
    vueNnnRouterScrollPlugin(),
  ],
});

Mặc định plugin quét page/layout .vue, .tsx, .jsx trong src/pages và sinh src/router/router-scroll.ts, gồm cả NNN_SCROLL và NNN_SMOOTH_SCROLL.

Tùy chọn Mặc định Ý nghĩa
rootPath "src" Thư mục gốc, tính từ Vite root
folderPages "pages" Thư mục pages bên trong rootPath
outFile "router/router-scroll.ts" File đầu ra bên trong rootPath
silent false Tắt cảnh báo khi không đọc được cấu hình tĩnh

Giống names plugin, thư mục quét là join(rootPath, folderPages) và file đầu ra là join(rootPath, outFile). Có thể bỏ qua toàn bộ cấu hình hoặc chỉ truyền tùy chọn cần đổi:

vueNnnRouterScrollPlugin({ outFile: "constants/router-scroll.ts" });
// Xuất: src/constants/router-scroll.ts

vueNnnRouterScrollPlugin({
  rootPath: "src/app",
  folderPages: "views",
  outFile: "constants/router-scroll.ts",
});
// Quét: src/app/views
// Xuất: src/app/constants/router-scroll.ts

rootPath: "" cho phép dùng thư mục ngay dưới Vite root. Khi chuyển từ API cũ, thay root/pages bằng Vite root và rootPath/folderPages, rồi bỏ phần rootPath khỏi outFile cũ.

Truyền navigation map vào router:

import { createNnnScrollBehavior } from "vue-nnn-router";
import { NNN_SCROLL } from "./router-scroll";

scrollBehavior: createNnnScrollBehavior({ scrollMap: NNN_SCROLL });

Sau đó cấu hình một page bằng giá trị literal:

<script setup lang="ts">
import { defineNnnScroll } from "vue-nnn-router";

defineNnnScroll({ top: 80, smooth: true });
// defineNnnScroll(false); // giữ nguyên vị trí hiện tại trên page này
</script>

Smooth scrolling liên tục với Lenis

Cài Lenis và import CSS:

npm install lenis
// src/main.ts
import "lenis/dist/lenis.css";

Khởi tạo controller một lần sau khi tạo router. Scroll plugin phía trên đồng thời sinh NNN_SMOOTH_SCROLL:

import { createNnnSmoothScroll } from "vue-nnn-router/smooth-scroll";
import { NNN_SMOOTH_SCROLL } from "./router-scroll";

const smoothScroll = createNnnSmoothScroll(router, {
  disableOnMobile: true,
  mobileBreakpoint: 767,
  scrollMap: NNN_SMOOTH_SCROLL,
  lenisOptions: {
    lerp: 0.1,
    smoothWheel: true,
  },
});

Bật hoặc tắt theo từng page:

<script setup lang="ts">
import { defineNnnSmoothScroll } from "vue-nnn-router";

defineNnnSmoothScroll(false); // page này dùng native scrolling

// Hoặc override cấu hình mobile toàn cục:
// defineNnnSmoothScroll({ enabled: true, disableOnMobile: false });
</script>
Tùy chọn Mặc định Công dụng
enabled true Trạng thái mặc định cho page không có override
disableOnMobile false Hủy Lenis và dùng native scrolling trên mobile
mobileBreakpoint 767 Chiều rộng mobile tối đa theo pixel
scrollMap — Áp dụng cấu hình riêng đã sinh cho từng page
lenisOptions { autoRaf: true } Chuyển các tùy chọn sang Lenis

Controller tự tạo hoặc hủy Lenis khi route và viewport thay đổi. Gọi smoothScroll.destroy() khi dispose router trong HMR, test hoặc micro-frontend.

Cả defineNnnScroll(...) và defineNnnSmoothScroll(...) phải nhận object hoặc boolean literal để Vite plugin có thể đọc được.

Progress bar khi chuyển trang

Progress bar không cần CSS hay dependency bên thứ ba:

import { createNnnProgress } from "vue-nnn-router/progress";

const progress = createNnnProgress(router, {
  color: "#ff4d00",
  height: 3,
  position: "top",
  delay: 120,
});
Tùy chọn Mặc định Công dụng
enabled true Bật progress bar
color "#ff4d00" Màu thanh và glow
height 3 Độ dày CSS; số được hiểu là pixel
position "top" "top" hoặc "bottom"
delay 120 Chờ trước khi hiện để tránh nhấp nháy
minimumVisible 0 Thời gian hiển thị tối thiểu theo mili giây
zIndex 2147483647 Thứ tự xếp lớp

Gọi progress.destroy() khi cần cleanup. Import createNnnProgress từ vue-nnn-router vẫn hoạt động để tương thích ngược.

Các đường dẫn import

Import Export chính
vue-nnn-router Sinh route, helper cho module, tên route, navigation scroll và khai báo theo page
vue-nnn-router/progress createNnnProgress
vue-nnn-router/smooth-scroll createNnnSmoothScroll và các type liên quan
vue-nnn-router/vite Plugin routes từ đường dẫn, sinh file tên route và cấu hình scroll

Mỗi leaf route được sinh có meta.nnnFile, chứa glob key gốc để debug. Package đã bao gồm TypeScript declarations.

Lỗi thường gặp

  • URL chứa /src/pages: đặt routesRoot: "src/pages" cho glob /src/pages/**.
  • Page lồng nhau bị trống: kiểm tra _layout.vue có render <RouterView />.
  • Middleware không chạy: tải _middleware.ts với { eager: true }.
  • Cấu hình scroll theo page không hoạt động: thêm vueNnnRouterScrollPlugin và dùng argument literal.
  • Không resolve được Lenis: cài lenis và import lenis/dist/lenis.css.

Demo

npm install
npm run demo:install
npm run demo:dev

Mở /smooth-scroll để test Lenis và /about để so sánh với native scrolling.

Giấy phép

MIT — xem LICENSE.

Related Packages

vue-antd-layout

基于antd快速搭建中台前端的layout

385 0
vite-plugin-vue-layouts-next

Router based layout plugin for Vite 8, Vue 3 and Vue Router 5.

896,261 72
vue-router-middleware-plugin

A vue.js plugin to implement a middleware pipeline between your routes.

4,012 11
@alikhalilll/nuxt-auto-middleware

Declarative, layout-based route middleware for Nuxt 3/4, with glob matching, nam...

1,516 4
vue-route-middleware

Make complex route middleware logic look easy and readable!

13,280 18

Version History

Version Released Vue License
0.1.1 ^3.3.0 MIT
0.1.0 ^3.3.0 MIT
0.0.9 ^3.3.0 MIT
0.0.8 ^3.3.0 MIT
0.0.7 ^3.3.0 MIT