├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── commit-convention.md ├── contributing.md ├── funding.yml └── workflows │ ├── pkg.pr.new.yml │ ├── release-tag.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── SECURITY.md ├── codecov.yml ├── netlify.toml ├── package.json ├── packages ├── docs │ ├── .gitignore │ ├── .vitepress │ │ ├── config │ │ │ ├── en.ts │ │ │ ├── index.ts │ │ │ ├── shared.ts │ │ │ └── zh.ts │ │ ├── theme │ │ │ ├── components │ │ │ │ ├── AsideSponsors.vue │ │ │ │ ├── HomeSponsors.vue │ │ │ │ ├── HomeSponsorsGroup.vue │ │ │ │ ├── MadVueBanner.vue │ │ │ │ ├── PiniaLogo.vue │ │ │ │ ├── TranslationStatus.vue │ │ │ │ ├── VueMasteryBanner.vue │ │ │ │ ├── VueMasteryHomeLink.vue │ │ │ │ ├── VueMasteryLogoLink.vue │ │ │ │ ├── VueSchoolLink.vue │ │ │ │ ├── VuejsdeConfBanner.vue │ │ │ │ └── sponsors.json │ │ │ ├── index.ts │ │ │ └── styles │ │ │ │ ├── home-links.css │ │ │ │ └── vars.css │ │ └── translation-status.json │ ├── guide │ │ ├── advanced │ │ │ ├── composition-api.md │ │ │ ├── data-fetching.md │ │ │ ├── dynamic-routing.md │ │ │ ├── extending-router-link.md │ │ │ ├── lazy-loading.md │ │ │ ├── meta.md │ │ │ ├── navigation-failures.md │ │ │ ├── navigation-guards.md │ │ │ ├── router-view-slot.md │ │ │ ├── scroll-behavior.md │ │ │ ├── transitions.md │ │ │ └── typed-routes.md │ │ ├── essentials │ │ │ ├── active-links.md │ │ │ ├── dynamic-matching.md │ │ │ ├── history-mode.md │ │ │ ├── named-routes.md │ │ │ ├── named-views.md │ │ │ ├── navigation.md │ │ │ ├── nested-routes.md │ │ │ ├── passing-props.md │ │ │ ├── redirect-and-alias.md │ │ │ └── route-matching-syntax.md │ │ ├── index.md │ │ └── migration │ │ │ └── index.md │ ├── index.md │ ├── installation.md │ ├── introduction.md │ ├── package.json │ ├── public │ │ ├── Vue_Router_-_Getting_Started.jpeg │ │ ├── banners │ │ │ └── vuejs-certification.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── mp-pinia-logo.svg │ │ ├── service-worker.js │ │ ├── sponsors │ │ │ ├── fincliplogo_black_svg.svg │ │ │ ├── fincliplogo_white_svg.svg │ │ │ ├── passionate-people-dark.png │ │ │ ├── passionate-people-light.png │ │ │ ├── vuejobs.png │ │ │ ├── vuetify-logo-dark-text.svg │ │ │ ├── vuetify-logo-light-text.svg │ │ │ └── vuetoronto.svg │ │ ├── vue-cert-logo.svg │ │ ├── vuejsde-conf │ │ │ ├── vuejsdeconf_banner_large.png │ │ │ ├── vuejsdeconf_banner_large_2x.png │ │ │ ├── vuejsdeconf_banner_medium.png │ │ │ ├── vuejsdeconf_banner_medium_2x.png │ │ │ ├── vuejsdeconf_banner_small.png │ │ │ ├── vuejsdeconf_banner_small_2x.png │ │ │ ├── vuejsdeconf_banner_smallest.png │ │ │ └── vuejsdeconf_banner_smallest_2x.png │ │ ├── vuemastery │ │ │ ├── background-bubbles-vuemastery.svg │ │ │ ├── lock-vuemastery.svg │ │ │ ├── unlock-vuemastery.svg │ │ │ └── vuemastery-white.svg │ │ └── your-logo-here.svg │ ├── run-typedoc.mjs │ ├── typedoc-markdown.mjs │ ├── typedoc.tsconfig.json │ └── zh │ │ ├── about-translation.md │ │ ├── api │ │ ├── enums │ │ │ └── NavigationFailureType.md │ │ ├── index.md │ │ └── interfaces │ │ │ ├── HistoryState.md │ │ │ ├── NavigationFailure.md │ │ │ ├── NavigationGuard.md │ │ │ ├── NavigationGuardNext.md │ │ │ ├── NavigationGuardWithThis.md │ │ │ ├── NavigationHookAfter.md │ │ │ ├── RouteLocation.md │ │ │ ├── RouteLocationMatched.md │ │ │ ├── RouteLocationNormalized.md │ │ │ ├── RouteLocationNormalizedLoaded.md │ │ │ ├── RouteLocationOptions.md │ │ │ ├── RouteMeta.md │ │ │ ├── RouteRecordBase.md │ │ │ ├── RouteRecordMultipleViews.md │ │ │ ├── RouteRecordMultipleViewsWithChildren.md │ │ │ ├── RouteRecordNormalized.md │ │ │ ├── RouteRecordRedirect.md │ │ │ ├── RouteRecordSingleView.md │ │ │ ├── RouteRecordSingleViewWithChildren.md │ │ │ ├── Router.md │ │ │ ├── RouterHistory.md │ │ │ ├── RouterLinkProps.md │ │ │ ├── RouterOptions.md │ │ │ ├── RouterScrollBehavior.md │ │ │ └── RouterViewProps.md │ │ ├── guide │ │ ├── advanced │ │ │ ├── composition-api.md │ │ │ ├── data-fetching.md │ │ │ ├── dynamic-routing.md │ │ │ ├── extending-router-link.md │ │ │ ├── lazy-loading.md │ │ │ ├── meta.md │ │ │ ├── navigation-failures.md │ │ │ ├── navigation-guards.md │ │ │ ├── router-view-slot.md │ │ │ ├── scroll-behavior.md │ │ │ ├── transitions.md │ │ │ └── typed-routes.md │ │ ├── essentials │ │ │ ├── active-links.md │ │ │ ├── dynamic-matching.md │ │ │ ├── history-mode.md │ │ │ ├── named-routes.md │ │ │ ├── named-views.md │ │ │ ├── navigation.md │ │ │ ├── nested-routes.md │ │ │ ├── passing-props.md │ │ │ ├── redirect-and-alias.md │ │ │ └── route-matching-syntax.md │ │ ├── index.md │ │ └── migration │ │ │ └── index.md │ │ ├── index.md │ │ ├── installation.md │ │ └── introduction.md ├── playground │ ├── env.d.ts │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── AppLink.vue │ │ ├── api │ │ │ └── index.ts │ │ ├── main.ts │ │ ├── router.ts │ │ ├── scrollWaiter.ts │ │ ├── store.ts │ │ └── views │ │ │ ├── ComponentWithData.vue │ │ │ ├── Dynamic.vue │ │ │ ├── Generic.vue │ │ │ ├── GuardedWithLeave.vue │ │ │ ├── Home.vue │ │ │ ├── LongView.vue │ │ │ ├── Nested.vue │ │ │ ├── NestedWithId.vue │ │ │ ├── NotFound.vue │ │ │ ├── RepeatedParams.vue │ │ │ └── User.vue │ ├── tsconfig.config.json │ ├── tsconfig.json │ └── vite.config.ts └── router │ ├── .gitignore │ ├── CHANGELOG.md │ ├── SECURITY.md │ ├── __tests__ │ ├── RouterLink.spec.ts │ ├── RouterView.spec.ts │ ├── __snapshots__ │ │ ├── RouterLink.spec.ts.snap │ │ └── RouterView.spec.ts.snap │ ├── createRouter.test-d.ts │ ├── encoding.spec.ts │ ├── errors.spec.ts │ ├── guards │ │ ├── afterEach.spec.ts │ │ ├── beforeEach.spec.ts │ │ ├── beforeEnter.spec.ts │ │ ├── beforeResolve.spec.ts │ │ ├── beforeRouteEnter.spec.ts │ │ ├── beforeRouteEnterCallback.spec.ts │ │ ├── beforeRouteLeave.spec.ts │ │ ├── beforeRouteUpdate.spec.ts │ │ ├── extractComponentsGuards.spec.ts │ │ ├── guardToPromiseFn.spec.ts │ │ ├── guardsContext.spec.ts │ │ ├── loadRouteLocation.spec.ts │ │ ├── navigatioGuardsInjections.spec.ts │ │ ├── onBeforeRouteLeave.spec.ts │ │ └── onBeforeRouteUpdate.spec.ts │ ├── hash-manual-navigation.spec.ts │ ├── history │ │ ├── hash.spec.ts │ │ ├── html5.spec.ts │ │ └── memory.spec.ts │ ├── initialNavigation.spec.ts │ ├── isReady.spec.ts │ ├── lazyLoading.spec.ts │ ├── location.spec.ts │ ├── matcher │ │ ├── __snapshots__ │ │ │ └── resolve.spec.ts.snap │ │ ├── addingRemoving.spec.ts │ │ ├── pathParser.spec.ts │ │ ├── pathRanking.spec.ts │ │ ├── records.spec.ts │ │ └── resolve.spec.ts │ ├── mount.ts │ ├── multipleApps.spec.ts │ ├── parseQuery.spec.ts │ ├── routeLocation.test-d.ts │ ├── router.spec.ts │ ├── scrollBehavior.spec.ts │ ├── setup.ts │ ├── ssr.spec.ts │ ├── stringifyQuery.spec.ts │ ├── urlEncoding.spec.ts │ ├── useApi.spec.ts │ ├── useLink.spec.ts │ ├── utils.ts │ ├── vitest-mock-warn.ts │ └── warnings.spec.ts │ ├── api-extractor.json │ ├── e2e │ ├── devServer.mjs │ ├── encoding │ │ ├── index.html │ │ └── index.ts │ ├── global.css │ ├── guards-instances │ │ ├── index.html │ │ └── index.ts │ ├── hash │ │ ├── index.html │ │ └── index.ts │ ├── index.html │ ├── index.ts │ ├── keep-alive │ │ ├── index.html │ │ └── index.ts │ ├── modal │ │ ├── index.html │ │ └── index.ts │ ├── multi-app │ │ ├── index.html │ │ └── index.ts │ ├── runner.mjs │ ├── scroll-behavior │ │ ├── index.html │ │ ├── index.ts │ │ └── scrollWaiter.ts │ ├── specs │ │ ├── encoding.js │ │ ├── guards-instances.js │ │ ├── hash.js │ │ ├── keep-alive.js │ │ ├── modal.js │ │ ├── multi-app.js │ │ ├── scroll-behavior.js │ │ ├── suspense.js │ │ └── transitions.js │ ├── suspense │ │ ├── index.html │ │ └── index.ts │ ├── transitions │ │ ├── index.html │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.mjs │ ├── index.js │ ├── nightwatch.conf.js │ ├── package.json │ ├── rollup.config.mjs │ ├── size-checks │ ├── rollup.config.mjs │ ├── webRouter.js │ └── webRouterAndVue.js │ ├── src │ ├── RouterLink.ts │ ├── RouterView.ts │ ├── config.ts │ ├── devtools.ts │ ├── encoding.ts │ ├── errors.ts │ ├── global.d.ts │ ├── globalExtensions.ts │ ├── history │ │ ├── common.ts │ │ ├── hash.ts │ │ ├── html5.ts │ │ └── memory.ts │ ├── index.ts │ ├── injectionSymbols.ts │ ├── location.ts │ ├── matcher │ │ ├── index.ts │ │ ├── pathMatcher.ts │ │ ├── pathParserRanker.ts │ │ ├── pathTokenizer.ts │ │ └── types.ts │ ├── navigationGuards.ts │ ├── query.ts │ ├── router.ts │ ├── scrollBehavior.ts │ ├── typed-routes │ │ ├── index.ts │ │ ├── navigation-guards.ts │ │ ├── params.ts │ │ ├── route-location.ts │ │ ├── route-map.ts │ │ └── route-records.ts │ ├── types │ │ ├── index.ts │ │ ├── typeGuards.ts │ │ └── utils.ts │ ├── useApi.ts │ ├── utils │ │ ├── README.md │ │ ├── callbacks.ts │ │ ├── env.ts │ │ └── index.ts │ └── warning.ts │ ├── test-dts │ ├── components.test-d.tsx │ ├── index.d.ts │ ├── legacy.test-d.ts │ ├── meta.test-d.ts │ ├── navigationGuards.test-d.ts │ ├── routeRecords.test-d.ts │ ├── tsconfig.json │ └── typed-routes.test-d.ts │ ├── tsconfig.json │ ├── vetur │ ├── attributes.json │ └── tags.json │ ├── vitest.config.ts │ ├── vue-router-auto-routes.d.ts │ └── vue-router-auto.d.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts ├── check-size.mjs ├── docs-check.sh ├── release.mjs └── verifyCommit.mjs └── vitest.workspace.js /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 👨‍💻 Support 4 | url: https://cal.com/posva/consultancy 5 | about: Get direct help from the author of Vue Router with your project 6 | - name: ❓ Help & Questions 7 | url: https://github.com/vuejs/router/discussions/new?category=help-and-questions 8 | about: Ask a question or discuss about Vue Router 9 | - name: 💡 Ideas 10 | url: https://github.com/vuejs/router/discussions/new?category=ideas 11 | about: Start a discussion to improve Vue Router 12 | - name: 🌟 GitHub Sponsors 13 | url: https://github.com/sponsors/posva 14 | about: Like this project? Please consider supporting the author. 15 | - name: Open Collective 16 | url: https://opencollective.com/vuejs/donate 17 | about: Love Vue.js? Please consider supporting us via Open Collective. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 New feature proposal" 2 | description: Suggest an idea for Vue Router 3 | labels: ['feature request'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for your interest in the project and taking the time to fill out this feature report! 9 | - type: textarea 10 | id: feature-description 11 | attributes: 12 | label: What problem is this solving 13 | description: 'A clear and concise description of what the problem is. Ex. when using the function X we cannot do Y.' 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: proposed-solution 18 | attributes: 19 | label: Proposed solution 20 | description: 'A clear and concise description of what you want to happen with an API proposal when applicable' 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: alternative 25 | attributes: 26 | label: Describe alternatives you've considered 27 | description: A clear and concise description of any alternative solutions or features you've considered. 28 | - type: markdown 29 | attributes: 30 | value: | 31 | ## Before creating a feature request make sure that: 32 | - This hasn't been [requested before](https://github.com/vuejs/router/issues). 33 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [posva] 2 | open_collective: vuejs 3 | -------------------------------------------------------------------------------- /.github/workflows/pkg.pr.new.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | 3 | on: 4 | pull_request: 5 | branches: main 6 | paths-ignore: 7 | - 'packages/docs/**' 8 | - 'packages/playground/**' 9 | 10 | push: 11 | branches: 12 | - '**' 13 | tags: 14 | - '!**' 15 | paths-ignore: 16 | - 'packages/docs/**' 17 | - 'packages/playground/**' 18 | 19 | jobs: 20 | build: 21 | if: github.repository == 'vuejs/router' 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | - uses: pnpm/action-setup@v4 30 | - uses: actions/setup-node@v4 31 | with: 32 | node-version: lts/* 33 | cache: pnpm 34 | 35 | - name: Install 36 | run: pnpm install --frozen-lockfile 37 | 38 | - name: Build 39 | run: pnpm -C packages/router build 40 | 41 | - name: Build DTS 42 | run: pnpm -C packages/router build:dts 43 | 44 | - name: Release 45 | run: pnpm dlx pkg-pr-new publish --compact --pnpm './packages/*' 46 | -------------------------------------------------------------------------------- /.github/workflows/release-tag.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 5 | 6 | name: Create Release 7 | 8 | jobs: 9 | build: 10 | name: Create Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@master 15 | - name: Create Release for Tag 16 | id: release_tag 17 | uses: yyx990803/release-tag@master 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | tag_name: ${{ github.ref }} 22 | body: | 23 | Please refer to [CHANGELOG.md](https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md) for details. 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'packages/docs/**' 9 | - 'packages/playground/**' 10 | pull_request: 11 | branches: 12 | - main 13 | paths-ignore: 14 | - 'packages/docs/**' 15 | - 'packages/playground/**' 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: pnpm/action-setup@v4 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: 'lts/*' 27 | cache: pnpm 28 | - name: 'BrowserStack Env Setup' 29 | uses: 'browserstack/github-actions/setup-env@master' 30 | # forks do not have access to secrets so just skip this 31 | if: ${{ github.repository == 'vuejs/router' && !github.event.pull_request.head.repo.fork }} 32 | with: 33 | username: ${{ secrets.BROWSERSTACK_USERNAME }} 34 | access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 35 | 36 | - run: pnpm install 37 | - run: pnpm run lint 38 | - run: pnpm run -r build 39 | - run: pnpm run -r build:dts 40 | - run: pnpm run -r test:types 41 | - run: pnpm run -r test:unit 42 | 43 | # e2e tests that that run locally 44 | - run: pnpm run -r test:e2e:ci 45 | 46 | - uses: codecov/codecov-action@v4 47 | with: 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | 50 | # - name: 'Start BrowserStackLocal Tunnel' 51 | # uses: 'browserstack/github-actions/setup-local@master' 52 | # with: 53 | # local-testing: 'start' 54 | # local-logging-level: 'all-logs' 55 | # local-identifier: 'random' 56 | 57 | # - run: pnpm run -r test:e2e:bs-test 58 | 59 | # - name: 'Stop BrowserStackLocal' 60 | # uses: 'browserstack/github-actions/setup-local@master' 61 | # with: 62 | # local-testing: 'stop' 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | .nyc_output 5 | .rpt2_cache 6 | .env 7 | .DS_Store 8 | temp 9 | explorations 10 | selenium-server.log 11 | browserstack.err 12 | .idea 13 | debug.log 14 | yalc.lock 15 | .yalc 16 | local.log 17 | _selenium-server.log 18 | packages/*/LICENSE 19 | tracing_output 20 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | detect_chromedriver_version=true 3 | strict-peer-dependencies=false 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | __build__ 2 | dist 3 | coverage 4 | tests_output 5 | packages/docs/.vitepress/cache 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests", 10 | "request": "launch", 11 | "args": [ 12 | "--runInBand" 13 | ], 14 | "cwd": "${workspaceFolder}", 15 | "console": "integratedTerminal", 16 | "internalConsoleOptions": "neverOpen", 17 | "disableOptimisticBPs": true, 18 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present Eduardo San Martin Morote 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a Vulnerability 2 | 3 | To report a vulnerability, please email security@vuejs.org. 4 | 5 | While the discovery of new vulnerabilities is rare, we also recommend always using the latest versions of Vue and its official companion libraries to ensure your application remains as secure as possible. 6 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | project: 5 | default: 6 | threshold: 2% 7 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | CHROMEDRIVER_SKIP_DOWNLOAD = "true" 3 | NODE_VERSION = "18" 4 | 5 | [build] 6 | command = "pnpm run docs:build" 7 | ignore = "./scripts/docs-check.sh" 8 | publish = "packages/docs/.vitepress/dist" 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue/router-root", 3 | "private": true, 4 | "packageManager": "pnpm@9.10.0", 5 | "type": "module", 6 | "engines": { 7 | "node": ">=20.9.0" 8 | }, 9 | "workspaces": [ 10 | "packages/*" 11 | ], 12 | "scripts": { 13 | "release": "node scripts/release.mjs", 14 | "size": "node scripts/check-size.mjs", 15 | "build": "pnpm run -r build", 16 | "build:dts": "pnpm run -r build:dts", 17 | "docs": "pnpm run --filter ./packages/docs -r docs", 18 | "docs:api": "pnpm run --filter ./packages/docs -r docs:api", 19 | "docs:translation:compare": "pnpm run --filter ./packages/docs -r docs:translation:compare", 20 | "docs:translation:update": "pnpm run --filter ./packages/docs -r docs:translation:update", 21 | "docs:translation:status": "pnpm run --filter ./packages/docs -r docs:translation:status", 22 | "docs:build": "pnpm run docs:api && pnpm run --filter ./packages/docs -r docs:build", 23 | "docs:preview": "pnpm run --filter ./packages/docs -r docs:preview", 24 | "play": "pnpm run -r play", 25 | "build:size": "pnpm run -r build:size", 26 | "lint": "pnpm run lint:script && pnpm run lint:html", 27 | "lint:script": "prettier -c --parser typescript \"packages/**/*.?(m)[jt]s?(x)\" \"scripts/*.mjs\"", 28 | "lint:html": "prettier -c --parser html \"packages/**/*.html\"", 29 | "lint:fix": "pnpm run lint:script --write && pnpm run lint:html --write", 30 | "test": "pnpm run -r test", 31 | "postinstall": "simple-git-hooks" 32 | }, 33 | "devDependencies": { 34 | "@vitest/coverage-v8": "^2.1.9", 35 | "@vitest/ui": "^2.1.9", 36 | "brotli": "^1.3.3", 37 | "chalk": "^5.4.1", 38 | "enquirer": "^2.4.1", 39 | "execa": "^9.5.2", 40 | "globby": "^14.1.0", 41 | "lint-staged": "^15.5.1", 42 | "minimist": "^1.2.8", 43 | "p-series": "^3.0.0", 44 | "prettier": "^3.5.3", 45 | "semver": "^7.7.1", 46 | "simple-git-hooks": "^2.13.0", 47 | "typedoc": "^0.26.11", 48 | "typedoc-plugin-markdown": "^4.2.10", 49 | "typescript": "~5.6.3", 50 | "vitest": "^2.1.9" 51 | }, 52 | "simple-git-hooks": { 53 | "pre-commit": "pnpm lint-staged", 54 | "commit-msg": "node scripts/verifyCommit.mjs" 55 | }, 56 | "lint-staged": { 57 | "*.?(m)js": [ 58 | "prettier --write" 59 | ], 60 | "*.?(m)ts?(x)": [ 61 | "prettier --parser=typescript --write" 62 | ], 63 | "*.html": [ 64 | "prettier --parser=html --write" 65 | ] 66 | }, 67 | "pnpm": { 68 | "peerDependencyRules": { 69 | "ignoreMissing": [ 70 | "react", 71 | "@types/react", 72 | "react-dom" 73 | ] 74 | } 75 | }, 76 | "volta": { 77 | "node": "20.11.1" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/docs/.gitignore: -------------------------------------------------------------------------------- 1 | .vitepress/cache 2 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/config/index.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import { enConfig } from './en' 3 | import { sharedConfig } from './shared' 4 | import { zhConfig } from './zh' 5 | 6 | export default defineConfig({ 7 | ...sharedConfig, 8 | 9 | locales: { 10 | root: { label: 'English', lang: 'en-US', link: '/', ...enConfig }, 11 | zh: { label: '简体中文', lang: 'zh-CN', link: '/zh/', ...zhConfig }, 12 | ko: { label: '한국어', lang: 'ko-KR', link: 'https://router.vuejs.kr/' }, 13 | pt: { 14 | label: 'Português', 15 | lang: 'pt-PT', 16 | link: 'https://vue-router-docs-pt.netlify.app/', 17 | }, 18 | ru: { 19 | label: 'Русский', 20 | lang: 'ru-RU', 21 | link: 'https://vue-router-ru.netlify.app', 22 | }, 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/theme/components/HomeSponsors.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | 40 | 41 | 79 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/theme/components/HomeSponsorsGroup.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 50 | 51 | 95 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/theme/components/TranslationStatus.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/theme/components/VueMasteryHomeLink.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 25 | 26 | 68 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/theme/components/VueMasteryLogoLink.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 74 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/theme/components/VueSchoolLink.vue: -------------------------------------------------------------------------------- 1 | 14 | 24 | 61 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/theme/components/sponsors.json: -------------------------------------------------------------------------------- 1 | { 2 | "platinum": [], 3 | "gold": [ 4 | { 5 | "alt": "CodeRabbit", 6 | "href": "https://www.coderabbit.ai/?utm_source=cr_org&utm_medium=github", 7 | "imgSrcDark": "https://posva-sponsors.pages.dev/logos/coderabbitai-dark.svg", 8 | "imgSrcLight": "https://posva-sponsors.pages.dev/logos/coderabbitai-light.svg" 9 | } 10 | ], 11 | "silver": [ 12 | { 13 | "alt": "VueMastery", 14 | "href": "https://www.vuemastery.com/", 15 | "imgSrcDark": "https://posva-sponsors.pages.dev/logos/vuemastery-dark.png", 16 | "imgSrcLight": "https://posva-sponsors.pages.dev/logos/vuemastery-light.svg" 17 | }, 18 | { 19 | "alt": "Prefect", 20 | "href": "https://www.prefect.io/", 21 | "imgSrcDark": "https://posva-sponsors.pages.dev/logos/prefectlogo-dark.svg", 22 | "imgSrcLight": "https://posva-sponsors.pages.dev/logos/prefectlogo-light.svg" 23 | }, 24 | { 25 | "alt": "Route Optimizer and Route Planner Software", 26 | "href": "https://route4me.com", 27 | "imgSrcDark": "https://posva-sponsors.pages.dev/logos/route4me.png", 28 | "imgSrcLight": "https://posva-sponsors.pages.dev/logos/route4me.png" 29 | } 30 | ], 31 | "bronze": [ 32 | { 33 | "alt": "Storyblok", 34 | "href": "https://storyblok.com", 35 | "imgSrcDark": "https://posva-sponsors.pages.dev/logos/storyblok.png", 36 | "imgSrcLight": "https://posva-sponsors.pages.dev/logos/storyblok.png" 37 | }, 38 | { 39 | "alt": "NuxtLabs", 40 | "href": "https://nuxtlabs.com", 41 | "imgSrcDark": "https://posva-sponsors.pages.dev/logos/nuxt-dark.svg", 42 | "imgSrcLight": "https://posva-sponsors.pages.dev/logos/nuxt-light.svg" 43 | }, 44 | { 45 | "alt": "Stanislas Ormières", 46 | "href": "https://stormier.ninja", 47 | "imgSrcDark": "https://avatars.githubusercontent.com/u/2486424?u=7b0c73ae5d090ce53bf59473094e9606fe082c59&v=4", 48 | "imgSrcLight": "https://avatars.githubusercontent.com/u/2486424?u=7b0c73ae5d090ce53bf59473094e9606fe082c59&v=4" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { Theme } from 'vitepress' 3 | import DefaultTheme from 'vitepress/theme' 4 | import AsideSponsors from './components/AsideSponsors.vue' 5 | // import HomeSponsors from './components/HomeSponsors.vue' 6 | import TranslationStatus from 'vitepress-translation-helper/ui/TranslationStatus.vue' 7 | import './styles/vars.css' 8 | import VueSchoolLink from './components/VueSchoolLink.vue' 9 | import VueMasteryLogoLink from './components/VueMasteryLogoLink.vue' 10 | import status from '../translation-status.json' 11 | import MadVueBanner from './components/MadVueBanner.vue' 12 | 13 | const i18nLabels = { 14 | zh: '该翻译已同步到了 ${date} 的版本,其对应的 commit hash 是 ${hash}。', 15 | } 16 | 17 | const theme: Theme = { 18 | extends: DefaultTheme, 19 | Layout() { 20 | return h(DefaultTheme.Layout, null, { 21 | // 'home-features-after': () => h(HomeSponsors), 22 | 'aside-ads-before': () => h(AsideSponsors), 23 | 'doc-before': () => h(TranslationStatus, { status, i18nLabels }), 24 | 'layout-top': () => h(MadVueBanner), 25 | }) 26 | }, 27 | 28 | enhanceApp({ app }) { 29 | app.component('VueSchoolLink', VueSchoolLink) 30 | app.component('VueMasteryLogoLink', VueMasteryLogoLink) 31 | }, 32 | 33 | // TODO: real date 34 | // setup() { 35 | // const { lang } = useData() 36 | // watchEffect(() => { 37 | // if (typeof document !== 'undefined') { 38 | // document.cookie = `nf_lang=${lang.value}; expires=Sun, 1 Jan 2023 00:00:00 UTC; path=/` 39 | // } 40 | // }) 41 | // }, 42 | } 43 | 44 | export default theme 45 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/theme/styles/home-links.css: -------------------------------------------------------------------------------- 1 | /* Style to get the cheat sheet link in the home page */ 2 | 3 | a.cta { 4 | text-align: center; 5 | border-radius: 8px; 6 | } 7 | 8 | a.cta:hover { 9 | border-color: var(--vp-c-brand); 10 | background-color: var(--c-bg-accent); 11 | } 12 | 13 | a.cta.vue-mastery::before { 14 | content: ''; 15 | display: inline-block; 16 | width: 25px; 17 | height: 25px; 18 | background-image: url('https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fvue-mastery-logo-small.png?alt=media&token=941fcc3a-2b6f-40e9-b4c8-56b3890da108'); 19 | background-size: 25px; 20 | background-repeat: no-repeat; 21 | background-position: bottom; 22 | margin-right: 0.5em; 23 | } 24 | 25 | a.cta.vueschool { 26 | position: relative; 27 | color: currentColor; 28 | padding-left: 38px !important; 29 | } 30 | a.cta.vueschool::before { 31 | content: ''; 32 | position: absolute; 33 | display: block; 34 | width: 20px; 35 | height: 20px; 36 | top: calc(50% - 10px); 37 | left: 12px; 38 | border-radius: 50%; 39 | border: 1px solid currentColor; 40 | } 41 | 42 | a.cta.vueschool::after { 43 | content: ''; 44 | position: absolute; 45 | display: block; 46 | width: 0; 47 | height: 0; 48 | top: calc(50% - 4px); 49 | left: 20px; 50 | border-top: 4px solid transparent; 51 | border-bottom: 4px solid transparent; 52 | border-left: 7px solid currentColor; 53 | } 54 | 55 | @media (max-width: 420px) { 56 | a.cta.cta.vue-mastery { 57 | max-width: 320px; 58 | line-height: 1.5em; 59 | white-space: normal; 60 | display: block; 61 | padding-bottom: 10px; 62 | margin-left: 8px; 63 | margin-right: 8px; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/theme/styles/vars.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-c-brand-1: var(--vp-c-green-1); 3 | --vp-c-brand-2: var(--vp-c-green-2); 4 | --vp-c-brand-3: var(--vp-c-green-3); 5 | --vp-c-brand-soft: var(--vp-c-green-soft); 6 | --vp-code-color: #476582; 7 | } 8 | 9 | :root.dark { 10 | --vp-code-color: #c9def1; 11 | 12 | --vp-home-hero-image-filter: blur(72px); 13 | --vp-home-hero-image-background-image: linear-gradient( 14 | 0deg, 15 | var(--vp-c-brand-soft) 50%, 16 | var(--vp-c-brand-soft) 50% 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/docs/.vitepress/translation-status.json: -------------------------------------------------------------------------------- 1 | { 2 | "zh": { 3 | "hash": "d842b6f", 4 | "date": "2024-05-17" 5 | } 6 | } -------------------------------------------------------------------------------- /packages/docs/guide/advanced/router-view-slot.md: -------------------------------------------------------------------------------- 1 | # RouterView slot 2 | 3 | The RouterView component exposes a slot that can be used to render the route component: 4 | 5 | ```vue-html 6 | 7 | 8 | 9 | ``` 10 | 11 | The code above is equivalent to using `` without the slot, but the slot provides extra flexibility when we want to work with other features. 12 | 13 | ## KeepAlive & Transition 14 | 15 | When working with the [KeepAlive](https://vuejs.org/guide/built-ins/keep-alive.html) component, we would usually want it to keep the route components alive, not the RouterView itself. We can achieve that by putting the KeepAlive inside the slot: 16 | 17 | ```vue-html 18 | 19 | 20 | 21 | 22 | 23 | ``` 24 | 25 | Similarly, the slot allows us to use a [Transition](https://vuejs.org/guide/built-ins/transition.html) component to transition between route components: 26 | 27 | ```vue-html 28 | 29 | 30 | 31 | 32 | 33 | ``` 34 | 35 | We can also use KeepAlive inside a Transition: 36 | 37 | ```vue-html 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ``` 46 | 47 | For more information about using RouterView with the Transition component, see the [Transitions](./transitions) guide. 48 | 49 | ## Passing props and slots 50 | 51 | We can use the slot to pass props or slots to the route component: 52 | 53 | ```vue-html 54 | 55 | 56 |

Some slotted content

57 |
58 |
59 | ``` 60 | 61 | In practice, this usually isn't something you would want to do, as the route components would **all need to use the same props and slots**. See [Passing Props to Route Components](../essentials/passing-props) for other ways to pass props. 62 | 63 | ## Template refs 64 | 65 | Using the slot allows us to put a [template ref](https://vuejs.org/guide/essentials/template-refs.html) directly on the route component: 66 | 67 | ```vue-html 68 | 69 | 70 | 71 | ``` 72 | 73 | If we put the ref on the `` instead then the ref would be populated with the RouterView instance, rather than the route component. 74 | -------------------------------------------------------------------------------- /packages/docs/guide/advanced/typed-routes.md: -------------------------------------------------------------------------------- 1 | # Typed Routes 2 | 3 | ![RouterLink to autocomplete](https://user-images.githubusercontent.com/664177/176442066-c4e7fa31-4f06-4690-a49f-ed0fd880dfca.png) 4 | 5 | It's possible to configure the router to have a _map_ of typed routes. While this can be done manually, it is recommended to use the [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) plugin to generate the routes and the types automatically. 6 | 7 | ## Manual Configuration 8 | 9 | Here is an example of how to manually configure typed routes: 10 | 11 | ```ts 12 | // import the `RouteRecordInfo` type from vue-router to type your routes 13 | import type { RouteRecordInfo } from 'vue-router' 14 | 15 | // Define an interface of routes 16 | export interface RouteNamedMap { 17 | // each key is a name 18 | home: RouteRecordInfo< 19 | // here we have the same name 20 | 'home', 21 | // this is the path, it will appear in autocompletion 22 | '/', 23 | // these are the raw params (what can be passed to router.push() and RouterLink's "to" prop) 24 | // In this case, there are no params allowed 25 | Record, 26 | // these are the normalized params (what you get from useRoute()) 27 | Record, 28 | // this is a union of all children route names, in this case, there are none 29 | never 30 | > 31 | // repeat for each route... 32 | // Note you can name them whatever you want 33 | 'named-param': RouteRecordInfo< 34 | 'named-param', 35 | '/:name', 36 | { name: string | number }, // Allows string or number 37 | { name: string }, // but always returns a string from the URL 38 | 'named-param-edit' 39 | > 40 | 'named-param-edit': RouteRecordInfo< 41 | 'named-param-edit', 42 | '/:name/edit', 43 | { name: string | number }, // we also include parent params 44 | { name: string }, 45 | never 46 | > 47 | 'article-details': RouteRecordInfo< 48 | 'article-details', 49 | '/articles/:id+', 50 | { id: Array }, 51 | { id: string[] }, 52 | never 53 | > 54 | 'not-found': RouteRecordInfo< 55 | 'not-found', 56 | '/:path(.*)', 57 | { path: string }, 58 | { path: string }, 59 | never 60 | > 61 | } 62 | 63 | // Last, you will need to augment the Vue Router types with this map of routes 64 | declare module 'vue-router' { 65 | interface TypesConfig { 66 | RouteNamedMap: RouteNamedMap 67 | } 68 | } 69 | ``` 70 | 71 | ::: tip 72 | 73 | This is indeed tedious and error-prone. That's why it's recommended to use [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) to generate the routes and the types automatically. 74 | 75 | ::: 76 | -------------------------------------------------------------------------------- /packages/docs/guide/essentials/named-routes.md: -------------------------------------------------------------------------------- 1 | # Named Routes 2 | 3 | 7 | 8 | When creating a route, we can optionally give the route a `name`: 9 | 10 | ```js 11 | const routes = [ 12 | { 13 | path: '/user/:username', 14 | name: 'profile', // [!code highlight] 15 | component: User 16 | } 17 | ] 18 | ``` 19 | 20 | We can then use the `name` instead of the `path` when passing the `to` prop to ``: 21 | 22 | ```vue-html 23 | 24 | User profile 25 | 26 | ``` 27 | 28 | The example above would create a link to `/user/erina`. 29 | 30 | - [See it in the Playground](https://play.vuejs.org/#eNqtVVtP2zAU/itWNqlFauNNIB6iUMEQEps0NjH2tOzBtKY1JLZlO6VTlP++4+PcelnFwyRofe7fubaKCiZk/GyjJBKFVsaRiswNZ45faU1q8mRUQUbrko8yuaPwlRfK/LkV1sHXpGHeq9JxMzScGmT19t5xkMaUaR1vOb9VBe+kntgWXz2Cs06O1LbCTwvRW7knGnEm50paRwIYcrEFd1xlkpBVyCQ5lN74ZOJV0Nom5JcnCFRCM7dKyIiOJkSygsNzBZiBmivAI7l0SUipRvuhCfPge7uWHBiGZPctS0iLJv7T2/YutFFPIt+JjgUJPn7DZ32CtWg7PIZ/4BASg7txKE6gC1VKNx69gw6NTqJJ1HQK5iR1vNA52M+8Yrr6OLuD+AuCtbQpBQYK9Oy6NAZAhLI1KKuKvEc69jSp65Tqw/oh3V7f00P9MsdveOWiecE75DDNhXwhiVMXWVRttYbUWdRpE2xOZ0sHxq1v2jl/a5jQyZ042Mv/HKjvt2aGFTCXFWmnAsTcCMkAxw4SHIjG9E2AUtpUusWyFvyVUGCltBsFmJB2W/dHZCHWswdYLwJ/XiulnrNr323zcQeodthDuAHTgmm4aEqCH1zsrBHYLIISheyyqD9Nnp1FK+e0TSgtpX5ZxrBBtNe4PItP4w8Q07oBN+a2mD4a9erPzDN4bzY1iy5BiS742imV2ynT4l8h9hQvz+Pz+COU/pGCdyrkgm/Qt3ddw/5Cms7CLXsSy50k/dJDT8037QTcuq1kWZ6r1y/Ic6bkHdD5is9fDvCf7SZA/m44ZLfmg+QcM0vugvjmxx3fwLsTFmpRwlwdE95zq/LSYwxqn0q5ANgDPUT7GXsm5PLB3mwcl7ZNygPFaqA+NvL6SOo93NP4bFDF9sfh+LThtgxvkF80fyxxy/Ac7U9i/RcYNWrd). 31 | 32 | Using a `name` has various advantages: 33 | 34 | - No hardcoded URLs. 35 | - Automatic encoding of `params`. 36 | - Avoids URL typos. 37 | - Bypassing path ranking, e.g. to display a lower-ranked route that matches the same path. 38 | 39 | Each name **must be unique** across all routes. If you add the same name to multiple routes, the router will only keep the last one. You can read more about this [in the Dynamic Routing](../advanced/dynamic-routing#Removing-routes) section. 40 | 41 | There are various other parts of Vue Router that can be passed a location, e.g. the methods `router.push()` and `router.replace()`. We'll go into more detail about those methods in the guide to [programmatic navigation](./navigation). Just like the `to` prop, these methods also support passing a location by `name`: 42 | 43 | ```js 44 | router.push({ name: 'profile', params: { username: 'erina' } }) 45 | ``` 46 | -------------------------------------------------------------------------------- /packages/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | title: Vue Router 5 | titleTemplate: The official Router for Vue.js 6 | 7 | hero: 8 | name: Vue Router 9 | text: The official Router for Vue.js 10 | tagline: Expressive, configurable and convenient routing for Vue.js 11 | image: 12 | src: /logo.svg 13 | alt: Vue Router 14 | actions: 15 | - theme: brand 16 | text: Get Started 17 | link: /installation 18 | - theme: cta vueschool 19 | text: Free Video Course 20 | link: https://vueschool.io/courses/vue-router-4-for-everyone?friend=vuerouter&utm_source=vuerouter&utm_medium=link&utm_campaign=homepage 21 | - theme: cta vue-mastery 22 | text: Get the Vue Router Cheat Sheet 23 | link: https://www.vuemastery.com/vue-router?coupon=ROUTER-DOCS&via=eduardo 24 | 25 | features: 26 | - title: 🛣 Expressive route syntax 27 | details: Define static and dynamic routes with an intuitive and powerful syntax. 28 | - title: 🛑 Fine-grained Navigation control 29 | details: Intercept any navigation and precisely control its outcome. 30 | - title: 🧱 Component-based configuration 31 | details: Map each route to the component that should display. 32 | - title: 🔌 History modes 33 | details: Choose between HTML5, Hash or Memory history modes. 34 | - title: 🎚 Scroll control 35 | details: Precisely control the scroll position in every page. 36 | - title: 🌐 Automatic Encoding 37 | details: Directly use unicode characters (你好) in your code. 38 | --- 39 | 40 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /packages/docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | 4 | 5 | ## Package managers 6 | 7 | If you have an existing project that uses a JavaScript package manager, you can install Vue Router from the npm registry: 8 | 9 | ::: code-group 10 | 11 | ```bash [npm] 12 | npm install vue-router@4 13 | ``` 14 | 15 | ```bash [yarn] 16 | yarn add vue-router@4 17 | ``` 18 | 19 | ```bash [pnpm] 20 | pnpm add vue-router@4 21 | ``` 22 | 23 | ```bash [bun] 24 | bun add vue-router@4 25 | ``` 26 | 27 | ::: 28 | 29 | If you're starting a new project, you might find it easier to use the [create-vue](https://github.com/vuejs/create-vue) scaffolding tool, which creates a Vite-based project with the option to include Vue Router: 30 | 31 | ::: code-group 32 | 33 | ```bash [npm] 34 | npm create vue@latest 35 | ``` 36 | 37 | ```bash [yarn] 38 | yarn create vue 39 | ``` 40 | 41 | ```bash [pnpm] 42 | pnpm create vue 43 | ``` 44 | 45 | ```bash [bun] 46 | bun create vue 47 | ``` 48 | 49 | ::: 50 | 51 | You'll be prompted with some questions about the kind of project you want to create. If you choose to install Vue Router, the example application will also demonstrate some of Vue Router's core features. 52 | 53 | Projects using package managers will typically use ES modules to access Vue Router, e.g. `import { createRouter } from 'vue-router'`. 54 | 55 | ## Direct Download / CDN 56 | 57 | [https://unpkg.com/vue-router@4](https://unpkg.com/vue-router@4) 58 | 59 | 60 | 61 | [Unpkg.com](https://unpkg.com) provides npm-based CDN links. The above link will always point to the latest release on npm. You can also use a specific version/tag via URLs like `https://unpkg.com/vue-router@4.0.15/dist/vue-router.global.js`. 62 | 63 | 64 | 65 | This will expose Vue Router via a global `VueRouter` object, e.g. `VueRouter.createRouter(...)`. 66 | -------------------------------------------------------------------------------- /packages/docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Watch a Free Vue Router Video Course 7 | 8 | Vue Router is the official router for [Vue.js](https://vuejs.org). It deeply integrates with Vue.js core to make building Single Page Applications with Vue.js a breeze. Features include: 9 | 10 | - Nested routes mapping 11 | - Dynamic Routing 12 | - Modular, component-based router configuration 13 | - Route params, query, wildcards 14 | - View transition effects powered by Vue.js' transition system 15 | - Fine-grained navigation control 16 | - Links with automatic active CSS classes 17 | - HTML5 history mode or hash mode 18 | - Customizable Scroll Behavior 19 | - Proper encoding for URLs 20 | 21 | [Get started](./guide/) or play with the [playground](https://github.com/vuejs/router/tree/main/packages/playground) (see [`README.md`](https://github.com/vuejs/router) to run them). 22 | 23 | 24 | 25 | 28 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue/router-docs", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "docs": "vitepress dev .", 8 | "docs:api": "node run-typedoc.mjs", 9 | "docs:translation:compare": "v-translation compare", 10 | "docs:translation:update": "v-translation update", 11 | "docs:translation:status": "v-translation status", 12 | "docs:build": "vitepress build .", 13 | "docs:preview": "vitepress preview ." 14 | }, 15 | "dependencies": { 16 | "simple-git": "^3.27.0", 17 | "vitepress": "1.5.0", 18 | "vitepress-translation-helper": "^0.2.1", 19 | "vue-router": "workspace:*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/docs/public/Vue_Router_-_Getting_Started.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/Vue_Router_-_Getting_Started.jpeg -------------------------------------------------------------------------------- /packages/docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/logo.png -------------------------------------------------------------------------------- /packages/docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/docs/public/service-worker.js: -------------------------------------------------------------------------------- 1 | // force clearing previous service worker 2 | self.addEventListener('install', function (e) { 3 | self.skipWaiting() 4 | }) 5 | 6 | self.addEventListener('activate', function (e) { 7 | self.registration 8 | .unregister() 9 | .then(function () { 10 | return self.clients.matchAll() 11 | }) 12 | .then(function (clients) { 13 | clients.forEach(client => client.navigate(client.url)) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/docs/public/sponsors/passionate-people-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/sponsors/passionate-people-dark.png -------------------------------------------------------------------------------- /packages/docs/public/sponsors/passionate-people-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/sponsors/passionate-people-light.png -------------------------------------------------------------------------------- /packages/docs/public/sponsors/vuejobs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/sponsors/vuejobs.png -------------------------------------------------------------------------------- /packages/docs/public/vue-cert-logo.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/docs/public/vuejsde-conf/vuejsdeconf_banner_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/vuejsde-conf/vuejsdeconf_banner_large.png -------------------------------------------------------------------------------- /packages/docs/public/vuejsde-conf/vuejsdeconf_banner_large_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/vuejsde-conf/vuejsdeconf_banner_large_2x.png -------------------------------------------------------------------------------- /packages/docs/public/vuejsde-conf/vuejsdeconf_banner_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/vuejsde-conf/vuejsdeconf_banner_medium.png -------------------------------------------------------------------------------- /packages/docs/public/vuejsde-conf/vuejsdeconf_banner_medium_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/vuejsde-conf/vuejsdeconf_banner_medium_2x.png -------------------------------------------------------------------------------- /packages/docs/public/vuejsde-conf/vuejsdeconf_banner_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/vuejsde-conf/vuejsdeconf_banner_small.png -------------------------------------------------------------------------------- /packages/docs/public/vuejsde-conf/vuejsdeconf_banner_small_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/vuejsde-conf/vuejsdeconf_banner_small_2x.png -------------------------------------------------------------------------------- /packages/docs/public/vuejsde-conf/vuejsdeconf_banner_smallest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/vuejsde-conf/vuejsdeconf_banner_smallest.png -------------------------------------------------------------------------------- /packages/docs/public/vuejsde-conf/vuejsdeconf_banner_smallest_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/router/30f8674ac4f3ee69a866bfae2280acd2a4db2daf/packages/docs/public/vuejsde-conf/vuejsdeconf_banner_smallest_2x.png -------------------------------------------------------------------------------- /packages/docs/run-typedoc.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import { createTypeDocApp } from './typedoc-markdown.mjs' 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 6 | 7 | createTypeDocApp({ 8 | name: 'API Documentation', 9 | tsconfig: path.resolve(__dirname, './typedoc.tsconfig.json'), 10 | categorizeByGroup: true, 11 | githubPages: false, 12 | disableSources: true, // some links are in node_modules and it's ugly 13 | plugin: ['typedoc-plugin-markdown'], 14 | entryPoints: [path.resolve(__dirname, '../router/src/index.ts')], 15 | }).then(app => app.build()) 16 | -------------------------------------------------------------------------------- /packages/docs/typedoc.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["../router/src/global.d.ts", "../router/src/**/*.ts"], 3 | "exclude": ["../*/__tests__/**/*.ts", "**/*.spec.ts"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "rootDir": "..", 7 | "outDir": "dist", 8 | "sourceMap": false, 9 | "noEmit": true, 10 | "paths": { 11 | "vue-router": ["../router/src"] 12 | }, 13 | 14 | "target": "esnext", 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "allowJs": false, 18 | "skipLibCheck": true, 19 | 20 | "noUnusedLocals": true, 21 | "strictNullChecks": true, 22 | "noImplicitAny": true, 23 | "noImplicitThis": true, 24 | "noImplicitReturns": false, 25 | "strict": true, 26 | "isolatedModules": true, 27 | 28 | "experimentalDecorators": true, 29 | "resolveJsonModule": true, 30 | "esModuleInterop": true, 31 | "removeComments": false, 32 | "jsx": "preserve", 33 | "lib": ["esnext", "dom"], 34 | "types": ["node"] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/docs/zh/about-translation.md: -------------------------------------------------------------------------------- 1 | # 关于中文翻译 2 | 3 | 这里是 Vue Router 文档的中文翻译,该翻译由 Vue 社区贡献完成,如对翻译有任何疑问,可在我们的 GitHub 仓库创建 issue 或 pull request。谢谢。 4 | 5 | ## 协作指南 6 | 7 | 在参与翻译之前,请移步这里了解翻译的基本流程和注意事项: 8 | 9 | https://github.com/vuejs/router/blob/main/.github/contributing.md#contributing-docs 10 | 11 | 如果想快速了解待更新的内容,可以访问此链接查阅 `packages/docs/` 部分的更新: 12 | 13 | https://github.com/vuejs/router/compare/docs-sync-zh...main 14 | 15 | 如果想快速了解待翻译的内容,可以访问此链接查阅 `packages/docs/zh/` 部分的更新: 16 | 17 | https://github.com/search?q=repo%3Avuejs%2Frouter+path%3Apackages%2Fdocs%2Fzh+TODO%3A+translation&type=code 18 | 19 | ## 翻译须知 20 | 21 | 首先,Vue Router 文档的中文翻译遵循 Vue 主站的翻译须知。详见: 22 | 23 | https://github.com/vuejs-translations/docs-zh-cn/wiki/%E7%BF%BB%E8%AF%91%E9%A1%BB%E7%9F%A5 24 | 25 | 其次,针对 Vue Router 本身的特殊语境,我们在上述翻译须知的基础上整理了一份附加的术语约定: 26 | 27 | | 英文 | 建议翻译 | 28 | | --- | --- | 29 | | active (link) | 匹配当前路由 (的链接) | 30 | | exact active (link) | 严格匹配当前路由 (的链接) | 31 | | location | 地址 | 32 | | (navigate) from | (导航) 离开 | 33 | | (navigate) to | (导航) 进入 | 34 | | (navigation) guard | (导航) 守卫 | 35 | | path | 路径 | 36 | 37 | ## 中文格式校验 38 | 39 | 我们使用 [zhlint](https://www.npmjs.com/package/zhlint) 对中文翻译的格式进行校对,用法可参考以下命令: 40 | 41 | ```bash 42 | pnpm add -g zhlint 43 | zhlint "./packages/docs/zh/**/*.md" 44 | ``` 45 | -------------------------------------------------------------------------------- /packages/docs/zh/api/enums/NavigationFailureType.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / NavigationFailureType 6 | 7 | # 枚举:NavigationFailureType %{#enumeration-navigationfailuretype}% 8 | 9 | 为导航失败枚举所有可能的类型。可以传入 [isNavigationFailure](../index.md#isnavigationfailure) 以检查特定的失败情况。 10 | 11 | ## 枚举成员 %{#Enumeration-Members}% 12 | 13 | ### aborted %{#Enumeration-Members-aborted}% 14 | 15 | • **aborted** = ``4`` 16 | 17 | 中断的导航是因为导航守卫返回 `false` 会调用了 `next(false)` 而导致失败的导航。 18 | 19 | ___ 20 | 21 | ### cancelled %{#Enumeration-Members-cancelled}% 22 | 23 | • **cancelled** = ``8`` 24 | 25 | 取消的导航是因为另一个更近的导航已经开始 (不需要完成) 而导致失败的导航。 26 | 27 | ___ 28 | 29 | ### duplicated %{#Enumeration-Members-duplicated}% 30 | 31 | • **duplicated** = ``16`` 32 | 33 | 重复的导航是因为其开始的时候已经处在相同的路径而导致失败的导航。 34 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/HistoryState.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / HistoryState 6 | 7 | # 接口:HistoryState %{#interface-historystate}% 8 | 9 | 允许的 HTML history.state 10 | 11 | ## 可索引成员 %{#Indexable}% 12 | 13 | ▪ [x: `number`]: `HistoryStateValue` 14 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/NavigationFailure.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / NavigationFailure 6 | 7 | # 接口:NavigationFailure %{#interface-navigationfailure}% 8 | 9 | Error 类型的扩展,包含导航失败的额外信息。 10 | 11 | ## 继承关系 %{#Hierarchy}% 12 | 13 | - `Error` 14 | 15 | ↳ **`NavigationFailure`** 16 | 17 | ## 属性 %{#Properties}% 18 | 19 | ### cause %{#Properties-cause}% 20 | 21 | • `可选` **cause**: `unknown` 22 | 23 | #### 继承自 %{#Properties-cause-Inherited-from}% 24 | 25 | Error.cause 26 | 27 | ___ 28 | 29 | ### from %{#Properties-from}% 30 | 31 | • **from**: [`RouteLocationNormalized`](RouteLocationNormalized.md) 32 | 33 | 上一个路由位置 34 | 35 | ___ 36 | 37 | ### message %{#Properties-message}% 38 | 39 | • **message**: `string` 40 | 41 | #### 继承自 %{#Properties-message-Inherited-from}% 42 | 43 | Error.message 44 | 45 | ___ 46 | 47 | ### name %{#Properties-name}% 48 | 49 | • **name**: `string` 50 | 51 | #### 继承自 %{#Properties-name-Inherited-from}% 52 | 53 | Error.name 54 | 55 | ___ 56 | 57 | ### stack %{#Properties-stack}% 58 | 59 | • `可选` **stack**: `string` 60 | 61 | #### 继承自 %{#Properties-stack-Inherited-from}% 62 | 63 | Error.stack 64 | 65 | ___ 66 | 67 | ### to %{#Properties-to}% 68 | 69 | • **to**: [`RouteLocationNormalized`](RouteLocationNormalized.md) 70 | 71 | 要导航至的下一个路由位置 72 | 73 | ___ 74 | 75 | ### type %{#Properties-type}% 76 | 77 | • **type**: `NAVIGATION_ABORTED` \| `NAVIGATION_CANCELLED` \| `NAVIGATION_DUPLICATED` 78 | 79 | 导航类型。属于 [NavigationFailureType](../enums/NavigationFailureType.md) 的一种。 80 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/NavigationGuard.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / NavigationGuard 6 | 7 | # 接口:NavigationGuard %{#interface-navigationguard}% 8 | 9 | 导航守卫。详情可查阅[导航守卫](/zh/guide/advanced/navigation-guards.md)。 10 | 11 | ## 可调用函数 %{#Callable}% 12 | 13 | ### NavigationGuard %{#Callable-NavigationGuard}% 14 | 15 | ▸ **NavigationGuard**(`to`, `from`, `next`): `NavigationGuardReturn` \| `Promise`\<`NavigationGuardReturn`\> 16 | 17 | #### 参数 %{#Callable-NavigationGuard-Parameters}% 18 | 19 | | 名称 | 类型 | 20 | | :------ | :------ | 21 | | `to` | [`RouteLocationNormalized`](RouteLocationNormalized.md) | 22 | | `from` | [`RouteLocationNormalized`](RouteLocationNormalized.md) | 23 | | `next` | [`NavigationGuardNext`](NavigationGuardNext.md) | 24 | 25 | #### 返回值 %{#Callable-NavigationGuard-Returns}% 26 | 27 | `NavigationGuardReturn` \| `Promise`\<`NavigationGuardReturn`\> 28 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/NavigationGuardNext.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / NavigationGuardNext 6 | 7 | # 接口:NavigationGuardNext %{#interface-navigationguardnext}% 8 | 9 | ## 可调用函数 %{#Callable}% 10 | 11 | ### NavigationGuardNext %{#Callable-NavigationGuardNext}% 12 | 13 | ▸ **NavigationGuardNext**(): `void` 14 | 15 | #### 返回值 %{#Callable-NavigationGuardNext-Returns}% 16 | 17 | `void` 18 | 19 | ### NavigationGuardNext %{#Callable-NavigationGuardNext_1}% 20 | 21 | ▸ **NavigationGuardNext**(`error`): `void` 22 | 23 | #### 参数 %{#Callable-NavigationGuardNext-Parameters}% 24 | 25 | | 名称 | 类型 | 26 | | :------ | :------ | 27 | | `error` | `Error` | 28 | 29 | #### 返回值 %{#Callable-NavigationGuardNext-Returns_1}% 30 | 31 | `void` 32 | 33 | ### NavigationGuardNext %{#Callable-NavigationGuardNext_2}% 34 | 35 | ▸ **NavigationGuardNext**(`location`): `void` 36 | 37 | #### 参数 %{#Callable-NavigationGuardNext-Parameters_1}% 38 | 39 | | 名称 | 类型 | 40 | | :------ | :------ | 41 | | `location` | [`RouteLocationRaw`](../index.md#Type-Aliases-RouteLocationRaw) | 42 | 43 | #### 返回值 %{#Callable-NavigationGuardNext-Returns_2}% 44 | 45 | `void` 46 | 47 | ### NavigationGuardNext %{#Callable-NavigationGuardNext_3}% 48 | 49 | ▸ **NavigationGuardNext**(`valid`): `void` 50 | 51 | #### 参数 %{#Callable-NavigationGuardNext-Parameters_2}% 52 | 53 | | 名称 | 类型 | 54 | | :------ | :------ | 55 | | `valid` | `undefined` \| `boolean` | 56 | 57 | #### 返回值 %{#Callable-NavigationGuardNext-Returns_3}% 58 | 59 | `void` 60 | 61 | ### NavigationGuardNext %{#Callable-NavigationGuardNext_4}% 62 | 63 | ▸ **NavigationGuardNext**(`cb`): `void` 64 | 65 | #### 参数 %{#Callable-NavigationGuardNext-Parameters_3}% 66 | 67 | | 名称 | 类型 | 68 | | :------ | :------ | 69 | | `cb` | `NavigationGuardNextCallback` | 70 | 71 | #### 返回值 %{#Callable-NavigationGuardNext-Returns_4}% 72 | 73 | `void` 74 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/NavigationGuardWithThis.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / NavigationGuardWithThis 6 | 7 | # 接口:NavigationGuardWithThis\ %{#interface-navigationguardwiththis-t}% 8 | 9 | 导航守卫。详情可查阅[导航守卫](/zh/guide/advanced/navigation-guards.md)。 10 | 11 | ## 类型参数 %{#Type-parameters}% 12 | 13 | | Name | 14 | | :------ | 15 | | `T` | 16 | 17 | ## 可调用函数 %{#Callable}% 18 | 19 | ### NavigationGuardWithThis %{#Callable-NavigationGuardWithThis}% 20 | 21 | ▸ **NavigationGuardWithThis**(`this`, `to`, `from`, `next`): `NavigationGuardReturn` \| `Promise`\<`NavigationGuardReturn`\> 22 | 23 | #### 参数 %{#Callable-NavigationGuardWithThis-Parameters}% 24 | 25 | | 名称 | 类型 | 26 | | :------ | :------ | 27 | | `this` | `T` | 28 | | `to` | [`RouteLocationNormalized`](RouteLocationNormalized.md) | 29 | | `from` | [`RouteLocationNormalized`](RouteLocationNormalized.md) | 30 | | `next` | [`NavigationGuardNext`](NavigationGuardNext.md) | 31 | 32 | #### 返回值 %{#Callable-NavigationGuardWithThis-Returns}% 33 | 34 | `NavigationGuardReturn` \| `Promise`\<`NavigationGuardReturn`\> 35 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/NavigationHookAfter.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / NavigationHookAfter 6 | 7 | # 接口:NavigationHookAfter %{#interface-navigationhookafter}% 8 | 9 | ## 可调用函数 %{#Callable}% 10 | 11 | ### NavigationHookAfter %{#Callable-NavigationHookAfter}% 12 | 13 | ▸ **NavigationHookAfter**(`to`, `from`, `failure?`): `any` 14 | 15 | #### 参数 %{#Callable-NavigationHookAfter-Parameters}% 16 | 17 | | 名称 | 类型 | 18 | | :------ | :------ | 19 | | `to` | [`RouteLocationNormalized`](RouteLocationNormalized.md) | 20 | | `from` | [`RouteLocationNormalized`](RouteLocationNormalized.md) | 21 | | `failure?` | `void` \| [`NavigationFailure`](NavigationFailure.md) | 22 | 23 | #### 返回值 %{#Callable-NavigationHookAfter-Returns}% 24 | 25 | `any` 26 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/RouteLocation.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / RouteLocation 6 | 7 | # 接口:RouteLocation %{#interface-routelocation}% 8 | 9 | 通过匹配解析出来的 [RouteLocationRaw](../index.md#Type-Aliases-RouteLocationRaw) 10 | 11 | ## 继承关系 %{#Hierarchy}% 12 | 13 | - `_RouteLocationBase` 14 | 15 | ↳ **`RouteLocation`** 16 | 17 | ## 属性 %{#Properties}% 18 | 19 | ### fullPath %{#Properties-fullPath}% 20 | 21 | • **fullPath**: `string` 22 | 23 | 包括 `search` 和 `hash` 在内的完整地址。该字符串是经过百分号编码的。 24 | 25 | #### 继承自 %{#Properties-fullPath-Inherited-from}% 26 | 27 | \_RouteLocationBase.fullPath 28 | 29 | ___ 30 | 31 | ### hash %{#Properties-hash}% 32 | 33 | • **hash**: `string` 34 | 35 | 当前地址的 hash。如果存在则以 `#` 开头。 36 | 37 | #### 继承自 %{#Properties-hash-Inherited-from}% 38 | 39 | \_RouteLocationBase.hash 40 | 41 | ___ 42 | 43 | ### matched %{#Properties-matched}% 44 | 45 | • **matched**: [`RouteRecordNormalized`](RouteRecordNormalized.md)[] 46 | 47 | 包含添加记录时被传入的 [RouteRecord](../index.md#routerecord) 的数组。它也可以包含重定向记录。不能直接使用。 48 | 49 | ___ 50 | 51 | ### meta %{#Properties-meta}% 52 | 53 | • **meta**: [`RouteMeta`](RouteMeta.md) 54 | 55 | 从所有匹配的路由记录中合并的 `meta` 属性。 56 | 57 | #### 继承自 %{#Properties-meta-Inherited-from}% 58 | 59 | \_RouteLocationBase.meta 60 | 61 | ___ 62 | 63 | ### name %{#Properties-name}% 64 | 65 | • **name**: `undefined` \| ``null`` \| [`RouteRecordName`](../index.md#routerecordname) 66 | 67 | 匹配的路由名称。 68 | 69 | #### 继承自 %{#Properties-name-Inherited-from}% 70 | 71 | \_RouteLocationBase.name 72 | 73 | ___ 74 | 75 | ### params %{#Properties-params}% 76 | 77 | • **params**: [`RouteParams`](../index.md#routeparams) 78 | 79 | 从 `path` 中提取出来并解码后的参数对象。 80 | 81 | #### 继承自 %{#Properties-params-Inherited-from}% 82 | 83 | \_RouteLocationBase.params 84 | 85 | ___ 86 | 87 | ### path %{#Properties-path}% 88 | 89 | • **path**: `string` 90 | 91 | 经过百分号编码的 URL 中的 pathname 段。 92 | 93 | #### 继承自 %{#Properties-path-Inherited-from}% 94 | 95 | \_RouteLocationBase.path 96 | 97 | ___ 98 | 99 | ### query %{#Properties-query}% 100 | 101 | • **query**: [`LocationQuery`](../index.md#locationquery) 102 | 103 | 代表当前地址的 `search` 属性的对象 104 | 105 | #### 继承自 %{#Properties-query-Inherited-from}% 106 | 107 | \_RouteLocationBase.query 108 | 109 | ___ 110 | 111 | ### redirectedFrom %{#Properties-redirectedFrom}% 112 | 113 | • **redirectedFrom**: `undefined` \| [`RouteLocation`](RouteLocation.md) 114 | 115 | 包含在重定向到当前地址之前,我们最初想访问的地址。 116 | 117 | #### 继承自 %{#Properties-redirectedFrom-Inherited-from}% 118 | 119 | \_RouteLocationBase.redirectedFrom 120 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/RouteLocationNormalized.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / RouteLocationNormalized 6 | 7 | # 接口:RouteLocationNormalized 8 | 9 | 和 [RouteLocation](RouteLocation.md) 类似但是其 [RouteLocationNormalized.matched](RouteLocationNormalized.md#matched) 无法包含重定向的记录 10 | 11 | ## 继承关系 %{#Hierarchy}% 12 | 13 | - `_RouteLocationBase` 14 | 15 | ↳ **`RouteLocationNormalized`** 16 | 17 | ## 属性 %{#Properties}% 18 | 19 | ### fullPath %{#Properties-fullPath}% 20 | 21 | • **fullPath**: `string` 22 | 23 | 包括 `search` 和 `hash` 在内的完整地址。该字符串是经过百分号编码的。 24 | 25 | #### 继承自 %{#Properties-fullPath-Inherited-from}% 26 | 27 | \_RouteLocationBase.fullPath 28 | 29 | ___ 30 | 31 | ### hash %{#Properties-hash}% 32 | 33 | • **hash**: `string` 34 | 35 | 当前地址的 hash。如果存在则以 `#` 开头。 36 | 37 | #### 继承自 %{#Properties-hash-Inherited-from}% 38 | 39 | \_RouteLocationBase.hash 40 | 41 | ___ 42 | 43 | ### matched %{#Properties-matched}% 44 | 45 | • **matched**: [`RouteRecordNormalized`](RouteRecordNormalized.md)[] 46 | 47 | [RouteRecordNormalized](RouteRecordNormalized.md) 数组。 48 | 49 | ___ 50 | 51 | ### meta %{#Properties-meta}% 52 | 53 | • **meta**: [`RouteMeta`](RouteMeta.md) 54 | 55 | 从所有匹配的路由记录中合并的 `meta` 属性。 56 | 57 | #### 继承自 %{#Properties-meta-Inherited-from}% 58 | 59 | \_RouteLocationBase.meta 60 | 61 | ___ 62 | 63 | ### name %{#Properties-name}% 64 | 65 | • **name**: `undefined` \| ``null`` \| [`RouteRecordName`](../index.md#routerecordname) 66 | 67 | 匹配的路由名称。 68 | 69 | #### 继承自 %{#Properties-name-Inherited-from}% 70 | 71 | \_RouteLocationBase.name 72 | 73 | ___ 74 | 75 | ### params %{#Properties-params}% 76 | 77 | • **params**: [`RouteParams`](../index.md#routeparams) 78 | 79 | 从 `path` 中提取出来并解码后的参数对象。 80 | 81 | #### 继承自 %{#Properties-params-Inherited-from}% 82 | 83 | \_RouteLocationBase.params 84 | 85 | ___ 86 | 87 | ### path %{#Properties-path}% 88 | 89 | • **path**: `string` 90 | 91 | 经过百分号编码的 URL 中的 pathname 段。 92 | 93 | #### 继承自 %{#Properties-path-Inherited-from}% 94 | 95 | \_RouteLocationBase.path 96 | 97 | ___ 98 | 99 | ### query %{#Properties-query}% 100 | 101 | • **query**: [`LocationQuery`](../index.md#locationquery) 102 | 103 | 代表当前地址的 `search` 属性的对象 104 | 105 | #### 继承自 %{#Properties-query-Inherited-from}% 106 | 107 | \_RouteLocationBase.query 108 | 109 | ___ 110 | 111 | ### redirectedFrom %{#Properties-redirectedFrom}% 112 | 113 | • **redirectedFrom**: `undefined` \| [`RouteLocation`](RouteLocation.md) 114 | 115 | 包含在重定向到当前地址之前,我们最初想访问的地址。 116 | 117 | #### 继承自 %{#Properties-redirectedFrom-Inherited-from}% 118 | 119 | \_RouteLocationBase.redirectedFrom 120 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/RouteLocationNormalizedLoaded.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / RouteLocationNormalizedLoaded 6 | 7 | # 接口:RouteLocationNormalizedLoaded 8 | 9 | 10 | 11 | [RouteLocationRaw](../index.md#Type-Aliases-RouteLocationRaw) with 12 | 13 | ## 继承关系 %{#Hierarchy}% 14 | 15 | - `_RouteLocationBase` 16 | 17 | ↳ **`RouteLocationNormalizedLoaded`** 18 | 19 | ## 属性 %{#Properties}% 20 | 21 | ### fullPath %{#Properties-fullPath}% 22 | 23 | • **fullPath**: `string` 24 | 25 | 包括 `search` 和 `hash` 在内的完整地址。该字符串是经过百分号编码的。 26 | 27 | #### 继承自 %{#Properties-fullPath-Inherited-from}% 28 | 29 | \_RouteLocationBase.fullPath 30 | 31 | ___ 32 | 33 | ### hash %{#Properties-hash}% 34 | 35 | • **hash**: `string` 36 | 37 | 当前地址的 hash。如果存在则以 `#` 开头。 38 | 39 | #### 继承自 %{#Properties-hash-Inherited-from}% 40 | 41 | \_RouteLocationBase.hash 42 | 43 | ___ 44 | 45 | ### matched %{#Properties-matched}% 46 | 47 | • **matched**: [`RouteLocationMatched`](RouteLocationMatched.md)[] 48 | 49 | [RouteLocationMatched](RouteLocationMatched.md) 数组,只包含直接的组件 (任何已被加载并在 `components` 对象内被替换掉的懒加载组件)。所以它可以被直接用于展示路由。同样它不包含重定向的记录。 50 | 51 | ___ 52 | 53 | ### meta %{#Properties-meta}% 54 | 55 | • **meta**: [`RouteMeta`](RouteMeta.md) 56 | 57 | 从所有匹配的路由记录中合并的 `meta` 属性。 58 | 59 | #### 继承自 %{#Properties-meta-Inherited-from}% 60 | 61 | \_RouteLocationBase.meta 62 | 63 | ___ 64 | 65 | ### name %{#Properties-name}% 66 | 67 | • **name**: `undefined` \| ``null`` \| [`RouteRecordName`](../index.md#routerecordname) 68 | 69 | 匹配的路由名称。 70 | 71 | #### 继承自 %{#Properties-name-Inherited-from}% 72 | 73 | \_RouteLocationBase.name 74 | 75 | ___ 76 | 77 | ### params %{#Properties-params}% 78 | 79 | • **params**: [`RouteParams`](../index.md#routeparams) 80 | 81 | 从 `path` 中提取出来并解码后的参数对象。 82 | 83 | #### 继承自 %{#Properties-params-Inherited-from}% 84 | 85 | \_RouteLocationBase.params 86 | 87 | ___ 88 | 89 | ### path %{#Properties-path}% 90 | 91 | • **path**: `string` 92 | 93 | 经过百分号编码的 URL 中的 pathname 段。 94 | 95 | #### 继承自 %{#Properties-path-Inherited-from}% 96 | 97 | \_RouteLocationBase.path 98 | 99 | ___ 100 | 101 | ### query %{#Properties-query}% 102 | 103 | • **query**: [`LocationQuery`](../index.md#locationquery) 104 | 105 | 代表当前地址的 `search` 属性的对象 106 | 107 | #### 继承自 %{#Properties-query-Inherited-from}% 108 | 109 | \_RouteLocationBase.query 110 | 111 | ___ 112 | 113 | ### redirectedFrom %{#Properties-redirectedFrom}% 114 | 115 | • **redirectedFrom**: `undefined` \| [`RouteLocation`](RouteLocation.md) 116 | 117 | 包含在重定向到当前地址之前,我们最初想访问的地址。 118 | 119 | #### 继承自 %{#Properties-redirectedFrom-Inherited-from}% 120 | 121 | \_RouteLocationBase.redirectedFrom 122 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/RouteLocationOptions.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / RouteLocationOptions 6 | 7 | # 接口:RouteLocationOptions 8 | 9 | 对所有导航方法通用的选项。 10 | 11 | ## 属性 %{#Properties}% 12 | 13 | ### force %{#Properties-force}% 14 | 15 | • `可选` **force**: `boolean` 16 | 17 | 触发导航,即使该地址与当前地址相同。请注意,这也会新添加一条历史记录,除非传入 `replace: true`。 18 | 19 | ___ 20 | 21 | ### replace %{#Properties-replace}% 22 | 23 | • `可选` **replace**: `boolean` 24 | 25 | 替换而不是加入一个新的历史记录。 26 | 27 | ___ 28 | 29 | ### state %{#Properties-state}% 30 | 31 | • `可选` **state**: [`HistoryState`](HistoryState.md) 32 | 33 | 使用 History API 保存的状态。它不能包含任何响应性的值,同时一些诸如 Symbol 的基础类型是被禁用的。更多信息见 https://developer.mozilla.org/en-US/docs/Web/API/History/state 34 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/RouteMeta.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / RouteMeta 6 | 7 | # 接口:RouteMeta 8 | 9 | 路由记录中的 `meta` 字段的类型接口。 10 | 11 | **`Example`** 12 | 13 | ```ts 14 | // typings.d.ts 或 router.ts 15 | import 'vue-router'; 16 | 17 | declare module 'vue-router' { 18 | interface RouteMeta { 19 | requiresAuth?: boolean 20 | } 21 | } 22 | ``` 23 | 24 | ## 继承关系 %{#Hierarchy}% 25 | 26 | - `Record`\<`string` \| `number` \| `symbol`, `unknown`\> 27 | 28 | ↳ **`RouteMeta`** 29 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/RouteRecordNormalized.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / RouteRecordNormalized 6 | 7 | # 接口:RouteRecordNormalized 8 | 9 | 一条[路由记录](../index.md#routerecord)的规范化版本。 10 | 11 | ## 继承关系 %{#Hierarchy}% 12 | 13 | - **`RouteRecordNormalized`** 14 | 15 | ↳ [`RouteLocationMatched`](RouteLocationMatched.md) 16 | 17 | ## 属性 %{#Properties}% 18 | 19 | ### aliasOf %{#Properties-aliasOf}% 20 | 21 | • **aliasOf**: `undefined` \| [`RouteRecordNormalized`](RouteRecordNormalized.md) 22 | 23 | 定义了是否这条记录是另一条的别名。如果记录是原始记录,则该属性为 `undefined`。 24 | 25 | ___ 26 | 27 | ### beforeEnter %{#Properties-beforeEnter}% 28 | 29 | • **beforeEnter**: `undefined` \| [`NavigationGuardWithThis`](NavigationGuardWithThis.md)\<`undefined`\> \| [`NavigationGuardWithThis`](NavigationGuardWithThis.md)\<`undefined`\>[] 30 | 31 | 被注册的 beforeEnter 守卫 32 | 33 | ___ 34 | 35 | ### children %{#Properties-children}% 36 | 37 | • **children**: [`RouteRecordRaw`](../index.md#routerecordraw)[] 38 | 39 | 嵌套的路由记录。 40 | 41 | ___ 42 | 43 | ### components %{#Properties-components}% 44 | 45 | • **components**: `undefined` \| ``null`` \| `Record`\<`string`, `RawRouteComponent`\> 46 | 47 | 当 URL 匹配到该路由时显示的组件。允许使用命名视图。 48 | 49 | ___ 50 | 51 | ### instances %{#Properties-instances}% 52 | 53 | • **instances**: `Record`\<`string`, `undefined` \| ``null`` \| `ComponentPublicInstance`\> 54 | 55 | 挂载的路由组件实例。 56 | 在记录上存在实例意味着,当有多个应用实例渲染相同的视图时,beforeRouteUpdate 和 beforeRouteLeave 守卫只能被最后挂载的应用实例调用。这样的渲染基本上只会对页面内容进行复制,在实际情况下并不应该发生。它可以在多个应用渲染不同的命名视图时工作。 57 | 58 | ___ 59 | 60 | ### meta %{#Properties-meta}% 61 | 62 | • **meta**: [`RouteMeta`](RouteMeta.md) 63 | 64 | 附加在记录上的任意数据。 65 | 66 | ___ 67 | 68 | ### name %{#Properties-name}% 69 | 70 | • **name**: `undefined` \| [`RouteRecordName`](../index.md#routerecordname) 71 | 72 | 路由记录的名称。必须唯一。 73 | 74 | ___ 75 | 76 | ### path %{#Properties-path}% 77 | 78 | • **path**: `string` 79 | 80 | 记录的路径。应该以 `/` 开头,除非该记录为另一条记录的子记录。 81 | 82 | ___ 83 | 84 | ### props %{#Properties-props}% 85 | 86 | • **props**: `Record`\<`string`, `_RouteRecordProps`\> 87 | 88 | 允许将参数作为 props 传递给由 `router-view` 渲染的组件。应是一个具有与 `components` 相同键的对象,或是一个应用于所有组件的布尔值。 89 | 90 | ___ 91 | 92 | ### redirect %{#Properties-redirect}% 93 | 94 | • **redirect**: `undefined` \| `RouteRecordRedirectOption` 95 | 96 | 路由直接匹配时重定向的位置。重定向发生在任何导航守卫和带有新目标位置的新导航触发之前。 97 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/RouterLinkProps.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / RouterLinkProps 6 | 7 | # 接口:RouterLinkProps 8 | 9 | ## 继承关系 %{#Hierarchy}% 10 | 11 | - `RouterLinkOptions` 12 | 13 | ↳ **`RouterLinkProps`** 14 | 15 | ## 属性 %{#Properties}% 16 | 17 | ### activeClass %{#Properties-activeClass}% 18 | 19 | • `可选` **activeClass**: `string` 20 | 21 | 链接在匹配当前路由时被应用到 class。 22 | 23 | ___ 24 | 25 | ### ariaCurrentValue %{#Properties-ariaCurrentValue}% 26 | 27 | • `可选` **ariaCurrentValue**: ``"location"`` \| ``"time"`` \| ``"page"`` \| ``"step"`` \| ``"date"`` \| ``"true"`` \| ``"false"`` 28 | 29 | 链接在匹配当前路由时传入 `aria-current` attribute 的值。 30 | 31 | **`Default Value`** 32 | 33 | `'page'` 34 | 35 | ___ 36 | 37 | ### custom %{#Properties-custom}% 38 | 39 | • `可选` **custom**: `boolean` 40 | 41 | RouterLink 是否应该将其内容包裹在一个 `a` 标签里。用于通过 `v-slot` 创建自定义 RouterLink。 42 | 43 | ___ 44 | 45 | ### exactActiveClass %{#Properties-exactActiveClass}% 46 | 47 | • `可选` **exactActiveClass**: `string` 48 | 49 | 链接在严格匹配当前路由时被应用到 class。 50 | 51 | ___ 52 | 53 | ### replace %{#Properties-replace}% 54 | 55 | • `可选` **replace**: `boolean` 56 | 57 | 调用 `router.replace` 以替换 `router.push`。 58 | 59 | #### 继承自 %{#Properties-replace-Inherited-from}% 60 | 61 | RouterLinkOptions.replace 62 | 63 | ___ 64 | 65 | ### to %{#Properties-to}% 66 | 67 | • **to**: [`RouteLocationRaw`](../index.md#Type-Aliases-RouteLocationRaw) 68 | 69 | 当点击该链接时应该进入的路由地址。 70 | 71 | #### 继承自 %{#Properties-to-Inherited-from}% 72 | 73 | RouterLinkOptions.to 74 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/RouterScrollBehavior.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / RouterScrollBehavior 6 | 7 | # 接口:RouterScrollBehavior 8 | 9 | 可以被传递给 `createRouter` 的 `scrollBehavior` 选项的类型。 10 | 11 | ## 可调用函数 %{#Callable}% 12 | 13 | ### RouterScrollBehavior %{#Callable-RouterScrollBehavior}% 14 | 15 | ▸ **RouterScrollBehavior**(`to`, `from`, `savedPosition`): `Awaitable`\<``false`` \| `void` \| `ScrollPosition`\> 16 | 17 | #### 参数 %{#Callable-RouterScrollBehavior-Parameters}% 18 | 19 | | 名称 | 类型 | 描述 | 20 | | :------ | :------ | :------ | 21 | | `to` | [`RouteLocationNormalized`](RouteLocationNormalized.md) | 我们要导航到的路由地址 | 22 | | `from` | [`RouteLocationNormalizedLoaded`](RouteLocationNormalizedLoaded.md) | 我们要离开的路由地址 | 23 | | `savedPosition` | ``null`` \| `_ScrollPositionNormalized` | 要保存的页面位置,如果不存在则是 `null` | 24 | 25 | #### 返回值 %{#Callable-RouterScrollBehavior-Returns}% 26 | 27 | `Awaitable`\<``false`` \| `void` \| `ScrollPosition`\> 28 | -------------------------------------------------------------------------------- /packages/docs/zh/api/interfaces/RouterViewProps.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | [API 参考](../index.md) / RouterViewProps 6 | 7 | # 接口:RouterViewProps 8 | 9 | ## 属性 %{#Properties}% 10 | 11 | ### name %{#Properties-name}% 12 | 13 | • `可选` **name**: `string` 14 | 15 | ___ 16 | 17 | ### route %{#Properties-route}% 18 | 19 | • `可选` **route**: [`RouteLocationNormalized`](RouteLocationNormalized.md) 20 | -------------------------------------------------------------------------------- /packages/docs/zh/guide/advanced/lazy-loading.md: -------------------------------------------------------------------------------- 1 | # 路由懒加载 2 | 3 | 7 | 8 | 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。 9 | 10 | Vue Router 支持开箱即用的[动态导入](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports),这意味着你可以用动态导入代替静态导入: 11 | 12 | ```js 13 | // 将 14 | // import UserDetails from './views/UserDetails.vue' 15 | // 替换成 16 | const UserDetails = () => import('./views/UserDetails.vue') 17 | 18 | const router = createRouter({ 19 | // ... 20 | routes: [ 21 | { path: '/users/:id', component: UserDetails } 22 | // 或在路由定义里直接使用它 23 | { path: '/users/:id', component: () => import('./views/UserDetails.vue') }, 24 | ], 25 | }) 26 | ``` 27 | 28 | `component` (和 `components`) 配置接收一个返回 Promise 组件的函数,Vue Router **只会在第一次进入页面时才会获取这个函数**,然后使用缓存数据。这意味着你也可以使用更复杂的函数,只要它们返回一个 Promise : 29 | 30 | ```js 31 | const UserDetails = () => 32 | Promise.resolve({ 33 | /* 组件定义 */ 34 | }) 35 | ``` 36 | 37 | 一般来说,对所有的路由**都使用动态导入**是个好主意。 38 | 39 | ::: tip 注意 40 | **不要**在路由中使用[异步组件](https://cn.vuejs.org/guide/components/async.html)。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。 41 | ::: 42 | 43 | 如果你使用的是 webpack 之类的打包器,它将自动从[代码分割](https://webpack.js.org/guides/code-splitting/)中受益。 44 | 45 | 如果你使用的是 Babel,你将需要添加 [syntax-dynamic-import](https://babeljs.io/docs/plugins/syntax-dynamic-import/) 插件,才能使 Babel 正确地解析语法。 46 | 47 | ## 把组件按组分块 48 | 49 | ### 使用 webpack 50 | 51 | 有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用[命名 chunk](https://webpack.js.org/guides/code-splitting/#dynamic-imports),一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4): 52 | 53 | ```js 54 | const UserDetails = () => 55 | import(/* webpackChunkName: "group-user" */ './UserDetails.vue') 56 | const UserDashboard = () => 57 | import(/* webpackChunkName: "group-user" */ './UserDashboard.vue') 58 | const UserProfileEdit = () => 59 | import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue') 60 | ``` 61 | 62 | webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。 63 | 64 | ### 使用 Vite 65 | 66 | 在Vite中,你可以在[`rollupOptions`](https://cn.vite.dev/config/build-options.html#build-rollupoptions)下定义分块: 67 | 68 | ```js 69 | // vite.config.js 70 | export default defineConfig({ 71 | build: { 72 | rollupOptions: { 73 | // https://rollupjs.org/guide/en/#outputmanualchunks 74 | output: { 75 | manualChunks: { 76 | 'group-user': [ 77 | './src/UserDetails', 78 | './src/UserDashboard', 79 | './src/UserProfileEdit', 80 | ], 81 | }, 82 | }, 83 | }, 84 | }, 85 | }) 86 | ``` 87 | -------------------------------------------------------------------------------- /packages/docs/zh/guide/advanced/meta.md: -------------------------------------------------------------------------------- 1 | # 路由元信息 2 | 3 | 7 | 8 | 有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的`meta`属性来实现,并且它可以在路由地址和导航守卫上都被访问到。定义路由的时候你可以这样配置 `meta` 字段: 9 | 10 | ```js 11 | const routes = [ 12 | { 13 | path: '/posts', 14 | component: PostsLayout, 15 | children: [ 16 | { 17 | path: 'new', 18 | component: PostsNew, 19 | // 只有经过身份验证的用户才能创建帖子 20 | meta: { requiresAuth: true }, 21 | }, 22 | { 23 | path: ':id', 24 | component: PostsDetail 25 | // 任何人都可以阅读文章 26 | meta: { requiresAuth: false }, 27 | } 28 | ] 29 | } 30 | ] 31 | ``` 32 | 33 | 那么如何访问这个 `meta` 字段呢? 34 | 35 | 36 | 37 | 首先,我们称呼 `routes` 配置中的每个路由对象为 **路由记录**。路由记录可以是嵌套的,因此,当一个路由匹配成功后,它可能匹配多个路由记录。 38 | 39 | 例如,根据上面的路由配置,`/posts/new` 这个 URL 将会匹配父路由记录 (`path: '/posts'`) 以及子路由记录 (`path: 'new'`)。 40 | 41 | 一个路由匹配到的所有路由记录会暴露为 `route` 对象(还有在导航守卫中的路由对象)的`route.matched` 数组。我们需要遍历这个数组来检查路由记录中的 `meta` 字段,但是 Vue Router 还为你提供了一个 `route.meta` 方法,它是一个非递归合并**所有 `meta`** 字段(从父字段到子字段)的方法。这意味着你可以简单地写 42 | 43 | ```js 44 | router.beforeEach((to, from) => { 45 | // 而不是去检查每条路由记录 46 | // to.matched.some(record => record.meta.requiresAuth) 47 | if (to.meta.requiresAuth && !auth.isLoggedIn()) { 48 | // 此路由需要授权,请检查是否已登录 49 | // 如果没有,则重定向到登录页面 50 | return { 51 | path: '/login', 52 | // 保存我们所在的位置,以便以后再来 53 | query: { redirect: to.fullPath }, 54 | } 55 | } 56 | }) 57 | ``` 58 | 59 | ## TypeScript 60 | 61 | 也可以继承来自 `vue-router` 中的 `RouteMeta` 来为 meta 字段添加类型: 62 | 63 | ```ts 64 | // 这段可以直接添加到你的任何 `.ts` 文件中,例如 `router.ts` 65 | // 也可以添加到一个 `.d.ts` 文件中。确保这个文件包含在 66 | // 项目的 `tsconfig.json` 中的 "file" 字段内。 67 | import 'vue-router' 68 | 69 | // 为了确保这个文件被当作一个模块,添加至少一个 `export` 声明 70 | export {} 71 | 72 | declare module 'vue-router' { 73 | interface RouteMeta { 74 | // 是可选的 75 | isAdmin?: boolean 76 | // 每个路由都必须声明 77 | requiresAuth: boolean 78 | } 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /packages/docs/zh/guide/advanced/router-view-slot.md: -------------------------------------------------------------------------------- 1 | # RouterView 插槽 2 | 3 | RouterView 组件暴露了一个插槽,可以用来渲染路由组件: 4 | 5 | ```vue-html 6 | 7 | 8 | 9 | ``` 10 | 11 | 上面的代码等价于不带插槽的 ``,但是当我们想要获得其他功能时,插槽提供了额外的扩展性。 12 | 13 | ## KeepAlive & Transition 14 | 15 | 当在处理 [KeepAlive](https://vuejs.org/guide/built-ins/keep-alive.html) 组件时,我们通常想要保持路由组件活跃,而不是 RouterView 本身。为了实现这个目的,我们可以将 KeepAlive 组件放置在插槽内: 16 | 17 | ```vue-html 18 | 19 | 20 | 21 | 22 | 23 | ``` 24 | 25 | 类似地,插槽允许我们使用一个 [Transition](https://vuejs.org/guide/built-ins/transition.html) 组件来实现在路由组件之间切换时实现过渡效果: 26 | 27 | ```vue-html 28 | 29 | 30 | 31 | 32 | 33 | ``` 34 | 35 | 我们也可以在 Transition 组件内使用 KeepAlive 组件: 36 | 37 | ```vue-html 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ``` 46 | 47 | 关于更多 RouterView 组件和 Transition 组件之间的互动,请参考 [Transitions](./transitions) 指南。 48 | 49 | ## 传递 props 和插槽 50 | 51 | 我们可以利用其插槽给路由组件传递 props 或插槽: 52 | 53 | ```vue-html 54 | 55 | 56 |

Some slotted content

57 |
58 |
59 | ``` 60 | 61 | 实践中通常不会这么做,因为这样会导致所有路由组件**都使用相同的 props 和插槽**。请查阅[传递 props 给路由组件](../essentials/passing-props)获取其他传递 props 的方式。 62 | 63 | ## 模板引用 64 | 65 | 使用插槽可以让我们直接将[模板引用](https://vuejs.org/guide/essentials/template-refs.html)放置在路由组件上: 66 | 67 | ```vue-html 68 | 69 | 70 | 71 | ``` 72 | 73 | 而如果我们将引用放在 `` 上,那引用将会被 RouterView 的实例填充,而不是路由组件本身。 74 | -------------------------------------------------------------------------------- /packages/docs/zh/guide/advanced/transitions.md: -------------------------------------------------------------------------------- 1 | # 过渡动效 2 | 3 | 7 | 8 | 想要在你的路径组件上使用转场,并对导航进行动画处理,你需要使用 [`` 插槽](./router-view-slot): 9 | 10 | ```html 11 | 12 | 13 | 14 | 15 | 16 | ``` 17 | 18 | [Transition 的 API](https://cn.vuejs.org/guide/built-ins/transition.html) 在这里同样适用。 19 | 20 | ## 单个路由的过渡 21 | 22 | 上面的用法会对所有的路由使用相同的过渡。如果你想让每个路由的组件有不同的过渡,你可以将[元信息](./meta.md)和动态的 `name` 结合在一起,放在`` 上: 23 | 24 | ```js 25 | const routes = [ 26 | { 27 | path: '/custom-transition', 28 | component: PanelLeft, 29 | meta: { transition: 'slide-left' }, 30 | }, 31 | { 32 | path: '/other-transition', 33 | component: PanelRight, 34 | meta: { transition: 'slide-right' }, 35 | }, 36 | ] 37 | ``` 38 | 39 | ```html 40 | 41 | 42 | 43 | 44 | 45 | 46 | ``` 47 | 48 | ## 基于路由的动态过渡 49 | 50 | 也可以根据目标路由和当前路由之间的关系,动态地确定使用的过渡。使用和刚才非常相似的片段: 51 | 52 | ```html 53 | 54 | 55 | 56 | 57 | 58 | 59 | ``` 60 | 61 | 我们可以添加一个 [after navigation hook](./navigation-guards.md#全局后置钩子),根据路径的深度动态添加信息到 `meta` 字段。 62 | 63 | ```js 64 | router.afterEach((to, from) => { 65 | const toDepth = to.path.split('/').length 66 | const fromDepth = from.path.split('/').length 67 | to.meta.transition = toDepth < fromDepth ? 'slide-right' : 'slide-left' 68 | }) 69 | ``` 70 | 71 | ## 强制在复用的视图之间进行过渡 72 | 73 | Vue 可能会自动复用看起来相似的组件,从而忽略了任何过渡。幸运的是,可以[添加一个 `key` 属性](https://cn.vuejs.org/api/built-in-special-attributes.html#key)来强制过渡。这也允许你在相同路由上使用不同的参数触发过渡: 74 | 75 | ```vue-html 76 | 77 | 78 | 79 | 80 | 81 | ``` 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /packages/docs/zh/guide/advanced/typed-routes.md: -------------------------------------------------------------------------------- 1 | # 类型化路由 (v4.1.0+) 2 | 3 | ::: danger ‼️ 实验性功能 4 | 5 | 从 v4.1.0 开始,我们引入一个新的功能,称为类型化路由。这个**实验性**功能通过 Vite/webpack/Rollup 插件启用。 6 | 7 | ![RouterLink to autocomplete](https://user-images.githubusercontent.com/664177/176442066-c4e7fa31-4f06-4690-a49f-ed0fd880dfca.png) 8 | 9 | [查看 v4.1 版本的发布说明](https://github.com/vuejs/router/releases/tag/v4.1.0) 获取有关此功能的更多信息。[查看插件](https://github.com/posva/unplugin-vue-router) 的 GitHub 仓库获取安装说明和文档。 10 | -------------------------------------------------------------------------------- /packages/docs/zh/guide/essentials/active-links.md: -------------------------------------------------------------------------------- 1 | # 匹配当前路由的链接 2 | 3 | 应用程序通常都会有一个渲染 RouterLink 列表的导航组件。我们也许想对这个列表中匹配当前路由的链接进行视觉区分。 4 | 5 | RouterLink 组件会为匹配当前路由的链接添加两个 CSS 类,`router-link-active` 和 `router-link-exact-active`。要理解它们之间的区别,我们首先需要了解 Vue Router 如何判断一个链接是**匹配当前路由的**。 6 | 7 | ## 链接在什么时候匹配当前路由 8 | 9 | 当满足以下条件时,RouterLink 被认为是**匹配当前路由的**: 10 | 11 | 1. 它与当前路径匹配相同的路由记录(即配置的路由)。 12 | 2. 它的 `params` 与当前路径的 `params` 相同。 13 | 14 | 如果你使用了[嵌套路由](./nested-routes),任何指向祖先路由的链接也会被认为是匹配当前路由的,只要相关的 `params` 匹配。 15 | 16 | 其他路由属性,例如 [`query`](../../api/interfaces/RouteLocationNormalized#query),不会被考虑在内。 17 | 18 | 路径不一定需要完全匹配。例如,使用 [`alias`](./redirect-and-alias#Alias) 仍然会被认为是匹配的,只要它解析到相同的路由记录和 `params`。 19 | 20 | 如果一个路由有 [`redirect`](./redirect-and-alias#Redirect),在检查链接是否匹配当前路由时不会跟随重定向。 21 | 22 | ## 精确匹配当前路由的链接 23 | 24 | **精确**匹配不包括祖先路由。 25 | 26 | 假设我们有以下路由: 27 | 28 | ```js 29 | const routes = [ 30 | { 31 | path: '/user/:username', 32 | component: User, 33 | children: [ 34 | { 35 | path: 'role/:roleId', 36 | component: Role, 37 | } 38 | ] 39 | } 40 | ] 41 | ``` 42 | 43 | 然后考虑这两个链接: 44 | 45 | ```vue-html 46 | 47 | User 48 | 49 | 50 | Role 51 | 52 | ``` 53 | 54 | 如果当前路径是 `/user/erina/role/admin`,那么这两个链接都会被认为是**匹配当前路由的**,因此 `router-link-active` 类会应用于这两个链接。但只有第二个链接会被认为是**精确的**,因此只有第二个链接会有 `router-link-exact-active` 类。 55 | 56 | ## 配置类名 57 | 58 | RouterLink 组件有两个属性,`activeClass` 和 `exactActiveClass`,可以用来更改应用的类名: 59 | 60 | ```vue-html 61 | 66 | ``` 67 | 68 | 默认的类名也可以通过传递 `linkActiveClass` 和 `linkExactActiveClass` 选项给 `createRouter()` 来全局更改: 69 | 70 | ```js 71 | const router = createRouter({ 72 | linkActiveClass: 'border-indigo-500', 73 | linkExactActiveClass: 'border-indigo-700', 74 | // ... 75 | }) 76 | ``` 77 | 78 | 参见[扩展 RouterLink](../advanced/extending-router-link) 以获取使用 `v-slot` API 进行更高级自定义的技术。 79 | -------------------------------------------------------------------------------- /packages/docs/zh/guide/essentials/named-routes.md: -------------------------------------------------------------------------------- 1 | # 命名路由 2 | 3 | 7 | 8 | 当创建一个路由时,我们可以选择给路由一个 `name`: 9 | 10 | ```js 11 | const routes = [ 12 | { 13 | path: '/user/:username', 14 | name: 'profile', // [!code highlight] 15 | component: User 16 | } 17 | ] 18 | ``` 19 | 20 | 然后我们可以使用 `name` 而不是 `path` 来传递 `to` 属性给 ``: 21 | 22 | ```vue-html 23 | 24 | User profile 25 | 26 | ``` 27 | 28 | 上述示例将创建一个指向 `/user/erina` 的链接。 29 | 30 | - [在演练场上查看](https://play.vuejs.org/#eNqtVVtP2zAU/itWNqlFauNNIB6iUMEQEps0NjH2tOzBtKY1JLZlO6VTlP++4+PcelnFwyRofe7fubaKCiZk/GyjJBKFVsaRiswNZ45faU1q8mRUQUbrko8yuaPwlRfK/LkV1sHXpGHeq9JxMzScGmT19t5xkMaUaR1vOb9VBe+kntgWXz2Cs06O1LbCTwvRW7knGnEm50paRwIYcrEFd1xlkpBVyCQ5lN74ZOJV0Nom5JcnCFRCM7dKyIiOJkSygsNzBZiBmivAI7l0SUipRvuhCfPge7uWHBiGZPctS0iLJv7T2/YutFFPIt+JjgUJPn7DZ32CtWg7PIZ/4BASg7txKE6gC1VKNx69gw6NTqJJ1HQK5iR1vNA52M+8Yrr6OLuD+AuCtbQpBQYK9Oy6NAZAhLI1KKuKvEc69jSp65Tqw/oh3V7f00P9MsdveOWiecE75DDNhXwhiVMXWVRttYbUWdRpE2xOZ0sHxq1v2jl/a5jQyZ042Mv/HKjvt2aGFTCXFWmnAsTcCMkAxw4SHIjG9E2AUtpUusWyFvyVUGCltBsFmJB2W/dHZCHWswdYLwJ/XiulnrNr323zcQeodthDuAHTgmm4aEqCH1zsrBHYLIISheyyqD9Nnp1FK+e0TSgtpX5ZxrBBtNe4PItP4w8Q07oBN+a2mD4a9erPzDN4bzY1iy5BiS742imV2ynT4l8h9hQvz+Pz+COU/pGCdyrkgm/Qt3ddw/5Cms7CLXsSy50k/dJDT8037QTcuq1kWZ6r1y/Ic6bkHdD5is9fDvCf7SZA/m44ZLfmg+QcM0vugvjmxx3fwLsTFmpRwlwdE95zq/LSYwxqn0q5ANgDPUT7GXsm5PLB3mwcl7ZNygPFaqA+NvL6SOo93NP4bFDF9sfh+LThtgxvkF80fyxxy/Ac7U9i/RcYNWrd)。 31 | 32 | 使用 `name` 有很多优点: 33 | 34 | - 没有硬编码的 URL。 35 | - `params` 的自动编码/解码。 36 | - 防止你在 URL 中出现打字错误。 37 | - 绕过路径排序,例如展示一个匹配相同路径但排序较低的路由。 38 | 39 | 所有路由的命名**都必须是唯一的**。如果为多条路由添加相同的命名,路由器只会保留最后那一条。你可以在[动态路由](../advanced/dynamic-routing.md#Removing-routes)章节了解更多。 40 | 41 | Vue Router 有很多其他部分可以传入网址,例如 `router.push()` 和 `router.replace()` 方法。我们将在[编程式导航](./navigation.md)指南中详细介绍这些方法。就像 `to` 属性一样,这些方法也支持通过 `name` 传入网址: 42 | 43 | ```js 44 | router.push({ name: 'user', params: { username: 'erina' } }) 45 | ``` 46 | -------------------------------------------------------------------------------- /packages/docs/zh/guide/essentials/redirect-and-alias.md: -------------------------------------------------------------------------------- 1 | # 重定向和别名 2 | 3 | 7 | 8 | ## 重定向 9 | 10 | 重定向也是通过 `routes` 配置来完成,下面例子是从 `/home` 重定向到 `/`: 11 | 12 | ```js 13 | const routes = [{ path: '/home', redirect: '/' }] 14 | ``` 15 | 16 | 重定向的目标也可以是一个命名的路由: 17 | 18 | ```js 19 | const routes = [{ path: '/home', redirect: { name: 'homepage' } }] 20 | ``` 21 | 22 | 甚至是一个方法,动态返回重定向目标: 23 | 24 | ```js 25 | const routes = [ 26 | { 27 | // /search/screens -> /search?q=screens 28 | path: '/search/:searchText', 29 | redirect: to => { 30 | // 方法接收目标路由作为参数 31 | // return 重定向的字符串路径/路径对象 32 | return { path: '/search', query: { q: to.params.searchText } } 33 | }, 34 | }, 35 | { 36 | path: '/search', 37 | // ... 38 | }, 39 | ] 40 | ``` 41 | 42 | 请注意,**[导航守卫](../advanced/navigation-guards.md)并没有应用在跳转路由上,而仅仅应用在其目标上**。在上面的例子中,在 `/home` 路由中添加 `beforeEnter` 守卫不会有任何效果。 43 | 44 | 在写 `redirect` 的时候,可以省略 `component` 配置,因为它从来没有被直接访问过,所以没有组件要渲染。唯一的例外是[嵌套路由](./nested-routes.md):如果一个路由记录有 `children` 和 `redirect` 属性,它也应该有 `component` 属性。 45 | 46 | ### 相对重定向 47 | 48 | 也可以重定向到相对位置: 49 | 50 | ```js 51 | const routes = [ 52 | { 53 | // 将总是把/users/123/posts重定向到/users/123/profile。 54 | path: '/users/:id/posts', 55 | redirect: to => { 56 | // 该函数接收目标路由作为参数 57 | return to.path.replace(/posts$/, 'profile') 58 | }, 59 | }, 60 | ] 61 | ``` 62 | 63 | ## 别名 64 | 65 | 重定向是指当用户访问 `/home` 时,URL 会被 `/` 替换,然后匹配成 `/`。那么什么是别名呢? 66 | 67 | **将 `/` 别名为 `/home`,意味着当用户访问 `/home` 时,URL 仍然是 `/home`,但会被匹配为用户正在访问 `/`。** 68 | 69 | 上面对应的路由配置为: 70 | 71 | ```js 72 | const routes = [{ path: '/', component: Homepage, alias: '/home' }] 73 | ``` 74 | 75 | 通过别名,你可以自由地将 UI 结构映射到一个任意的 URL,而不受配置的嵌套结构的限制。使别名以 `/` 开头,以使嵌套路径中的路径成为绝对路径。你甚至可以将两者结合起来,用一个数组提供多个别名: 76 | 77 | ```js 78 | const routes = [ 79 | { 80 | path: '/users', 81 | component: UsersLayout, 82 | children: [ 83 | // 为这 3 个 URL 呈现 UserList 84 | // - /users 85 | // - /users/list 86 | // - /people 87 | { path: '', component: UserList, alias: ['/people', 'list'] }, 88 | ], 89 | }, 90 | ] 91 | ``` 92 | 93 | 如果你的路由有参数,请确保在任何绝对别名中包含它们: 94 | 95 | ```js 96 | const routes = [ 97 | { 98 | path: '/users/:id', 99 | component: UsersByIdLayout, 100 | children: [ 101 | // 为这 3 个 URL 呈现 UserDetails 102 | // - /users/24 103 | // - /users/24/profile 104 | // - /24 105 | { path: 'profile', component: UserDetails, alias: ['/:id', ''] }, 106 | ], 107 | }, 108 | ] 109 | ``` 110 | 111 | **关于 SEO 的注意事项**: 使用别名时,一定要[定义规范链接](https://support.google.com/webmasters/answer/139066?hl=en). 112 | -------------------------------------------------------------------------------- /packages/docs/zh/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | title: Vue Router 5 | titleTemplate: Vue.js 的官方路由 6 | 7 | hero: 8 | name: Vue Router 9 | text: Vue.js 的官方路由 10 | tagline: 为 Vue.js 提供富有表现力、可配置的、方便的路由 11 | image: 12 | src: /logo.svg 13 | alt: Vue Router 14 | actions: 15 | - theme: brand 16 | text: 入门 → 17 | link: /zh/introduction 18 | - theme: cta vueschool 19 | text: 免费视频课程 20 | link: https://vueschool.io/courses/vue-router-4-for-everyone?friend=vuerouter&utm_source=vuerouter&utm_medium=link&utm_campaign=homepage 21 | - theme: cta vue-mastery 22 | text: Get the Vue Router Cheat Sheet 23 | link: https://www.vuemastery.com/vue-router?coupon=ROUTER-DOCS&via=eduardo 24 | 25 | features: 26 | - title: 🛣 富有表现力的路由语法 27 | details: 用直观且强大的语法来定义静态或动态路由。 28 | - title: 🛑 细致的导航控制 29 | details: 可拦截任何导航并更精确地控制其结果。 30 | - title: 🧱 基于组件的配置方法 31 | details: 将每条路由映射到应该显示的组件上。 32 | - title: 🔌 支持历史模式 33 | details: 有 HTML5、hash 或记忆历史模式可供选择。 34 | - title: 🎚 支持滚动控制 35 | details: 可精确控制每个页面的滚动位置。 36 | - title: 🌐 支持自动编码 37 | details: 可直接在代码中使用 unicode 字符(你好)。 38 | --- 39 | 40 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /packages/docs/zh/installation.md: -------------------------------------------------------------------------------- 1 | # 安装 2 | 3 | 4 | 5 | ## 包管理器 6 | 7 | 对于一个现有的使用 JavaScript 包管理器的项目,你可以从 npm registry 中安装 Vue Router: 8 | 9 | ::: code-group 10 | 11 | ```bash [npm] 12 | npm install vue-router@4 13 | ``` 14 | 15 | ```bash [yarn] 16 | yarn add vue-router@4 17 | ``` 18 | 19 | ```bash [pnpm] 20 | pnpm add vue-router@4 21 | ``` 22 | 23 | ::: 24 | 25 | 如果你打算启动一个新项目,你可能会发现使用 [create-vue](https://github.com/vuejs/create-vue) 这个脚手架工具更容易,它能创建一个基于 Vite 的项目,并包含加入 Vue Router 的选项: 26 | 27 | ::: code-group 28 | 29 | ```bash [npm] 30 | npm create vue@latest 31 | ``` 32 | 33 | ```bash [yarn] 34 | yarn create vue 35 | ``` 36 | 37 | ```bash [pnpm] 38 | pnpm create vue 39 | ``` 40 | 41 | ::: 42 | 43 | 你需要回答一些关于你想创建的项目类型的问题。如果您选择安装 Vue Router,示例应用还将演示 Vue Router 的一些核心特性。 44 | 45 | 使用包管理器的项目通常会使用 ES 模块来访问 Vue Router,例如 `import { createRouter } from 'vue-router'`。 46 | 47 | ## 直接下载 / CDN 48 | 49 | [https://unpkg.com/vue-router@4](https://unpkg.com/vue-router@4) 50 | 51 | 52 | 53 | [Unpkg.com](https://unpkg.com) 提供了基于 npm 的 CDN 链接。上述链接将始终指向 npm 上的最新版本。 你也可以通过像 `https://unpkg.com/vue-router@4.0.15/dist/vue-router.global.js` 这样的 URL 来使用特定的版本或 Tag。 54 | 55 | 56 | 57 | 这将把 Vue Router 暴露在一个全局的 `VueRouter` 对象上,例如 `VueRouter.createRouter(...)`。 58 | 59 | -------------------------------------------------------------------------------- /packages/docs/zh/introduction.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | 7 | 8 | Vue Router 是 [Vue.js](https://cn.vuejs.org/) 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。功能包括: 9 | 10 | - 嵌套路由映射 11 | - 动态路由选择 12 | - 模块化、基于组件的路由配置 13 | - 路由参数、查询、通配符 14 | - 展示由 Vue.js 的过渡系统提供的过渡效果 15 | - 细致的导航控制 16 | - 自动激活 CSS 类的链接 17 | - HTML5 history 模式或 hash 模式 18 | - 可定制的滚动行为 19 | - URL 的正确编码 20 | 21 | [入门](./guide/)或使用 [playground](https://github.com/vuejs/router/tree/main/packages/playground) (详见[`README.md`](https://github.com/vuejs/router)来运行它们)。 22 | 23 | 24 | 25 | 28 | -------------------------------------------------------------------------------- /packages/playground/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare module '*.vue' { 5 | import { Component } from 'vue' 6 | var component: Component 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /packages/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Testing History HTML5 8 | 9 | 65 | 66 | 67 |
68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /packages/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue/router-playground", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "play": "vite", 7 | "build": "vite build", 8 | "types": "vue-tsc --noEmit", 9 | "preview": "vite preview --port 4173" 10 | }, 11 | "dependencies": { 12 | "vue": "~3.5.13" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^20.17.31", 16 | "@vitejs/plugin-vue": "^5.2.3", 17 | "@vue/compiler-sfc": "~3.5.13", 18 | "@vue/tsconfig": "^0.6.0", 19 | "vite": "^5.4.18", 20 | "vue-router": "workspace:*", 21 | "vue-tsc": "^2.2.10" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/playground/src/AppLink.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 64 | -------------------------------------------------------------------------------- /packages/playground/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export let delay = (t: number = 100) => 2 | new Promise(resolve => setTimeout(resolve, t)) 3 | 4 | export async function getData() { 5 | await delay(500) 6 | 7 | return { 8 | message: 'Hello', 9 | time: Date.now(), 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/playground/src/main.ts: -------------------------------------------------------------------------------- 1 | // necessary for webpack 2 | import { createApp } from 'vue' 3 | import type { ComponentPublicInstance } from 'vue' 4 | import { router, routerHistory } from './router' 5 | import { globalState } from './store' 6 | import App from './App.vue' 7 | import { useRoute, type ParamValue, type RouteRecordInfo } from 'vue-router' 8 | 9 | declare global { 10 | interface Window { 11 | // h: HTML5History 12 | h: typeof routerHistory 13 | r: typeof router 14 | vm: ComponentPublicInstance 15 | } 16 | } 17 | 18 | // for testing purposes 19 | window.h = routerHistory 20 | window.r = router 21 | 22 | const app = createApp(App) 23 | app.mixin({ 24 | beforeRouteEnter() { 25 | console.log('mixin enter') 26 | }, 27 | }) 28 | 29 | app.provide('state', globalState) 30 | app.use(router) 31 | 32 | window.vm = app.mount('#app') 33 | 34 | export interface RouteNamedMap { 35 | home: RouteRecordInfo< 36 | 'home', 37 | '/', 38 | Record, 39 | Record, 40 | never 41 | > 42 | '/[name]': RouteRecordInfo< 43 | '/[name]', 44 | '/:name', 45 | { name: ParamValue }, 46 | { name: ParamValue }, 47 | '/[name]/edit' 48 | > 49 | '/[name]/edit': RouteRecordInfo< 50 | '/[name]/edit', 51 | '/:name/edit', 52 | { name: ParamValue }, 53 | { name: ParamValue }, 54 | never 55 | > 56 | '/[...path]': RouteRecordInfo< 57 | '/[...path]', 58 | '/:path(.*)', 59 | { path: ParamValue }, 60 | { path: ParamValue }, 61 | never 62 | > 63 | } 64 | 65 | declare module 'vue-router' { 66 | interface TypesConfig { 67 | RouteNamedMap: RouteNamedMap 68 | } 69 | } 70 | 71 | function _ok() { 72 | const r = useRoute() 73 | 74 | if (r.name === '/[name]') { 75 | r.params.name.toUpperCase() 76 | // @ts-expect-error: Not existing route 77 | } else if (r.name === 'nope') { 78 | console.log('nope') 79 | } 80 | 81 | router.push({ 82 | name: '/[name]', 83 | params: { name: 'hey' }, 84 | }) 85 | 86 | router 87 | .resolve({ name: '/[name]', params: { name: 2 } }) 88 | .params.name.toUpperCase() 89 | } 90 | -------------------------------------------------------------------------------- /packages/playground/src/scrollWaiter.ts: -------------------------------------------------------------------------------- 1 | class ScrollQueue { 2 | private resolve: ((value?: any) => void) | null = null 3 | private promise: Promise | null = null 4 | 5 | add() { 6 | this.promise = new Promise(resolve => { 7 | this.resolve = resolve 8 | }) 9 | } 10 | 11 | flush() { 12 | this.resolve && this.resolve() 13 | this.resolve = null 14 | this.promise = null 15 | } 16 | 17 | async wait() { 18 | await this.promise 19 | } 20 | } 21 | 22 | export const scrollWaiter = new ScrollQueue() 23 | -------------------------------------------------------------------------------- /packages/playground/src/store.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue' 2 | 3 | export const globalState = reactive({ 4 | cancelNextNavigation: false, 5 | }) 6 | -------------------------------------------------------------------------------- /packages/playground/src/views/ComponentWithData.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 39 | -------------------------------------------------------------------------------- /packages/playground/src/views/Dynamic.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /packages/playground/src/views/Generic.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /packages/playground/src/views/GuardedWithLeave.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | -------------------------------------------------------------------------------- /packages/playground/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 41 | -------------------------------------------------------------------------------- /packages/playground/src/views/LongView.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 28 | -------------------------------------------------------------------------------- /packages/playground/src/views/Nested.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 78 | -------------------------------------------------------------------------------- /packages/playground/src/views/NestedWithId.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 37 | -------------------------------------------------------------------------------- /packages/playground/src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /packages/playground/src/views/RepeatedParams.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 40 | -------------------------------------------------------------------------------- /packages/playground/src/views/User.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /packages/playground/tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*" 7 | ], 8 | "compilerOptions": { 9 | "composite": true, 10 | "types": [ 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": [ 4 | "env.d.ts", 5 | "src/**/*.ts", 6 | "src/**/*.vue" 7 | ], 8 | "exclude": [ 9 | "**/node_modules", 10 | "**/.*/" 11 | ], 12 | "compilerOptions": { 13 | "allowJs": false, 14 | "baseUrl": ".", 15 | "paths": { 16 | "@/*": [ 17 | "./src/*" 18 | ], 19 | "vue-router": [ 20 | "../router/src" 21 | ] 22 | } 23 | }, 24 | "references": [ 25 | { 26 | "path": "./tsconfig.config.json" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /packages/playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | import { defineConfig } from 'vite' 3 | import Vue from '@vitejs/plugin-vue' 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [Vue()], 8 | resolve: { 9 | alias: { 10 | '@': fileURLToPath(new URL('./src', import.meta.url)), 11 | 'vue-router': fileURLToPath(new URL('../router/src', import.meta.url)), 12 | }, 13 | }, 14 | define: { 15 | __DEV__: JSON.stringify(!process.env.prod), 16 | __BROWSER__: 'true', 17 | 'process.env': { 18 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 19 | }, 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /packages/router/.gitignore: -------------------------------------------------------------------------------- 1 | e2e/reports 2 | e2e/screenshots 3 | old-e2e/reports 4 | old-e2e/screenshots 5 | tests_output 6 | logs 7 | # copied from root during release 8 | README.md 9 | -------------------------------------------------------------------------------- /packages/router/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | This is the list of versions of Vue Router which are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ---------- | ------------------ | 10 | | 4.0.x | :white_check_mark: | 11 | | 3.5.x | :white_check_mark: | 12 | | < 3.5.x | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | To report a vulnerability please send an email with the details to security@vuejs.org. 17 | This will help us to assess the risk and start the necessary steps. More information 18 | can be found in [Vue Documentation](https://vuejs.org/v2/guide/security.html#Reporting-Vulnerabilities). 19 | 20 | Thanks for helping to keep Vue Router secure. 21 | -------------------------------------------------------------------------------- /packages/router/__tests__/__snapshots__/RouterLink.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`RouterLink > v-slot > provides information on v-slot 1`] = `" route: {"href":"/home","fullPath":"/home","path":"/home","params":{},"meta":{},"query":{},"hash":"","matched":[{}],"name":"home"} href: "/home" isActive: "true" isExactActive: "true" "`; 4 | -------------------------------------------------------------------------------- /packages/router/__tests__/__snapshots__/RouterView.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`RouterView > displays deeply nested views 1`] = ` 4 | "
5 |

Nested

6 |
7 |

Nested

8 |
Foo
9 |
10 |
" 11 | `; 12 | 13 | exports[`RouterView > displays nested views 1`] = ` 14 | "
15 |

Nested

16 |
Foo
17 |
" 18 | `; 19 | 20 | exports[`RouterView > v-slot > passes a Component and route 1`] = ` 21 | "home 22 |
Home
" 23 | `; 24 | 25 | exports[`RouterView > warnings > does not warn RouterView is wrapped 1`] = ` 26 | "
27 |
Home
28 |
" 29 | `; 30 | -------------------------------------------------------------------------------- /packages/router/__tests__/createRouter.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'vitest' 2 | import { createRouter, createWebHistory } from '../src' 3 | import { defineComponent, h } from 'vue' 4 | 5 | describe('createRouter', () => { 6 | const component = defineComponent({}) 7 | 8 | const WithProps = defineComponent({ 9 | props: { 10 | id: { 11 | type: String, 12 | required: true, 13 | }, 14 | }, 15 | }) 16 | 17 | const Foo = defineComponent({ 18 | props: { 19 | test: String, 20 | }, 21 | setup() { 22 | return { 23 | title: 'homepage', 24 | } 25 | }, 26 | render() { 27 | return h('div', `${this.title}: ${this.test}`) 28 | }, 29 | }) 30 | 31 | it('works', () => { 32 | createRouter({ 33 | history: createWebHistory(), 34 | routes: [ 35 | { path: '/', component }, 36 | { path: '/foo', component: Foo }, 37 | { path: '/', component: WithProps }, 38 | ], 39 | parseQuery: search => ({}), 40 | stringifyQuery: query => '', 41 | strict: true, 42 | end: true, 43 | sensitive: true, 44 | scrollBehavior(to, from, savedPosition) {}, 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/router/__tests__/guards/afterEach.spec.ts: -------------------------------------------------------------------------------- 1 | import { createDom, newRouter as createRouter } from '../utils' 2 | import { RouteRecordRaw } from '../../src/types' 3 | import { vi, describe, expect, it, beforeAll } from 'vitest' 4 | 5 | const Home = { template: `
Home
` } 6 | const Foo = { template: `
Foo
` } 7 | const Nested = { template: `
Nested
` } 8 | 9 | const routes: RouteRecordRaw[] = [ 10 | { path: '/', component: Home }, 11 | { path: '/foo', component: Foo }, 12 | { 13 | path: '/nested', 14 | component: Nested, 15 | children: [ 16 | { path: '', name: 'nested-default', component: Foo }, 17 | { path: 'home', name: 'nested-home', component: Home }, 18 | ], 19 | }, 20 | ] 21 | 22 | describe('router.afterEach', () => { 23 | beforeAll(() => { 24 | createDom() 25 | }) 26 | 27 | it('calls afterEach guards on push', async () => { 28 | const spy = vi.fn() 29 | const router = createRouter({ routes }) 30 | router.afterEach(spy) 31 | await router.push('/foo') 32 | expect(spy).toHaveBeenCalledTimes(1) 33 | expect(spy).toHaveBeenCalledWith( 34 | expect.objectContaining({ fullPath: '/foo' }), 35 | expect.objectContaining({ fullPath: '/' }), 36 | undefined 37 | ) 38 | }) 39 | 40 | it('can be removed', async () => { 41 | const spy = vi.fn() 42 | const router = createRouter({ routes }) 43 | const remove = router.afterEach(spy) 44 | remove() 45 | await router.push('/foo') 46 | expect(spy).not.toHaveBeenCalled() 47 | }) 48 | 49 | it('calls afterEach guards on multiple push', async () => { 50 | const spy = vi.fn() 51 | const router = createRouter({ routes }) 52 | await router.push('/nested') 53 | router.afterEach(spy) 54 | await router.push('/nested/home') 55 | expect(spy).toHaveBeenCalledTimes(1) 56 | expect(spy).toHaveBeenLastCalledWith( 57 | expect.objectContaining({ name: 'nested-home' }), 58 | expect.objectContaining({ name: 'nested-default' }), 59 | undefined 60 | ) 61 | await router.push('/nested') 62 | expect(spy).toHaveBeenLastCalledWith( 63 | expect.objectContaining({ name: 'nested-default' }), 64 | expect.objectContaining({ name: 'nested-home' }), 65 | undefined 66 | ) 67 | expect(spy).toHaveBeenCalledTimes(2) 68 | }) 69 | 70 | it('removing an afterEach guard within one does not affect others', async () => { 71 | const spy1 = vi.fn() 72 | const spy2 = vi.fn() 73 | const router = createRouter({ routes }) 74 | router.afterEach(spy1) 75 | const remove = router.afterEach(spy2) 76 | spy1.mockImplementationOnce(remove) 77 | await router.push('/foo') 78 | expect(spy1).toHaveBeenCalledTimes(1) 79 | expect(spy2).toHaveBeenCalledTimes(1) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /packages/router/__tests__/guards/beforeResolve.spec.ts: -------------------------------------------------------------------------------- 1 | import { createDom, noGuard, newRouter as createRouter } from '../utils' 2 | import { RouteRecordRaw } from '../../src/types' 3 | import { vi, describe, expect, it, beforeAll } from 'vitest' 4 | 5 | const Home = { template: `
Home
` } 6 | const Foo = { template: `
Foo
` } 7 | 8 | const routes: RouteRecordRaw[] = [ 9 | { path: '/', component: Home }, 10 | { path: '/foo', component: Foo }, 11 | ] 12 | 13 | describe('router.beforeEach', () => { 14 | beforeAll(() => { 15 | createDom() 16 | }) 17 | 18 | it('calls beforeEach guards on navigation', async () => { 19 | const spy = vi.fn() 20 | const router = createRouter({ routes }) 21 | router.beforeResolve(spy) 22 | spy.mockImplementationOnce(noGuard) 23 | await router.push('/foo') 24 | expect(spy).toHaveBeenCalledTimes(1) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/router/__tests__/guards/beforeRouteEnterCallback.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment jsdom 3 | */ 4 | import { defineComponent, h } from 'vue' 5 | import { mount } from '@vue/test-utils' 6 | import { createRouter, createMemoryHistory, RouterOptions } from '../../src' 7 | import { vi, describe, expect, it, beforeEach } from 'vitest' 8 | 9 | const nextCallbacks = { 10 | Default: vi.fn(), 11 | Other: vi.fn(), 12 | } 13 | const Default = defineComponent({ 14 | beforeRouteEnter(to, from, next) { 15 | next(nextCallbacks.Default) 16 | }, 17 | name: 'Default', 18 | setup() { 19 | return () => h('div', 'Default content') 20 | }, 21 | }) 22 | 23 | const Other = defineComponent({ 24 | beforeRouteEnter(to, from, next) { 25 | next(nextCallbacks.Other) 26 | }, 27 | name: 'Other', 28 | setup() { 29 | return () => h('div', 'Other content') 30 | }, 31 | }) 32 | 33 | const Third = defineComponent({ 34 | name: 'Third', 35 | setup() { 36 | return () => h('div', 'Third content') 37 | }, 38 | }) 39 | 40 | beforeEach(() => { 41 | for (const key in nextCallbacks) { 42 | nextCallbacks[key as keyof typeof nextCallbacks].mockClear() 43 | } 44 | }) 45 | 46 | describe('beforeRouteEnter next callback', () => { 47 | async function factory(options: Partial) { 48 | const history = createMemoryHistory() 49 | const router = createRouter({ 50 | history, 51 | routes: [], 52 | ...options, 53 | }) 54 | 55 | const wrapper = mount( 56 | { 57 | template: ` 58 |
59 | 60 | 61 |
62 | `, 63 | }, 64 | { 65 | global: { 66 | plugins: [router], 67 | }, 68 | } 69 | ) 70 | 71 | return { wrapper, router } 72 | } 73 | 74 | it('calls each beforeRouteEnter callback once', async () => { 75 | const { router } = await factory({ 76 | routes: [ 77 | { 78 | path: '/:p(.*)', 79 | components: { 80 | default: Default, 81 | other: Other, 82 | third: Third, 83 | }, 84 | }, 85 | ], 86 | }) 87 | 88 | await router.isReady() 89 | 90 | expect(nextCallbacks.Default).toHaveBeenCalledTimes(1) 91 | expect(nextCallbacks.Other).toHaveBeenCalledTimes(1) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /packages/router/__tests__/guards/onBeforeRouteLeave.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment jsdom 3 | */ 4 | import { 5 | createRouter, 6 | createMemoryHistory, 7 | onBeforeRouteLeave, 8 | } from '../../src' 9 | import { createApp, defineComponent } from 'vue' 10 | import { vi, describe, expect, it } from 'vitest' 11 | 12 | const component = { 13 | template: '
Generic
', 14 | } 15 | 16 | describe('onBeforeRouteLeave', () => { 17 | it('removes guards when leaving the route', async () => { 18 | const spy = vi.fn() 19 | const WithLeave = defineComponent({ 20 | template: `text`, 21 | setup() { 22 | onBeforeRouteLeave(spy) 23 | }, 24 | }) 25 | 26 | const router = createRouter({ 27 | history: createMemoryHistory(), 28 | routes: [ 29 | { path: '/', component }, 30 | { path: '/leave', component: WithLeave as any }, 31 | ], 32 | }) 33 | const app = createApp({ 34 | template: ` 35 | 36 | `, 37 | }) 38 | app.use(router) 39 | const rootEl = document.createElement('div') 40 | document.body.appendChild(rootEl) 41 | app.mount(rootEl) 42 | 43 | await router.isReady() 44 | await router.push('/leave') 45 | await router.push('/') 46 | expect(spy).toHaveBeenCalledTimes(1) 47 | await router.push('/leave') 48 | await router.push('/') 49 | expect(spy).toHaveBeenCalledTimes(2) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/router/__tests__/hash-manual-navigation.spec.ts: -------------------------------------------------------------------------------- 1 | import { createMemoryHistory, createRouter, RouterHistory } from '../src' 2 | import { tick } from './utils' 3 | import { describe, expect, it } from 'vitest' 4 | 5 | const component = {} 6 | 7 | interface RouterHistory_Test extends RouterHistory { 8 | changeURL(url: string): void 9 | } 10 | 11 | describe('hash history edge cases', () => { 12 | it('correctly sets the url when it is manually changed but aborted with a redirect in guard', async () => { 13 | const history = createMemoryHistory() as RouterHistory_Test 14 | const router = createRouter({ 15 | history, 16 | routes: [ 17 | { path: '/', component }, 18 | { path: '/foo', component }, 19 | ], 20 | }) 21 | 22 | await router.push('/foo?step=1') 23 | await router.push('/foo?step=2') 24 | await router.push('/foo?step=3') 25 | router.back() 26 | await tick() // wait for router listener on history 27 | expect(router.currentRoute.value.fullPath).toBe('/foo?step=2') 28 | 29 | // force a redirect next time 30 | const removeListener = router.beforeEach(to => { 31 | if (to.path === '/') { 32 | removeListener() 33 | return '/foo?step=2' 34 | } 35 | return 36 | }) 37 | 38 | // const spy = vi.spyOn(history, 'go') 39 | 40 | history.changeURL('/') 41 | await tick() 42 | expect(router.currentRoute.value.fullPath).toBe('/foo?step=2') 43 | expect(history.location).toBe('/foo?step=2') 44 | // expect(spy).toHaveBeenCalledTimes(1) 45 | // expect(spy).toHaveBeenCalledWith(-1) 46 | }) 47 | 48 | it('correctly sets the url when it is manually changed but aborted with guard', async () => { 49 | const history = createMemoryHistory() as RouterHistory_Test 50 | const router = createRouter({ 51 | history, 52 | routes: [ 53 | { path: '/', component }, 54 | { path: '/foo', component }, 55 | ], 56 | }) 57 | 58 | await router.push('/foo?step=1') 59 | await router.push('/foo?step=2') 60 | await router.push('/foo?step=3') 61 | router.back() 62 | await tick() // wait for router listener on history 63 | expect(router.currentRoute.value.fullPath).toBe('/foo?step=2') 64 | 65 | // force a redirect next time 66 | const removeListener = router.beforeEach(to => { 67 | if (to.path === '/') { 68 | removeListener() 69 | return false 70 | } 71 | 72 | return 73 | }) 74 | 75 | // const spy = vi.spyOn(history, 'go') 76 | 77 | history.changeURL('/') 78 | await tick() 79 | expect(router.currentRoute.value.fullPath).toBe('/foo?step=2') 80 | expect(history.location).toBe('/foo?step=2') 81 | // expect(spy).toHaveBeenCalledTimes(1) 82 | // expect(spy).toHaveBeenCalledWith(-1) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /packages/router/__tests__/initialNavigation.spec.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom' 2 | import { createRouter, createWebHistory } from '../src' 3 | import { createDom, components, nextNavigation } from './utils' 4 | import { RouteRecordRaw } from '../src/types' 5 | import { describe, expect, it, beforeAll, vi, afterAll } from 'vitest' 6 | 7 | // override the value of isBrowser because the variable is created before JSDOM 8 | // is created 9 | vi.mock('../src/utils/env', () => ({ 10 | isBrowser: true, 11 | })) 12 | 13 | // generic component because we are not displaying anything so it doesn't matter 14 | const component = components.Home 15 | 16 | const routes: RouteRecordRaw[] = [ 17 | { path: '/home', redirect: '/' }, 18 | { path: '/', component }, 19 | { 20 | path: '/home-before', 21 | component, 22 | beforeEnter: (to, from, next) => { 23 | next('/') 24 | }, 25 | }, 26 | { path: '/bar', component }, 27 | { path: '/foo', component, name: 'Foo' }, 28 | { path: '/to-foo', redirect: '/foo' }, 29 | ] 30 | 31 | describe('Initial Navigation', () => { 32 | let dom: JSDOM 33 | function newRouter( 34 | url: string, 35 | options: Partial[0]> = {} 36 | ) { 37 | dom.reconfigure({ url: 'https://example.com' + url }) 38 | const history = options.history || createWebHistory() 39 | const router = createRouter({ history, routes, ...options }) 40 | 41 | return { history, router } 42 | } 43 | 44 | beforeAll(() => { 45 | dom = createDom() 46 | }) 47 | 48 | afterAll(() => { 49 | dom.window.close() 50 | }) 51 | 52 | it('handles initial navigation with redirect', async () => { 53 | const { history, router } = newRouter('/home') 54 | expect(history.location).toBe('/home') 55 | // this is done automatically on install but there is none here 56 | await router.push(history.location) 57 | expect(router.currentRoute.value).toMatchObject({ path: '/' }) 58 | await router.push('/foo') 59 | expect(router.currentRoute.value).toMatchObject({ path: '/foo' }) 60 | history.go(-1) 61 | await nextNavigation(router) 62 | expect(router.currentRoute.value).toMatchObject({ path: '/' }) 63 | }) 64 | 65 | it('handles initial navigation with beforeEnter', async () => { 66 | const { history, router } = newRouter('/home-before') 67 | expect(history.location).toBe('/home-before') 68 | // this is done automatically on mount but there is no mount here 69 | await router.push(history.location) 70 | expect(router.currentRoute.value).toMatchObject({ path: '/' }) 71 | await router.push('/foo') 72 | expect(router.currentRoute.value).toMatchObject({ path: '/foo' }) 73 | history.go(-1) 74 | await nextNavigation(router) 75 | expect(router.currentRoute.value).toMatchObject({ path: '/' }) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /packages/router/__tests__/matcher/__snapshots__/resolve.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`RouterMatcher.resolve > LocationAsName > throws if the named route does not exists 1`] = ` 4 | [Error: No match for 5 | {"name":"Home"}] 6 | `; 7 | 8 | exports[`RouterMatcher.resolve > LocationAsRelative > throws if the current named route does not exists 1`] = ` 9 | [Error: No match for 10 | {"params":{"a":"foo"}} 11 | while being at 12 | {"name":"home","params":{},"path":"/","meta":{}}] 13 | `; 14 | -------------------------------------------------------------------------------- /packages/router/__tests__/matcher/records.spec.ts: -------------------------------------------------------------------------------- 1 | import { normalizeRouteRecord } from '../../src/matcher' 2 | import { vi, describe, expect, it } from 'vitest' 3 | 4 | describe('normalizeRouteRecord', () => { 5 | it('transforms a single view into multiple views', () => { 6 | const record = normalizeRouteRecord({ 7 | path: '/home', 8 | component: {}, 9 | }) 10 | expect(record).toMatchObject({ 11 | beforeEnter: undefined, 12 | children: [], 13 | aliasOf: undefined, 14 | components: { default: {} }, 15 | leaveGuards: expect.any(Set), 16 | updateGuards: expect.any(Set), 17 | instances: {}, 18 | meta: {}, 19 | name: undefined, 20 | path: '/home', 21 | props: { default: false }, 22 | }) 23 | }) 24 | 25 | it('keeps original values in single view', () => { 26 | const beforeEnter = vi.fn() 27 | const record = normalizeRouteRecord({ 28 | path: '/home', 29 | beforeEnter, 30 | children: [{ path: '/child' } as any], 31 | meta: { foo: true }, 32 | name: 'name', 33 | component: {}, 34 | }) 35 | expect(record).toMatchObject({ 36 | beforeEnter, 37 | children: [{ path: '/child' }], 38 | components: { default: {} }, 39 | leaveGuards: expect.any(Set), 40 | updateGuards: expect.any(Set), 41 | instances: {}, 42 | meta: { foo: true }, 43 | name: 'name', 44 | path: '/home', 45 | props: { default: false }, 46 | }) 47 | }) 48 | 49 | it('keeps original values in redirect', () => { 50 | const record = normalizeRouteRecord({ 51 | path: '/redirect', 52 | redirect: '/home', 53 | meta: { foo: true }, 54 | name: 'name', 55 | }) 56 | 57 | expect(record).toMatchObject({ 58 | aliasOf: undefined, 59 | components: {}, 60 | meta: { foo: true }, 61 | name: 'name', 62 | path: '/redirect', 63 | redirect: '/home', 64 | }) 65 | }) 66 | 67 | it('keeps original values in multiple views', () => { 68 | const beforeEnter = vi.fn() 69 | const record = normalizeRouteRecord({ 70 | path: '/home', 71 | beforeEnter, 72 | children: [{ path: '/child' } as any], 73 | meta: { foo: true }, 74 | name: 'name', 75 | components: { one: {} }, 76 | }) 77 | expect(record).toMatchObject({ 78 | beforeEnter, 79 | children: [{ path: '/child' }], 80 | components: { one: {} }, 81 | leaveGuards: expect.any(Set), 82 | updateGuards: expect.any(Set), 83 | instances: {}, 84 | meta: { foo: true }, 85 | name: 'name', 86 | path: '/home', 87 | props: { one: false }, 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /packages/router/__tests__/mount.ts: -------------------------------------------------------------------------------- 1 | import { nextTick, shallowRef, shallowReactive } from 'vue' 2 | import { RouteLocationNormalizedLoose } from './utils' 3 | import { 4 | routeLocationKey, 5 | routerViewLocationKey, 6 | } from '../src/injectionSymbols' 7 | import { RouteLocationNormalized } from '../src' 8 | 9 | export function createMockedRoute( 10 | initialValue: RouteLocationNormalizedLoose | RouteLocationNormalized 11 | ) { 12 | const routeRef = shallowRef< 13 | RouteLocationNormalized | RouteLocationNormalizedLoose 14 | >(initialValue) 15 | 16 | function set( 17 | newRoute: RouteLocationNormalizedLoose | RouteLocationNormalized 18 | ) { 19 | routeRef.value = newRoute 20 | return nextTick() 21 | } 22 | 23 | const route = {} as RouteLocationNormalizedLoose 24 | 25 | for (let key in initialValue) { 26 | Object.defineProperty(route, key, { 27 | enumerable: true, 28 | get: () => routeRef.value[key as keyof RouteLocationNormalizedLoose], 29 | }) 30 | } 31 | 32 | const value = shallowReactive(route) 33 | 34 | return { 35 | value, 36 | set, 37 | provides: { 38 | [routeLocationKey as symbol]: value, 39 | [routerViewLocationKey as symbol]: routeRef, 40 | }, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/router/__tests__/multipleApps.spec.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createMemoryHistory } from '../src' 2 | import { h } from 'vue' 3 | import { createDom } from './utils' 4 | import { vi, describe, expect, it, beforeAll } from 'vitest' 5 | 6 | const delay = (t: number) => new Promise(resolve => setTimeout(resolve, t)) 7 | 8 | function newRouter(options: Partial[0]> = {}) { 9 | const history = options.history || createMemoryHistory() 10 | const router = createRouter({ 11 | history, 12 | routes: [ 13 | { 14 | path: '/:pathMatch(.*)', 15 | component: { 16 | render: () => h('div', 'any route'), 17 | }, 18 | }, 19 | ], 20 | ...options, 21 | }) 22 | 23 | return { history, router } 24 | } 25 | 26 | describe('Multiple apps', () => { 27 | beforeAll(() => { 28 | createDom() 29 | const rootEl = document.createElement('div') 30 | rootEl.id = 'app' 31 | document.body.appendChild(rootEl) 32 | }) 33 | 34 | it('does not listen to url changes before being ready', async () => { 35 | const { router, history } = newRouter() 36 | 37 | const spy = vi.fn((to, from, next) => { 38 | next() 39 | }) 40 | router.beforeEach(spy) 41 | 42 | history.push('/foo') 43 | history.push('/bar') 44 | history.go(-1, true) 45 | 46 | await delay(5) 47 | expect(spy).not.toHaveBeenCalled() 48 | 49 | await router.push('/baz') 50 | 51 | history.go(-1, true) 52 | await delay(5) 53 | expect(spy).toHaveBeenCalledTimes(2) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /packages/router/__tests__/parseQuery.spec.ts: -------------------------------------------------------------------------------- 1 | import { parseQuery } from '../src/query' 2 | import { mockWarn } from './vitest-mock-warn' 3 | import { describe, expect, it } from 'vitest' 4 | 5 | describe('parseQuery', () => { 6 | mockWarn() 7 | 8 | it('works with leading ?', () => { 9 | expect(parseQuery('?foo=a')).toEqual({ 10 | foo: 'a', 11 | }) 12 | }) 13 | 14 | it('works without leading ?', () => { 15 | expect(parseQuery('foo=a')).toEqual({ 16 | foo: 'a', 17 | }) 18 | }) 19 | 20 | it('works with an empty string', () => { 21 | const emptyQuery = parseQuery('') 22 | expect(Object.keys(emptyQuery)).toHaveLength(0) 23 | expect(emptyQuery).toEqual({}) 24 | expect(parseQuery('?')).toEqual({}) 25 | }) 26 | 27 | it('decodes values in query', () => { 28 | expect(parseQuery('e=%25')).toEqual({ 29 | e: '%', 30 | }) 31 | }) 32 | 33 | it('parses empty string values', () => { 34 | expect(parseQuery('e=&c=a')).toEqual({ 35 | e: '', 36 | c: 'a', 37 | }) 38 | }) 39 | 40 | it('allows = inside values', () => { 41 | expect(parseQuery('e=c=a')).toEqual({ 42 | e: 'c=a', 43 | }) 44 | }) 45 | 46 | it('parses empty values as null', () => { 47 | expect(parseQuery('e&b&c=a')).toEqual({ 48 | e: null, 49 | b: null, 50 | c: 'a', 51 | }) 52 | }) 53 | 54 | it('parses empty values as null in arrays', () => { 55 | expect(parseQuery('e&e&e=a')).toEqual({ 56 | e: [null, null, 'a'], 57 | }) 58 | }) 59 | 60 | it('decodes array values in query', () => { 61 | expect(parseQuery('e=%25&e=%22')).toEqual({ 62 | e: ['%', '"'], 63 | }) 64 | expect(parseQuery('e=%25&e=a')).toEqual({ 65 | e: ['%', 'a'], 66 | }) 67 | }) 68 | 69 | it('decodes the + as space', () => { 70 | expect(parseQuery('a+b=c+d')).toEqual({ 71 | 'a b': 'c d', 72 | }) 73 | }) 74 | 75 | it('decodes the encoded + as +', () => { 76 | expect(parseQuery('a%2Bb=c%2Bd')).toEqual({ 77 | 'a+b': 'c+d', 78 | }) 79 | }) 80 | 81 | // this is for browsers like IE that allow invalid characters 82 | it('keep invalid values as is', () => { 83 | expect(parseQuery('e=%&e=%25')).toEqual({ 84 | e: ['%', '%'], 85 | }) 86 | 87 | expect('decoding "%"').toHaveBeenWarnedTimes(1) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /packages/router/__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | import { TextEncoder, TextDecoder } from 'util' 2 | 3 | global.TextEncoder = TextEncoder 4 | // @ts-expect-error: ok 5 | global.TextDecoder = TextDecoder 6 | -------------------------------------------------------------------------------- /packages/router/__tests__/ssr.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment node 3 | */ 4 | import { createRouter, createMemoryHistory } from '../src' 5 | import { createSSRApp, resolveComponent, Component } from 'vue' 6 | import { 7 | renderToString, 8 | ssrInterpolate, 9 | ssrRenderComponent, 10 | } from '@vue/server-renderer' 11 | import { describe, expect, it } from 'vitest' 12 | 13 | const delay = (t: number) => new Promise(resolve => setTimeout(resolve, t)) 14 | 15 | describe('SSR', () => { 16 | const Home = { 17 | ssrRender(ctx: any, push: any) { 18 | push('Home') 19 | }, 20 | } 21 | const Page = { 22 | ssrRender(ctx: any, push: any) { 23 | push(`${ssrInterpolate(ctx.$route.fullPath)}`) 24 | }, 25 | } 26 | 27 | const AsyncPage = async () => { 28 | await delay(10) 29 | return Page 30 | } 31 | 32 | it('works', async () => { 33 | const router = createRouter({ 34 | history: createMemoryHistory(), 35 | routes: [ 36 | { path: '/', component: Home }, 37 | { 38 | path: '/:id', 39 | component: Page, 40 | }, 41 | ], 42 | }) 43 | const App = { 44 | ssrRender(ctx: any, push: any, parent: any) { 45 | push( 46 | ssrRenderComponent( 47 | resolveComponent('router-view') as Component, 48 | null, 49 | null, 50 | parent 51 | ) 52 | ) 53 | }, 54 | } 55 | const app = createSSRApp(App) 56 | app.use(router) 57 | // const rootEl = document.createElement('div') 58 | // document.body.appendChild(rootEl) 59 | 60 | router.push('/hello') 61 | await router.isReady() 62 | 63 | const xxx = await renderToString(app) 64 | expect(xxx).toMatchInlineSnapshot(`"/hello"`) 65 | }) 66 | 67 | it('handles async components', async () => { 68 | const router = createRouter({ 69 | history: createMemoryHistory(), 70 | routes: [ 71 | { path: '/', component: Home }, 72 | { 73 | path: '/:id', 74 | component: AsyncPage, 75 | }, 76 | ], 77 | }) 78 | const App = { 79 | ssrRender(ctx: any, push: any, parent: any) { 80 | push( 81 | ssrRenderComponent( 82 | resolveComponent('router-view') as Component, 83 | null, 84 | null, 85 | parent 86 | ) 87 | ) 88 | }, 89 | } 90 | const app = createSSRApp(App) 91 | app.use(router) 92 | // const rootEl = document.createElement('div') 93 | // document.body.appendChild(rootEl) 94 | 95 | router.push('/hello') 96 | await router.isReady() 97 | 98 | const xxx = await renderToString(app) 99 | expect(xxx).toMatchInlineSnapshot(`"/hello"`) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /packages/router/__tests__/stringifyQuery.spec.ts: -------------------------------------------------------------------------------- 1 | import { stringifyQuery } from '../src/query' 2 | import { mockWarn } from './vitest-mock-warn' 3 | import { describe, expect, it } from 'vitest' 4 | 5 | describe('stringifyQuery', () => { 6 | mockWarn() 7 | 8 | it('stringifies multiple values', () => { 9 | expect(stringifyQuery({ e: 'a', b: 'c' })).toEqual('e=a&b=c') 10 | }) 11 | 12 | it('stringifies null values', () => { 13 | expect(stringifyQuery({ e: null })).toEqual('e') 14 | expect(stringifyQuery({ e: null, b: null })).toEqual('e&b') 15 | }) 16 | 17 | it('stringifies null values in arrays', () => { 18 | expect(stringifyQuery({ e: [null] })).toEqual('e') 19 | expect(stringifyQuery({ e: [null, 'c'] })).toEqual('e&e=c') 20 | }) 21 | 22 | it('stringifies numbers', () => { 23 | expect(stringifyQuery({ e: 2 })).toEqual('e=2') 24 | expect(stringifyQuery({ e: [2, 'b'] })).toEqual('e=2&e=b') 25 | }) 26 | 27 | it('ignores undefined values', () => { 28 | expect(stringifyQuery({ e: undefined })).toEqual('') 29 | expect(stringifyQuery({ e: undefined, b: 'a' })).toEqual('b=a') 30 | }) 31 | 32 | it('avoids trailing &', () => { 33 | expect(stringifyQuery({ a: 'a', b: undefined })).toEqual('a=a') 34 | expect(stringifyQuery({ a: 'a', c: [] })).toEqual('a=a') 35 | }) 36 | 37 | it('skips undefined in arrays', () => { 38 | expect(stringifyQuery({ a: [undefined, '3'] })).toEqual('a=3') 39 | expect(stringifyQuery({ a: [1, undefined, '3'] })).toEqual('a=1&a=3') 40 | expect(stringifyQuery({ a: [1, undefined, '3', undefined] })).toEqual( 41 | 'a=1&a=3' 42 | ) 43 | }) 44 | 45 | it('stringifies arrays', () => { 46 | expect(stringifyQuery({ e: ['b', 'a'] })).toEqual('e=b&e=a') 47 | }) 48 | 49 | it('encodes values', () => { 50 | expect(stringifyQuery({ e: '%', b: 'c' })).toEqual('e=%25&b=c') 51 | }) 52 | 53 | it('encodes values in arrays', () => { 54 | expect(stringifyQuery({ e: ['%', 'a'], b: 'c' })).toEqual('e=%25&e=a&b=c') 55 | }) 56 | 57 | it('encodes = in key', () => { 58 | expect(stringifyQuery({ '=': 'a' })).toEqual('%3D=a') 59 | }) 60 | 61 | it('keeps = in value', () => { 62 | expect(stringifyQuery({ a: '=' })).toEqual('a==') 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /packages/router/__tests__/useApi.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment jsdom 3 | */ 4 | import { mount } from '@vue/test-utils' 5 | import { computed } from 'vue' 6 | import { useRoute, createRouter, createMemoryHistory } from '../src' 7 | import { describe, expect, it } from 'vitest' 8 | 9 | describe('use apis', () => { 10 | it('unwraps useRoute()', async () => { 11 | const router = createRouter({ 12 | history: createMemoryHistory(), 13 | routes: [ 14 | { 15 | path: '/:any(.*)', 16 | component: {} as any, 17 | }, 18 | ], 19 | }) 20 | 21 | const wrapper = mount( 22 | { 23 | template: `

Query: {{ q }}

`, 24 | setup() { 25 | const route = useRoute() 26 | const q = computed(() => route.query.q) 27 | 28 | return { q } 29 | }, 30 | }, 31 | { 32 | global: { 33 | plugins: [router], 34 | }, 35 | } 36 | ) 37 | 38 | expect(wrapper.text()).toBe('Query:') 39 | 40 | await router.push('/?q=hi') 41 | expect(wrapper.text()).toBe('Query: hi') 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /packages/router/api-extractor.json: -------------------------------------------------------------------------------- 1 | // this the shared base config for all packages. 2 | { 3 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 4 | 5 | "mainEntryPointFilePath": "./dist/src/index.d.ts", 6 | 7 | "apiReport": { 8 | "enabled": true, 9 | "reportFolder": "/temp/" 10 | }, 11 | 12 | "docModel": { 13 | "enabled": true 14 | }, 15 | 16 | "dtsRollup": { 17 | "enabled": true, 18 | "untrimmedFilePath": "./dist/.d.ts" 19 | }, 20 | 21 | "tsdocMetadata": { 22 | "enabled": false 23 | }, 24 | 25 | "messages": { 26 | "compilerMessageReporting": { 27 | "default": { 28 | "logLevel": "warning" 29 | } 30 | }, 31 | 32 | "extractorMessageReporting": { 33 | "default": { 34 | "logLevel": "warning", 35 | "addToApiReportFile": true 36 | }, 37 | 38 | "ae-missing-release-tag": { 39 | "logLevel": "none" 40 | } 41 | }, 42 | 43 | "tsdocMessageReporting": { 44 | "default": { 45 | "logLevel": "warning" 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/router/e2e/devServer.mjs: -------------------------------------------------------------------------------- 1 | import { createServer } from 'vite' 2 | import viteConfig from './vite.config.mjs' 3 | const config = viteConfig({ prod: false }) 4 | 5 | /** @type {import('vite').ViteDevServer} */ 6 | let server = null 7 | 8 | ;(async () => { 9 | const app = await createServer({ 10 | configFile: false, 11 | ...config, 12 | }) 13 | server = await app.listen(process.env.PORT || 3000) 14 | internalResolve(server) 15 | })() 16 | 17 | let internalResolve = () => {} 18 | 19 | export function getServer() { 20 | return new Promise((resolve, reject) => { 21 | if (server) { 22 | resolve(server) 23 | } else { 24 | internalResolve = resolve 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /packages/router/e2e/encoding/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Router e2e tests - Encoding 8 | 9 | 10 | 11 | << Back to Homepage 12 |
13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/router/e2e/global.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, 4 | Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 5 | color: #2c3e50; 6 | } 7 | 8 | #app { 9 | padding: 0 20px; 10 | } 11 | 12 | ul { 13 | line-height: 1.5em; 14 | padding-left: 1.5em; 15 | } 16 | 17 | a { 18 | color: #7f8c8d; 19 | text-decoration: none; 20 | } 21 | 22 | a:hover { 23 | color: #4fc08d; 24 | } 25 | 26 | .router-link-active { 27 | color: darkorange; 28 | } 29 | .router-link-exact-active { 30 | color: red; 31 | } 32 | -------------------------------------------------------------------------------- /packages/router/e2e/guards-instances/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Router e2e tests - instances in guards 8 | 19 | 20 | 21 | 22 | << Back to Homepage 23 |
24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/router/e2e/hash/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Router e2e tests - Hash 8 | 9 | 10 | 11 | << Back to Homepage 12 |
13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/router/e2e/hash/index.ts: -------------------------------------------------------------------------------- 1 | import '../global.css' 2 | import { 3 | createRouter, 4 | useRoute, 5 | createWebHashHistory, 6 | RouteComponent, 7 | } from 'vue-router' 8 | import { createApp } from 'vue' 9 | 10 | const Home: RouteComponent = { 11 | template: `
home
`, 12 | } 13 | 14 | const Foo: RouteComponent = { template: '
Foo
' } 15 | const Bar: RouteComponent = { template: '
Bar
' } 16 | 17 | const Unicode: RouteComponent = { 18 | setup() { 19 | const route = useRoute() 20 | return { route } 21 | }, 22 | template: `
param: {{ route.params.id }}
`, 23 | } 24 | 25 | const router = createRouter({ 26 | // keep a trailing slash in this specific case because we are using a hash 27 | // history 28 | history: createWebHashHistory(), 29 | routes: [ 30 | { path: '/', component: Home }, 31 | { path: '/redirect', name: 'redirect', redirect: '/foo' }, 32 | { path: '/foo', component: Foo }, 33 | { path: '/bar', component: Bar }, 34 | { path: '/unicode/:id', name: 'unicode', component: Unicode }, 35 | { path: encodeURI('/n/é'), name: 'encoded', component: Foo }, 36 | ], 37 | }) 38 | 39 | const app = createApp({ 40 | setup() { 41 | const route = useRoute() 42 | return { route } 43 | }, 44 | 45 | template: ` 46 | 75 | 76 |

77 | path: {{ route.path }} 78 |
79 | query.t: {{ route.query.t }} 80 |
81 | hash: {{ route.hash }} 82 |

83 | 84 | 85 | `, 86 | }) 87 | app.use(router) 88 | 89 | window.r = router 90 | window.vm = app.mount('#app') 91 | -------------------------------------------------------------------------------- /packages/router/e2e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Router Examples 8 | 9 | 10 | 11 |
12 |

Vue Router Examples

13 | 14 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/router/e2e/index.ts: -------------------------------------------------------------------------------- 1 | import './global.css' 2 | import { createApp, ComponentPublicInstance, App } from 'vue' 3 | import { Router } from '../src' 4 | 5 | const tsmap = import.meta.glob('./**/index.ts') 6 | 7 | const DIR_RE = /^\.\/([^/]+)\// 8 | 9 | const examples: string[] = Object.keys(tsmap) 10 | .map(path => DIR_RE.exec(path)) 11 | .filter(match => !!match) 12 | .map(match => match![1] + '/') 13 | .sort() 14 | 15 | declare global { 16 | interface Window { 17 | app: App 18 | vm: ComponentPublicInstance 19 | r: Router 20 | } 21 | } 22 | 23 | const app = createApp({ 24 | data: () => ({ examples }), 25 | }) 26 | 27 | app.mount('#app') 28 | window.app = app 29 | -------------------------------------------------------------------------------- /packages/router/e2e/keep-alive/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Router e2e tests - Keep Alive 8 | 9 | 10 | 11 | << Back to Homepage 12 |
13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/router/e2e/keep-alive/index.ts: -------------------------------------------------------------------------------- 1 | import '../global.css' 2 | import { 3 | RouteComponent, 4 | createRouter, 5 | createWebHistory, 6 | useRouter, 7 | } from 'vue-router' 8 | import { createApp, ref } from 'vue' 9 | 10 | const Home: RouteComponent = { 11 | template: ` 12 |
13 |

Home

14 |

Counter: {{ n }}

15 | 16 |
17 | `, 18 | setup() { 19 | return { 20 | n: ref(0), 21 | } 22 | }, 23 | } 24 | 25 | const Foo: RouteComponent = { template: '
foo
' } 26 | 27 | const WithGuards: RouteComponent = { 28 | template: `
29 |

Enter Count {{ enterCount }}

30 |

Update Count {{ updateCount }}

31 |

Leave Count {{ leaveCount }}

32 | 33 | 34 |
`, 35 | 36 | beforeRouteEnter(to, from, next) { 37 | next(vm => { 38 | ;(vm as any).enterCount++ 39 | }) 40 | }, 41 | 42 | beforeRouteUpdate(to, from, next) { 43 | this.updateCount++ 44 | next() 45 | }, 46 | beforeRouteLeave(to, from, next) { 47 | this.leaveCount++ 48 | next() 49 | }, 50 | 51 | setup() { 52 | const enterCount = ref(0) 53 | const updateCount = ref(0) 54 | const leaveCount = ref(0) 55 | const router = useRouter() 56 | 57 | function reset() { 58 | enterCount.value = 0 59 | updateCount.value = 0 60 | leaveCount.value = 0 61 | } 62 | 63 | function changeQuery() { 64 | router.push({ query: { q: Date.now() } }) 65 | } 66 | return { 67 | reset, 68 | changeQuery, 69 | enterCount, 70 | updateCount, 71 | leaveCount, 72 | } 73 | }, 74 | } 75 | 76 | const webHistory = createWebHistory('/keep-alive') 77 | const router = createRouter({ 78 | history: webHistory, 79 | routes: [ 80 | { path: '/', component: Home }, 81 | { path: '/with-guards', component: WithGuards }, 82 | { 83 | path: '/foo', 84 | component: Foo, 85 | }, 86 | ], 87 | }) 88 | const app = createApp({ 89 | template: ` 90 |

KeepAlive

91 |
    92 |
  • /
  • 93 |
  • /foo
  • 94 |
  • /with-guards
  • 95 |
96 | 97 | 98 | 99 | 100 | 101 | `, 102 | }) 103 | app.use(router) 104 | 105 | app.mount('#app') 106 | -------------------------------------------------------------------------------- /packages/router/e2e/modal/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Router e2e tests - Modal 8 | 9 | 38 | 39 | 40 | 41 | << Back to Homepage 42 |
43 | 44 |
45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /packages/router/e2e/multi-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Router e2e tests - Multi app 8 | 9 | 10 | 11 | << Back to Homepage 12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | popstate count: 27 |
28 | guard call count: 29 | 30 |
31 |
32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /packages/router/e2e/runner.mjs: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv' 2 | import Nightwatch from 'nightwatch' 3 | import browserstack from 'browserstack-local' 4 | 5 | config() 6 | 7 | const { BROWSERSTACK_ACCESS_KEY } = process.env 8 | 9 | const args = process.argv.slice(2) 10 | 11 | // allow running browserstack local 12 | // note this works because nighwatch doesn't use this option 13 | const isLocal = args.indexOf('--local') > -1 14 | 15 | function getServer() { 16 | return args.indexOf('--dev') > -1 17 | ? null 18 | : import('./devServer.mjs').then(({ getServer }) => getServer()) 19 | } 20 | 21 | ;(async () => { 22 | const server = await getServer() 23 | 24 | try { 25 | /** @type {import('browserstack-local').Local} */ 26 | let bs_local 27 | if (isLocal) { 28 | if (!BROWSERSTACK_ACCESS_KEY) { 29 | throw new Error( 30 | ` 31 | (ONLY FOR MAINTAINERS) 32 | BROWSERSTACK_ACCESS_KEY is not set. Did you create the .env file? 33 | ` 34 | ) 35 | } 36 | // Code to start browserstack local before start of test 37 | console.log('Connecting local') 38 | bs_local = new browserstack.Local() 39 | Nightwatch.bs_local = bs_local 40 | 41 | bs_local.start( 42 | { key: process.env.BROWSERSTACK_ACCESS_KEY }, 43 | async error => { 44 | if (error) throw error 45 | 46 | console.log('Connected. Now testing...') 47 | await runNightwatchCli().finally(() => { 48 | // Code to stop browserstack local after end of single test 49 | bs_local.stop(() => { 50 | server?.close() 51 | }) 52 | }) 53 | } 54 | ) 55 | } else { 56 | await runNightwatchCli() 57 | server?.close() 58 | } 59 | } catch (ex) { 60 | console.log('There was an error while starting the test runner:\n\n') 61 | process.stderr.write(ex.stack + '\n') 62 | server?.close() 63 | process.exit(2) 64 | } 65 | })() 66 | 67 | function runNightwatchCli() { 68 | return new Promise((resolve, reject) => { 69 | Nightwatch.cli(argv => { 70 | Nightwatch.CliRunner(argv).setup().runTests().then(resolve).catch(reject) 71 | }) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /packages/router/e2e/scroll-behavior/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Router e2e tests - Scroll Behavior 8 | 9 | 26 | 27 | 28 | 29 | << Back to Homepage 30 |
31 | 32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /packages/router/e2e/scroll-behavior/scrollWaiter.ts: -------------------------------------------------------------------------------- 1 | function createScrollWaiter() { 2 | let resolve: ((value?: any) => void) | undefined 3 | let promise: Promise | undefined 4 | 5 | function add() { 6 | promise = new Promise(r => { 7 | resolve = r 8 | }) 9 | } 10 | 11 | function flush() { 12 | resolve && resolve() 13 | resolve = undefined 14 | promise = undefined 15 | } 16 | 17 | const waiter = { 18 | promise, 19 | add, 20 | flush, 21 | } 22 | 23 | Object.defineProperty(waiter, 'promise', { 24 | get: () => promise, 25 | }) 26 | 27 | return waiter 28 | } 29 | 30 | export const scrollWaiter = createScrollWaiter() 31 | -------------------------------------------------------------------------------- /packages/router/e2e/specs/encoding.js: -------------------------------------------------------------------------------- 1 | // const bsStatus = require('../browserstack-send-status') 2 | 3 | const baseURL = 'http://localhost:3000/encoding' 4 | 5 | const rawText = ' !"#$&\'()*+,/:;<=>?@[]^`{|}' 6 | 7 | const TIMEOUT = 2000 8 | 9 | module.exports = { 10 | // ...bsStatus(), 11 | 12 | '@tags': ['history', 'encoding', 'browserstack'], 13 | 14 | /** @type {import('nightwatch').NightwatchTest} */ 15 | 'encodes values'(browser) { 16 | browser 17 | .url(baseURL + '/') 18 | .assert.urlEquals(baseURL + '/') 19 | .waitForElementPresent('#app > *', TIMEOUT) 20 | 21 | .click('li:nth-child(3) a') 22 | .assert.urlEquals(baseURL + '/documents/%E2%82%ACuro') 23 | .assert.textContains('#fullPath', '/documents/%E2%82%ACuro') 24 | .assert.textContains('#path', '/documents/%E2%82%ACuro') 25 | .assert.textContains('#p-id', '"€uro"') 26 | 27 | // full encoding test 28 | .click('li:nth-child(8) a') 29 | browser.expect.element('#p-id').text.equals(`"${rawText}"`) 30 | browser.expect 31 | .element('#query') 32 | .text.equals(JSON.stringify({ 'a=': rawText }, null, 2)) 33 | browser.expect.element('#hash').text.equals('#' + rawText) 34 | 35 | // link by the browser with minimal encoding 36 | // browsers will encode it differently but the resulted decoded values 37 | // should be consistent across browsers 38 | browser 39 | .click('li:nth-child(7) a') 40 | .waitForElementPresent('#app > *', TIMEOUT) 41 | browser.expect.element('#p-id').text.equals(`"${rawText}"`) 42 | browser.expect 43 | .element('#query') 44 | .text.equals(JSON.stringify({ 'a=': rawText }, null, 2)) 45 | browser.expect.element('#hash').text.equals('#' + rawText) 46 | 47 | // check initial visit 48 | browser 49 | .url(baseURL + '/documents/%E2%82%ACuro') 50 | .waitForElementPresent('#app > *', TIMEOUT) 51 | // .assert.textContains('#fullPath', '/documents/%E2%82%ACuro') 52 | // .assert.textContains('#path', '/documents/%E2%82%ACuro') 53 | .assert.textContains('#p-id', '"€uro"') 54 | 55 | // TODO: invalid in safari, tests on those where this is valid 56 | // .url(baseURL + '/unicode/€uro') 57 | // .waitForElementPresent('#app > *', TIMEOUT) 58 | // navigation to unencoded value 59 | // depending on the browser the value will be encoded or not 60 | // .assert.textContains('#params', JSON.stringify({ id: '€uro' }, null, 2)) 61 | 62 | .end() 63 | }, 64 | } 65 | -------------------------------------------------------------------------------- /packages/router/e2e/specs/keep-alive.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '@tags': [], 3 | 4 | /** @type {import('nightwatch').NightwatchTest} */ 5 | KeepAlive: function (browser) { 6 | browser 7 | .url('http://localhost:3000/keep-alive/') 8 | .waitForElementPresent('#app > *', 1000) 9 | 10 | .assert.textContains('#counter', '0') 11 | .click('#increment') 12 | .assert.textContains('#counter', '1') 13 | 14 | .click('li:nth-child(2) a') 15 | .assert.textContains('.view', 'foo') 16 | .click('li:nth-child(1) a') 17 | .assert.textContains('#counter', '1') 18 | 19 | .click('li:nth-child(3) a') 20 | .assert.textContains('#enter-count', '1') 21 | .assert.textContains('#update-count', '0') 22 | .click('#change-query') 23 | .assert.textContains('#enter-count', '1') 24 | .assert.textContains('#update-count', '1') 25 | .back() 26 | .assert.textContains('#update-count', '2') 27 | .assert.textContains('#leave-count', '0') 28 | .back() 29 | .assert.textContains('#counter', '1') 30 | .forward() 31 | .assert.textContains('#enter-count', '2') 32 | .assert.textContains('#update-count', '2') 33 | .assert.textContains('#leave-count', '1') 34 | 35 | .end() 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /packages/router/e2e/specs/suspense.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '@tags': [], 3 | 4 | /** @type {import('nightwatch').NightwatchTest} */ 5 | 'suspense with guards': function (browser) { 6 | browser 7 | .url('http://localhost:3000/suspense/foo') 8 | .waitForElementPresent('#app > *', 1000) 9 | 10 | browser 11 | .click('li:nth-child(2) a') 12 | .waitForElementPresent('#Foo', 1000) 13 | .click('li:nth-child(4) a') 14 | .click('li:nth-child(3) a') 15 | .waitForElementPresent('#FooAsync', 1000) 16 | .click('li:nth-child(4) a') 17 | .click('li:nth-child(2) a') 18 | .waitForElementPresent('#Foo', 1000) 19 | .click('li:nth-child(4) a') 20 | .click('li:nth-child(1) a') 21 | .expect.element('#logs') 22 | .text.to.equal( 23 | [ 24 | `Foo: setup:update /foo - /foo?n=1`, 25 | `Foo: setup:leave /foo?n=1 - /foo-async`, 26 | `FooAsync: setup:update /foo-async - /foo-async?n=1`, 27 | `FooAsync: setup:leave /foo-async?n=1 - /foo`, 28 | `Foo: setup:update /foo - /foo?n=1`, 29 | `Foo: setup:leave /foo?n=1 - /`, 30 | ].join('\n') 31 | ) 32 | 33 | browser.end() 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /packages/router/e2e/specs/transitions.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '@tags': ['no-headless'], 3 | 4 | transitions: function (browser) { 5 | const TIMEOUT = 3000 6 | 7 | browser 8 | .url('http://localhost:3000/transitions/') 9 | .waitForElementPresent('#app > *', 1000) 10 | 11 | .click('li:nth-child(2) a') 12 | .assert.hasClass('.view.home', 'fade-leave-active') 13 | .waitForElementPresent('.view.parent', TIMEOUT) 14 | .assert.hasClass('.view.parent', 'fade-enter-active') 15 | .assert.not.hasClass('.child-view.default', 'slide-left-enter-active') 16 | .waitForElementNotPresent('.view.parent.fade-enter-active', TIMEOUT) 17 | 18 | .click('li:nth-child(3) a') 19 | .assert.hasClass('.child-view.default', 'slide-left-leave-active') 20 | .assert.hasClass('.child-view.foo', 'slide-left-enter-active') 21 | .waitForElementNotPresent('.child-view.default', TIMEOUT) 22 | 23 | .click('li:nth-child(4) a') 24 | .assert.hasClass('.child-view.foo', 'slide-left-leave-active') 25 | .assert.hasClass('.child-view.bar', 'slide-left-enter-active') 26 | .waitForElementNotPresent('.child-view.foo', TIMEOUT) 27 | 28 | .click('li:nth-child(2) a') 29 | .assert.hasClass('.child-view.bar', 'slide-right-leave-active') 30 | .assert.hasClass('.child-view.default', 'slide-right-enter-active') 31 | .waitForElementNotPresent('.child-view.bar', TIMEOUT) 32 | 33 | .click('li:nth-child(1) a') 34 | .assert.hasClass('.view.parent', 'fade-leave-active') 35 | .waitForElementPresent('.view.home', TIMEOUT) 36 | .assert.hasClass('.view.home', 'fade-enter-active') 37 | .waitForElementNotPresent('.view.home.fade-enter-active', TIMEOUT) 38 | 39 | .click('li:nth-child(5) a') 40 | .assert.hasClass('.view.home', 'fade-leave-active') 41 | .waitForElementNotPresent('.view.home', TIMEOUT) 42 | .click('li:nth-child(2) a') 43 | .assert.hasClass('.view.parent', 'fade-enter-active') 44 | 45 | .end() 46 | }, 47 | 48 | 'out in transitions': function (browser) { 49 | browser 50 | .url('http://localhost:3000/transitions/') 51 | .waitForElementPresent('#app > *', 1000) 52 | .click('#toggle-transition') 53 | 54 | .click('li:nth-child(7) a') 55 | .assert.textContains('.nested-view', 'foo') 56 | .click('li:nth-child(1) a') 57 | .waitForElementPresent('.view.home', 1000) 58 | .click('li:nth-child(7) a') 59 | .assert.textContains('.nested-view', 'foo') 60 | 61 | .end() 62 | }, 63 | } 64 | -------------------------------------------------------------------------------- /packages/router/e2e/suspense/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Router e2e tests - Suspense 8 | 9 | 10 | 11 | << Back to Homepage 12 |
13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/router/e2e/transitions/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Router e2e tests - Transitions 8 | 9 | 39 | 40 | 41 | 42 | << Back to Homepage 43 |
44 | 45 |
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /packages/router/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["index.ts", "*/*.ts", "../src/global.d.ts"], 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "ESNext", 6 | // "lib": ["es2017.object"] /* Specify library files to be included in the compilation. */, 7 | "declaration": true, 8 | // "declarationMap": true, 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "removeComments": false, 12 | "noEmit": false, 13 | 14 | "strict": true, 15 | 16 | "noUnusedLocals": true, 17 | // "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | 20 | "moduleResolution": "node", 21 | "esModuleInterop": true, 22 | "types": ["vite/client"], 23 | "paths": { 24 | "vue-router": ["../src"] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/router/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | module.exports = require('./dist/vue-router.prod.cjs') 5 | } else { 6 | module.exports = require('./dist/vue-router.cjs') 7 | } 8 | -------------------------------------------------------------------------------- /packages/router/size-checks/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import ts from 'rollup-plugin-typescript2' 4 | import replace from '@rollup/plugin-replace' 5 | import resolve from '@rollup/plugin-node-resolve' 6 | import commonjs from '@rollup/plugin-commonjs' 7 | import terser from '@rollup/plugin-terser' 8 | import { defineConfig } from 'rollup' 9 | 10 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 11 | 12 | const config = defineConfig({ 13 | external: ['vue'], 14 | output: { 15 | file: path.resolve(__dirname, './dist/webRouter.js'), 16 | format: 'es', 17 | }, 18 | input: path.resolve(__dirname, './webRouter.js'), 19 | plugins: [ 20 | replace({ 21 | preventAssignment: true, 22 | values: { 23 | __DEV__: 'false', 24 | // this is only used during tests 25 | __TEST__: 'false', 26 | // If the build is expected to run directly in the browser (global / esm builds) 27 | __BROWSER__: 'true', 28 | // is targeting bundlers? 29 | __BUNDLER__: 'false', 30 | __GLOBAL__: 'false', 31 | // is targeting Node (SSR)? 32 | __NODE_JS__: 'false', 33 | __VUE_PROD_DEVTOOLS__: 'false', 34 | 'process.env.NODE_ENV': JSON.stringify('production'), 35 | }, 36 | }), 37 | ts({ 38 | check: false, 39 | tsconfig: path.resolve(__dirname, '../tsconfig.json'), 40 | cacheRoot: path.resolve(__dirname, '../node_modules/.rts2_cache'), 41 | tsconfigOverride: { 42 | compilerOptions: { 43 | sourceMap: false, 44 | declaration: false, 45 | declarationMap: false, 46 | }, 47 | exclude: ['__tests__', 'test-dts'], 48 | }, 49 | }), 50 | resolve(), 51 | commonjs(), 52 | terser({ 53 | format: { 54 | comments: false, 55 | }, 56 | module: true, 57 | compress: { 58 | ecma: 2015, 59 | pure_getters: true, 60 | }, 61 | }), 62 | ], 63 | }) 64 | 65 | export default config 66 | -------------------------------------------------------------------------------- /packages/router/size-checks/webRouter.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from '../dist/vue-router.esm-bundler' 2 | 3 | createRouter({ 4 | history: createWebHistory(), 5 | routes: [], 6 | }) 7 | -------------------------------------------------------------------------------- /packages/router/size-checks/webRouterAndVue.js: -------------------------------------------------------------------------------- 1 | import { h, createApp } from 'vue' 2 | import { createRouter, createWebHistory } from '../dist/vue-router.esm-bundler' 3 | 4 | createRouter({ 5 | history: createWebHistory(), 6 | routes: [], 7 | }) 8 | 9 | // The bare minimum code required for rendering something to the screen 10 | createApp({ 11 | render: () => h('div', 'hello world!'), 12 | }).mount('#app') 13 | -------------------------------------------------------------------------------- /packages/router/src/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows customizing existing types of the router that are used globally like `$router`, ``, etc. **ONLY FOR INTERNAL USAGE**. 3 | * 4 | * - `$router` - the router instance 5 | * - `$route` - the current route location 6 | * - `beforeRouteEnter` - Page component option 7 | * - `beforeRouteUpdate` - Page component option 8 | * - `beforeRouteLeave` - Page component option 9 | * - `RouterLink` - RouterLink Component 10 | * - `RouterView` - RouterView Component 11 | * 12 | * @internal 13 | */ 14 | export interface TypesConfig {} 15 | -------------------------------------------------------------------------------- /packages/router/src/global.d.ts: -------------------------------------------------------------------------------- 1 | // Global compile-time constants 2 | declare var __DEV__: boolean 3 | declare var __TEST__: boolean 4 | declare var __FEATURE_PROD_DEVTOOLS__: boolean 5 | declare var __BROWSER__: boolean 6 | declare var __NODE_JS__: boolean 7 | declare var __CI__: boolean 8 | -------------------------------------------------------------------------------- /packages/router/src/history/hash.ts: -------------------------------------------------------------------------------- 1 | import { RouterHistory } from './common' 2 | import { createWebHistory } from './html5' 3 | import { warn } from '../warning' 4 | 5 | /** 6 | * Creates a hash history. Useful for web applications with no host (e.g. `file://`) or when configuring a server to 7 | * handle any URL is not possible. 8 | * 9 | * @param base - optional base to provide. Defaults to `location.pathname + location.search` If there is a `` tag 10 | * in the `head`, its value will be ignored in favor of this parameter **but note it affects all the history.pushState() 11 | * calls**, meaning that if you use a `` tag, it's `href` value **has to match this parameter** (ignoring anything 12 | * after the `#`). 13 | * 14 | * @example 15 | * ```js 16 | * // at https://example.com/folder 17 | * createWebHashHistory() // gives a url of `https://example.com/folder#` 18 | * createWebHashHistory('/folder/') // gives a url of `https://example.com/folder/#` 19 | * // if the `#` is provided in the base, it won't be added by `createWebHashHistory` 20 | * createWebHashHistory('/folder/#/app/') // gives a url of `https://example.com/folder/#/app/` 21 | * // you should avoid doing this because it changes the original url and breaks copying urls 22 | * createWebHashHistory('/other-folder/') // gives a url of `https://example.com/other-folder/#` 23 | * 24 | * // at file:///usr/etc/folder/index.html 25 | * // for locations with no `host`, the base is ignored 26 | * createWebHashHistory('/iAmIgnored') // gives a url of `file:///usr/etc/folder/index.html#` 27 | * ``` 28 | */ 29 | export function createWebHashHistory(base?: string): RouterHistory { 30 | // Make sure this implementation is fine in terms of encoding, specially for IE11 31 | // for `file://`, directly use the pathname and ignore the base 32 | // location.pathname contains an initial `/` even at the root: `https://example.com` 33 | base = location.host ? base || location.pathname + location.search : '' 34 | // allow the user to provide a `#` in the middle: `/base/#/app` 35 | if (!base.includes('#')) base += '#' 36 | 37 | if (__DEV__ && !base.endsWith('#/') && !base.endsWith('#')) { 38 | warn( 39 | `A hash base must end with a "#":\n"${base}" should be "${base.replace( 40 | /#.*$/, 41 | '#' 42 | )}".` 43 | ) 44 | } 45 | return createWebHistory(base) 46 | } 47 | -------------------------------------------------------------------------------- /packages/router/src/injectionSymbols.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey, ComputedRef, Ref } from 'vue' 2 | import type { RouteLocationNormalizedLoaded } from './typed-routes' 3 | import { RouteRecordNormalized } from './matcher/types' 4 | import type { Router } from './router' 5 | 6 | /** 7 | * RouteRecord being rendered by the closest ancestor Router View. Used for 8 | * `onBeforeRouteUpdate` and `onBeforeRouteLeave`. rvlm stands for Router View 9 | * Location Matched 10 | * 11 | * @internal 12 | */ 13 | export const matchedRouteKey = Symbol( 14 | __DEV__ ? 'router view location matched' : '' 15 | ) as InjectionKey> 16 | 17 | /** 18 | * Allows overriding the router view depth to control which component in 19 | * `matched` is rendered. rvd stands for Router View Depth 20 | * 21 | * @internal 22 | */ 23 | export const viewDepthKey = Symbol( 24 | __DEV__ ? 'router view depth' : '' 25 | ) as InjectionKey | number> 26 | 27 | /** 28 | * Allows overriding the router instance returned by `useRouter` in tests. r 29 | * stands for router 30 | * 31 | * @internal 32 | */ 33 | export const routerKey = Symbol(__DEV__ ? 'router' : '') as InjectionKey 34 | 35 | /** 36 | * Allows overriding the current route returned by `useRoute` in tests. rl 37 | * stands for route location 38 | * 39 | * @internal 40 | */ 41 | export const routeLocationKey = Symbol( 42 | __DEV__ ? 'route location' : '' 43 | ) as InjectionKey 44 | 45 | /** 46 | * Allows overriding the current route used by router-view. Internally this is 47 | * used when the `route` prop is passed. 48 | * 49 | * @internal 50 | */ 51 | export const routerViewLocationKey = Symbol( 52 | __DEV__ ? 'router view location' : '' 53 | ) as InjectionKey> 54 | -------------------------------------------------------------------------------- /packages/router/src/matcher/pathMatcher.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecord } from './types' 2 | import { 3 | tokensToParser, 4 | PathParser, 5 | PathParserOptions, 6 | } from './pathParserRanker' 7 | import { tokenizePath } from './pathTokenizer' 8 | import { warn } from '../warning' 9 | import { assign } from '../utils' 10 | 11 | export interface RouteRecordMatcher extends PathParser { 12 | record: RouteRecord 13 | parent: RouteRecordMatcher | undefined 14 | children: RouteRecordMatcher[] 15 | // aliases that must be removed when removing this record 16 | alias: RouteRecordMatcher[] 17 | } 18 | 19 | export function createRouteRecordMatcher( 20 | record: Readonly, 21 | parent: RouteRecordMatcher | undefined, 22 | options?: PathParserOptions 23 | ): RouteRecordMatcher { 24 | const parser = tokensToParser(tokenizePath(record.path), options) 25 | 26 | // warn against params with the same name 27 | if (__DEV__) { 28 | const existingKeys = new Set() 29 | for (const key of parser.keys) { 30 | if (existingKeys.has(key.name)) 31 | warn( 32 | `Found duplicated params with name "${key.name}" for path "${record.path}". Only the last one will be available on "$route.params".` 33 | ) 34 | existingKeys.add(key.name) 35 | } 36 | } 37 | 38 | const matcher: RouteRecordMatcher = assign(parser, { 39 | record, 40 | parent, 41 | // these needs to be populated by the parent 42 | children: [], 43 | alias: [], 44 | }) 45 | 46 | if (parent) { 47 | // both are aliases or both are not aliases 48 | // we don't want to mix them because the order is used when 49 | // passing originalRecord in Matcher.addRoute 50 | if (!matcher.record.aliasOf === !parent.record.aliasOf) 51 | parent.children.push(matcher) 52 | } 53 | 54 | return matcher 55 | } 56 | -------------------------------------------------------------------------------- /packages/router/src/typed-routes/index.ts: -------------------------------------------------------------------------------- 1 | export type * from './params' 2 | export type * from './route-map' 3 | export type * from './route-location' 4 | export type * from './route-records' 5 | export type * from './navigation-guards' 6 | -------------------------------------------------------------------------------- /packages/router/src/typed-routes/params.ts: -------------------------------------------------------------------------------- 1 | import type { RouteMap } from './route-map' 2 | 3 | /** 4 | * Utility type for raw and non raw params like :id+ 5 | * 6 | */ 7 | export type ParamValueOneOrMore = [ 8 | ParamValue, 9 | ...ParamValue[], 10 | ] 11 | 12 | /** 13 | * Utility type for raw and non raw params like :id* 14 | * 15 | */ 16 | export type ParamValueZeroOrMore = true extends isRaw 17 | ? ParamValue[] | undefined | null 18 | : ParamValue[] | undefined 19 | 20 | /** 21 | * Utility type for raw and non raw params like :id? 22 | * 23 | */ 24 | export type ParamValueZeroOrOne = true extends isRaw 25 | ? string | number | null | undefined 26 | : string 27 | 28 | /** 29 | * Utility type for raw and non raw params like :id 30 | * 31 | */ 32 | export type ParamValue = true extends isRaw 33 | ? string | number 34 | : string 35 | 36 | // TODO: finish this refactor 37 | // export type ParamValueOneOrMoreRaw = [ParamValueRaw, ...ParamValueRaw[]] 38 | // export type ParamValue = string 39 | // export type ParamValueRaw = string | number 40 | 41 | /** 42 | * Generate a type safe params for a route location. Requires the name of the route to be passed as a generic. 43 | * @see {@link RouteParamsGeneric} 44 | */ 45 | export type RouteParams = 46 | RouteMap[Name]['params'] 47 | 48 | /** 49 | * Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic. 50 | * @see {@link RouteParamsRaw} 51 | */ 52 | export type RouteParamsRaw = 53 | RouteMap[Name]['paramsRaw'] 54 | -------------------------------------------------------------------------------- /packages/router/src/typed-routes/route-map.ts: -------------------------------------------------------------------------------- 1 | import type { TypesConfig } from '../config' 2 | import type { RouteParamsGeneric, RouteParamsRawGeneric } from '../types' 3 | import type { RouteRecord } from '../matcher/types' 4 | 5 | /** 6 | * Helper type to define a Typed `RouteRecord` 7 | * @see {@link RouteRecord} 8 | */ 9 | export interface RouteRecordInfo< 10 | // the name cannot be nullish here as that would not allow type narrowing 11 | Name extends string | symbol = string, 12 | Path extends string = string, 13 | // TODO: could probably be inferred from the Params 14 | ParamsRaw extends RouteParamsRawGeneric = RouteParamsRawGeneric, 15 | Params extends RouteParamsGeneric = RouteParamsGeneric, 16 | // NOTE: this is the only type param that feels wrong because its default 17 | // value is the default value to avoid breaking changes but it should be the 18 | // generic version by default instead (string | symbol) 19 | ChildrenNames extends string | symbol = never, 20 | // TODO: implement meta with a defineRoute macro 21 | // Meta extends RouteMeta = RouteMeta, 22 | > { 23 | name: Name 24 | path: Path 25 | paramsRaw: ParamsRaw 26 | params: Params 27 | childrenNames: ChildrenNames 28 | // TODO: implement meta with a defineRoute macro 29 | // meta: Meta 30 | } 31 | 32 | export type RouteRecordInfoGeneric = RouteRecordInfo< 33 | string | symbol, 34 | string, 35 | RouteParamsRawGeneric, 36 | RouteParamsGeneric, 37 | string | symbol 38 | > 39 | 40 | /** 41 | * Convenience type to get the typed RouteMap or a generic one if not provided. It is extracted from the {@link TypesConfig} if it exists, it becomes {@link RouteMapGeneric} otherwise. 42 | */ 43 | export type RouteMap = 44 | TypesConfig extends Record<'RouteNamedMap', infer RouteNamedMap> 45 | ? RouteNamedMap 46 | : RouteMapGeneric 47 | 48 | /** 49 | * Generic version of the `RouteMap`. 50 | */ 51 | export type RouteMapGeneric = Record 52 | -------------------------------------------------------------------------------- /packages/router/src/typed-routes/route-records.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | RouteLocation, 3 | RouteLocationNormalized, 4 | RouteLocationRaw, 5 | } from './route-location' 6 | import type { RouteMap, RouteMapGeneric } from './route-map' 7 | 8 | /** 9 | * @internal 10 | */ 11 | export type RouteRecordRedirectOption = 12 | | RouteLocationRaw 13 | | ((to: RouteLocation) => RouteLocationRaw) 14 | 15 | /** 16 | * Generic version of {@link RouteRecordName}. 17 | */ 18 | export type RouteRecordNameGeneric = string | symbol | undefined 19 | 20 | /** 21 | * Possible values for a route record **after normalization** 22 | * 23 | * NOTE: since `RouteRecordName` is a type, it evaluates too early and it's often the generic version {@link RouteRecordNameGeneric}. If you need a typed version of all of the names of routes, use {@link RouteMap | `keyof RouteMap`} 24 | */ 25 | export type RouteRecordName = RouteMapGeneric extends RouteMap 26 | ? RouteRecordNameGeneric 27 | : keyof RouteMap 28 | 29 | /** 30 | * @internal 31 | */ 32 | export type _RouteRecordProps = 33 | | boolean 34 | | Record 35 | | ((to: RouteLocationNormalized) => Record) 36 | -------------------------------------------------------------------------------- /packages/router/src/types/typeGuards.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationRaw, RouteRecordNameGeneric } from '../typed-routes' 2 | 3 | export function isRouteLocation(route: any): route is RouteLocationRaw { 4 | return typeof route === 'string' || (route && typeof route === 'object') 5 | } 6 | 7 | export function isRouteName(name: any): name is RouteRecordNameGeneric { 8 | return typeof name === 'string' || typeof name === 'symbol' 9 | } 10 | -------------------------------------------------------------------------------- /packages/router/src/types/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a union type that still allows autocompletion for strings. 3 | * @internal 4 | */ 5 | export type _LiteralUnion = 6 | | LiteralType 7 | | (BaseType & Record) 8 | 9 | /** 10 | * Maybe a promise maybe not 11 | * @internal 12 | */ 13 | export type _Awaitable = T | PromiseLike 14 | 15 | /** 16 | * @internal 17 | */ 18 | export type _Simplify = { [K in keyof T]: T[K] } 19 | 20 | /** 21 | * @internal 22 | */ 23 | export type _AlphaNumeric = 24 | | 'a' 25 | | 'A' 26 | | 'b' 27 | | 'B' 28 | | 'c' 29 | | 'C' 30 | | 'd' 31 | | 'D' 32 | | 'e' 33 | | 'E' 34 | | 'f' 35 | | 'F' 36 | | 'g' 37 | | 'G' 38 | | 'h' 39 | | 'H' 40 | | 'i' 41 | | 'I' 42 | | 'j' 43 | | 'J' 44 | | 'k' 45 | | 'K' 46 | | 'l' 47 | | 'L' 48 | | 'm' 49 | | 'M' 50 | | 'n' 51 | | 'N' 52 | | 'o' 53 | | 'O' 54 | | 'p' 55 | | 'P' 56 | | 'q' 57 | | 'Q' 58 | | 'r' 59 | | 'R' 60 | | 's' 61 | | 'S' 62 | | 't' 63 | | 'T' 64 | | 'u' 65 | | 'U' 66 | | 'v' 67 | | 'V' 68 | | 'w' 69 | | 'W' 70 | | 'x' 71 | | 'X' 72 | | 'y' 73 | | 'Y' 74 | | 'z' 75 | | 'Z' 76 | | '0' 77 | | '1' 78 | | '2' 79 | | '3' 80 | | '4' 81 | | '5' 82 | | '6' 83 | | '7' 84 | | '8' 85 | | '9' 86 | | '_' 87 | -------------------------------------------------------------------------------- /packages/router/src/useApi.ts: -------------------------------------------------------------------------------- 1 | import { inject } from 'vue' 2 | import { routerKey, routeLocationKey } from './injectionSymbols' 3 | import { Router } from './router' 4 | import { RouteMap } from './typed-routes/route-map' 5 | import { RouteLocationNormalizedLoaded } from './typed-routes' 6 | 7 | /** 8 | * Returns the router instance. Equivalent to using `$router` inside 9 | * templates. 10 | */ 11 | export function useRouter(): Router { 12 | return inject(routerKey)! 13 | } 14 | 15 | /** 16 | * Returns the current route location. Equivalent to using `$route` inside 17 | * templates. 18 | */ 19 | export function useRoute( 20 | _name?: Name 21 | ) { 22 | return inject(routeLocationKey) as RouteLocationNormalizedLoaded< 23 | Name | RouteMap[Name]['childrenNames'] 24 | > 25 | } 26 | -------------------------------------------------------------------------------- /packages/router/src/utils/README.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | Split in multiple files to enable mocking in tests 4 | -------------------------------------------------------------------------------- /packages/router/src/utils/callbacks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a list of callbacks that can be reset. Used to create before and after navigation guards list 3 | */ 4 | export function useCallbacks() { 5 | let handlers: T[] = [] 6 | 7 | function add(handler: T): () => void { 8 | handlers.push(handler) 9 | return () => { 10 | const i = handlers.indexOf(handler) 11 | if (i > -1) handlers.splice(i, 1) 12 | } 13 | } 14 | 15 | function reset() { 16 | handlers = [] 17 | } 18 | 19 | return { 20 | add, 21 | list: () => handlers.slice(), 22 | reset, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/router/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | export const isBrowser = typeof document !== 'undefined' 2 | -------------------------------------------------------------------------------- /packages/router/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RouteParamsGeneric, 3 | RouteComponent, 4 | RouteParamsRawGeneric, 5 | RouteParamValueRaw, 6 | RawRouteComponent, 7 | } from '../types' 8 | 9 | export * from './env' 10 | 11 | /** 12 | * Allows differentiating lazy components from functional components and vue-class-component 13 | * @internal 14 | * 15 | * @param component 16 | */ 17 | export function isRouteComponent( 18 | component: RawRouteComponent 19 | ): component is RouteComponent { 20 | return ( 21 | typeof component === 'object' || 22 | 'displayName' in component || 23 | 'props' in component || 24 | '__vccOpts' in component 25 | ) 26 | } 27 | 28 | export function isESModule(obj: any): obj is { default: RouteComponent } { 29 | return ( 30 | obj.__esModule || 31 | obj[Symbol.toStringTag] === 'Module' || 32 | // support CF with dynamic imports that do not 33 | // add the Module string tag 34 | (obj.default && isRouteComponent(obj.default)) 35 | ) 36 | } 37 | 38 | export const assign = Object.assign 39 | 40 | export function applyToParams( 41 | fn: (v: string | number | null | undefined) => string, 42 | params: RouteParamsRawGeneric | undefined 43 | ): RouteParamsGeneric { 44 | const newParams: RouteParamsGeneric = {} 45 | 46 | for (const key in params) { 47 | const value = params[key] 48 | newParams[key] = isArray(value) 49 | ? value.map(fn) 50 | : fn(value as Exclude) 51 | } 52 | 53 | return newParams 54 | } 55 | 56 | export const noop = () => {} 57 | 58 | /** 59 | * Typesafe alternative to Array.isArray 60 | * https://github.com/microsoft/TypeScript/pull/48228 61 | */ 62 | export const isArray: (arg: ArrayLike | any) => arg is ReadonlyArray = 63 | Array.isArray 64 | -------------------------------------------------------------------------------- /packages/router/src/warning.ts: -------------------------------------------------------------------------------- 1 | export function warn(msg: string, ..._args: any[]): void 2 | export function warn(msg: string): void { 3 | // avoid using ...args as it breaks in older Edge builds 4 | const args = Array.from(arguments).slice(1) 5 | console.warn.apply( 6 | console, 7 | ['[Vue Router warn]: ' + msg].concat(args) as [string, ...any[]] 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /packages/router/test-dts/components.test-d.tsx: -------------------------------------------------------------------------------- 1 | import 'vue/jsx' 2 | import { 3 | RouterLink, 4 | RouterView, 5 | createRouter, 6 | createMemoryHistory, 7 | } from './index' 8 | import { it, describe, expectTypeOf } from 'vitest' 9 | 10 | describe('Components', () => { 11 | let router = createRouter({ 12 | history: createMemoryHistory(), 13 | routes: [], 14 | }) 15 | 16 | // TODO: split into multiple tests 17 | it('works', () => { 18 | // RouterLink 19 | // @ts-expect-error missing to 20 | expectError() 21 | // @ts-expect-error: invalid prop 22 | expectError() 23 | // @ts-expect-error: invalid prop 24 | expectError() 25 | expectTypeOf() 26 | expectTypeOf() 27 | expectTypeOf() 28 | expectTypeOf() 29 | expectTypeOf() 30 | 31 | // RouterView 32 | expectTypeOf() 33 | expectTypeOf() 34 | expectTypeOf() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/router/test-dts/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../dist/vue-router' 2 | // export * from '../src' 3 | -------------------------------------------------------------------------------- /packages/router/test-dts/legacy.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, expectTypeOf, it } from 'vitest' 2 | import { 3 | useRouter, 4 | useRoute, 5 | // rename types for better error messages, otherwise they have the same name 6 | // RouteLocationNormalizedLoadedTyped as I_RLNLT 7 | } from './index' 8 | import { defineComponent } from 'vue' 9 | 10 | describe('Instance types', () => { 11 | it('creates a $route instance property', () => { 12 | defineComponent({ 13 | methods: { 14 | doStuff() { 15 | // TODO: can't do a proper check because of typed routes 16 | expectTypeOf(this.$route.params).toMatchTypeOf(useRoute().params) 17 | }, 18 | }, 19 | }) 20 | }) 21 | 22 | it('creates $router instance properties', () => { 23 | defineComponent({ 24 | methods: { 25 | doStuff() { 26 | // TODO: can't do a proper check because of typed routes 27 | expectTypeOf(this.$router.back).toEqualTypeOf(useRouter().back) 28 | }, 29 | }, 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/router/test-dts/meta.test-d.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from './index' 2 | import { defineComponent } from 'vue' 3 | import { describe, it, expectTypeOf } from 'vitest' 4 | 5 | const component = defineComponent({}) 6 | 7 | declare module '.' { 8 | interface RouteMeta { 9 | requiresAuth?: boolean 10 | nested: { foo: string } 11 | } 12 | } 13 | 14 | describe('RouteMeta', () => { 15 | it('route creation', () => { 16 | const router = createRouter({ 17 | history: createWebHistory(), 18 | routes: [ 19 | { 20 | path: '/', 21 | component, 22 | meta: { 23 | requiresAuth: true, 24 | lol: true, 25 | nested: { 26 | foo: 'bar', 27 | }, 28 | }, 29 | }, 30 | { 31 | path: '/hey', 32 | component, 33 | // @ts-expect-error: meta is missing `nested` 34 | meta: {}, 35 | }, 36 | ], 37 | }) 38 | 39 | router.addRoute({ 40 | path: '/foo', 41 | component, 42 | meta: { 43 | nested: { 44 | foo: 'foo', 45 | }, 46 | }, 47 | }) 48 | }) 49 | 50 | it('route location in guards', () => { 51 | const router = createRouter({ 52 | history: createWebHistory(), 53 | routes: [], 54 | }) 55 | router.beforeEach(to => { 56 | expectTypeOf<{ requiresAuth?: Boolean; nested?: { foo: string } }>( 57 | to.meta 58 | ) 59 | expectTypeOf(to.meta.lol) 60 | if (to.meta.nested?.foo == 'foo' || to.meta.lol) return false 61 | return 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /packages/router/test-dts/navigationGuards.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectTypeOf, describe, it } from 'vitest' 2 | import { 3 | createRouter, 4 | createWebHistory, 5 | isNavigationFailure, 6 | NavigationFailure, 7 | NavigationFailureType, 8 | RouteLocationNormalized, 9 | RouteLocationRaw, 10 | } from './index' 11 | 12 | const router = createRouter({ 13 | history: createWebHistory(), 14 | routes: [], 15 | }) 16 | 17 | describe('Navigation guards', () => { 18 | // TODO: split into multiple tests 19 | it('works', () => { 20 | router.beforeEach((to, from) => { 21 | return { path: '/' } 22 | }) 23 | 24 | router.beforeEach((to, from) => { 25 | return '/' 26 | }) 27 | 28 | router.beforeEach((to, from) => { 29 | return false 30 | }) 31 | 32 | router.beforeEach((to, from, next) => { 33 | next(undefined) 34 | }) 35 | 36 | // @ts-expect-error 37 | router.beforeEach((to, from, next) => { 38 | return Symbol('not supported') 39 | }) 40 | // @ts-expect-error 41 | router.beforeEach(() => { 42 | return Symbol('not supported') 43 | }) 44 | 45 | router.beforeEach((to, from, next) => { 46 | // @ts-expect-error 47 | next(Symbol('not supported')) 48 | }) 49 | 50 | router.afterEach((to, from, failure) => { 51 | expectTypeOf(failure) 52 | if (isNavigationFailure(failure)) { 53 | expectTypeOf(failure.from) 54 | expectTypeOf(failure.to) 55 | } 56 | if (isNavigationFailure(failure, NavigationFailureType.cancelled)) { 57 | expectTypeOf(failure.from) 58 | expectTypeOf(failure.to) 59 | } 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /packages/router/test-dts/routeRecords.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'vitest' 2 | import type { RouteLocationNormalized, RouteRecordRaw } from './index' 3 | import { defineComponent } from 'vue' 4 | 5 | const component = defineComponent({}) 6 | const components = { default: component } 7 | 8 | const routes: RouteRecordRaw[] = [] 9 | 10 | describe('RouteRecords', () => { 11 | // TODO: split into multiple tests 12 | it('works', () => { 13 | routes.push({ path: '/', redirect: '/foo' }) 14 | 15 | // @ts-expect-error cannot have components and component at the same time 16 | routes.push({ path: '/', components, component }) 17 | 18 | // a redirect record with children to point to a child 19 | routes.push({ 20 | path: '/', 21 | redirect: '/foo', 22 | children: [ 23 | { 24 | path: 'foo', 25 | component, 26 | }, 27 | ], 28 | }) 29 | 30 | // same but with a nested route 31 | routes.push({ 32 | path: '/', 33 | component, 34 | redirect: '/foo', 35 | children: [ 36 | { 37 | path: 'foo', 38 | component, 39 | }, 40 | ], 41 | }) 42 | 43 | routes.push({ path: '/a/b', component, props: true }) 44 | routes.push({ 45 | path: '/a/b', 46 | component, 47 | props: (to: RouteLocationNormalized<'/[id]+'>) => to.params.id, 48 | }) 49 | // @ts-expect-error: props should be an object 50 | routes.push({ path: '/a/b', components, props: to => to.params.id }) 51 | routes.push({ 52 | path: '/a/b', 53 | components, 54 | props: { 55 | default: (to: RouteLocationNormalized<'/[id]+'>) => to.params.id, 56 | }, 57 | }) 58 | routes.push({ path: '/', components, props: true }) 59 | 60 | // let r: RouteRecordRaw = { 61 | // path: '/', 62 | // component, 63 | // components, 64 | // } 65 | 66 | function filterNestedChildren(children: RouteRecordRaw[]) { 67 | return children.filter(r => { 68 | if (r.redirect) { 69 | r.children?.map(() => {}) 70 | } 71 | if (r.children) { 72 | r.children = filterNestedChildren(r.children) 73 | } 74 | }) 75 | } 76 | filterNestedChildren(routes) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /packages/router/test-dts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "declaration": true, 6 | "paths": { 7 | "vue-router": [ 8 | "../dist" 9 | ] 10 | }, 11 | "noImplicitReturns": false 12 | }, 13 | "include": [ 14 | "./", 15 | "components.test-d.tsx", 16 | "../__tests__/createRouter.test-d.ts" 17 | ], 18 | "exclude": [ 19 | "../__tests__", 20 | "../src" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/global.d.ts", 4 | "src/**/*.ts", 5 | "__tests__/**/*.ts", 6 | "test-dts/**/*.ts" 7 | ], 8 | "compilerOptions": { 9 | "baseUrl": ".", 10 | "rootDir": ".", 11 | "outDir": "dist", 12 | "sourceMap": false, 13 | "noEmit": true, 14 | "target": "esnext", 15 | "module": "esnext", 16 | "moduleResolution": "Bundler", 17 | "allowJs": false, 18 | "noUnusedLocals": true, 19 | "strictNullChecks": true, 20 | "noImplicitAny": true, 21 | "noImplicitThis": true, 22 | "noImplicitReturns": true, 23 | "strict": true, 24 | "skipLibCheck": true, 25 | // "noUncheckedIndexedAccess": true, 26 | "experimentalDecorators": true, 27 | "resolveJsonModule": true, 28 | "esModuleInterop": true, 29 | "removeComments": false, 30 | "jsx": "preserve", 31 | "lib": [ 32 | "esnext", 33 | "dom" 34 | ], 35 | "types": [ 36 | "node", 37 | "vite/client" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/router/vetur/attributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "type": "string | symbol", 4 | "description": "When a `` has a `name` prop, it will render the component with the corresponding name in the matched route record's components option." 5 | }, 6 | "route": { 7 | "description": "When a `` has a `route` prop, it will use that resolved Route Location instead of the current location." 8 | }, 9 | "to": { 10 | "description": "Denotes the target route of the link. When clicked, the value of the `to` prop will be internally passed to `router.push()`, so the value can be either a string or a location descriptor object." 11 | }, 12 | "replace": { 13 | "type": "boolean", 14 | "description": "Setting replace prop will call `router.replace()` instead of `router.push()` when clicked, so the navigation will replace the current history entry." 15 | }, 16 | "custom": { 17 | "type": "boolean", 18 | "description": "Whether `` should not wrap its content in an `` tag." 19 | }, 20 | "active-class": { 21 | "type": "string", 22 | "description": "Configure the active CSS class applied when the link is active. Note the default value can also be configured globally via the `linkActiveClass` router constructor option." 23 | }, 24 | "exact-active-class": { 25 | "type": "string", 26 | "description": "Configure the active CSS class applied when the link is exactly active. Note the default value can also be configured globally via the `linkExactActiveClass` router constructor option." 27 | }, 28 | "aria-current-value": { 29 | "options": ["page", "step", "location", "date", "time", "true", "false"], 30 | "description": "Configure the value of `aria-current` when the link is active with exact match. It must be one of the [allowed values for `aria-current`](https://www.w3.org/TR/wai-aria-1.2/#aria-current) in the ARIA spec. In most cases, the default of `page` should be the best fit." 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/router/vetur/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "router-view": { 3 | "attributes": ["name", "route"], 4 | "description": "Component that renders the matched component for the current location. Components rendered by `` can also contain their own `` to render nested routes." 5 | }, 6 | "router-link": { 7 | "attributes": ["to", "replace", "custom", "active-class","exact-active-class", "aria-current-value"], 8 | "description": "Component that renders an `` with the correct `href` attribute and click listeners to trigger a local navigation when clicked. Can also customize its rendering by providing the `custom` prop and using its `v-slot` API." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/router/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import Vue from '@vitejs/plugin-vue' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | const __dirname = new URL('.', import.meta.url).pathname 6 | export default defineConfig({ 7 | resolve: { 8 | alias: [], 9 | }, 10 | define: { 11 | __DEV__: true, 12 | __TEST__: true, 13 | __BROWSER__: true, 14 | }, 15 | plugins: [Vue()], 16 | 17 | test: { 18 | // open: false, 19 | coverage: { 20 | include: ['src/**/*.ts'], 21 | exclude: [ 22 | 'src/**/*.d.ts', 23 | 'src/**/*.test-d.ts', 24 | 'src/**/*.spec.ts', 25 | // '/node_modules/', 26 | 'src/index.ts', 27 | 'src/devtools.ts', 28 | ], 29 | }, 30 | typecheck: { 31 | enabled: true, 32 | checker: 'vue-tsc', 33 | // only: true, 34 | // by default it includes all specs too 35 | // include: ['**/*.test-d.ts'], 36 | 37 | // tsconfig: './tsconfig.typecheck.json', 38 | }, 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /packages/router/vue-router-auto-routes.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Array of routes generated by unplugin-vue-router 3 | */ 4 | export declare const routes: any[] 5 | -------------------------------------------------------------------------------- /packages/router/vue-router-auto.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extended by unplugin-vue-router to create typed routes. 3 | */ 4 | export interface RouteNamedMap {} 5 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /scripts/check-size.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import chalk from 'chalk' 5 | import { gzipSync } from 'zlib' 6 | import { compress } from 'brotli' 7 | 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 9 | 10 | async function checkFileSize(filePath) { 11 | const stat = await fs.stat(filePath).catch(() => null) 12 | if (!stat?.isFile()) { 13 | console.error(chalk.red(`File ${chalk.bold(filePath)} not found`)) 14 | return 15 | } 16 | const file = await fs.readFile(filePath) 17 | const minSize = (file.length / 1024).toFixed(2) + 'kb' 18 | const [gzipped, compressed] = await Promise.all([ 19 | gzipSync(file), 20 | // 21 | compress(file), 22 | ]) 23 | const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb' 24 | const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb' 25 | console.log( 26 | `${chalk.gray( 27 | chalk.bold(path.basename(filePath)) 28 | )} min:${minSize} / gzip:${gzippedSize} / brotli:${compressedSize}` 29 | ) 30 | } 31 | 32 | ;(async () => { 33 | await Promise.all( 34 | [ 35 | path.resolve( 36 | __dirname, 37 | '../packages/router/size-checks/dist/webRouter.js' 38 | ), 39 | path.resolve( 40 | __dirname, 41 | '../packages/router/dist/vue-router.global.prod.js' 42 | ), 43 | ].map(checkFileSize) 44 | ) 45 | })() 46 | -------------------------------------------------------------------------------- /scripts/docs-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # check if doc files changes for netlify 4 | # needed because we cannot use && in netlify.toml 5 | 6 | # exit 0 will skip the build while exit 1 will build 7 | 8 | # - check any change in docs 9 | # - check for new version of vite related deps 10 | # - changes in netlify conf 11 | # - a commit message that starts with docs like docs: ... or docs(nuxt): ... 12 | 13 | git diff --quiet 'HEAD^' HEAD ./packages/docs/ && ! git diff 'HEAD^' HEAD ./pnpm-lock.yaml | grep --quiet vitepress && git diff --quiet 'HEAD^' HEAD netlify.toml && ! git log -1 --pretty=%B | grep '^docs' 14 | -------------------------------------------------------------------------------- /scripts/verifyCommit.mjs: -------------------------------------------------------------------------------- 1 | // Invoked on the commit-msg git hook by simple-git-hooks. 2 | 3 | import chalk from 'chalk' 4 | import { readFileSync } from 'node:fs' 5 | import path from 'node:path' 6 | 7 | const msgPath = path.resolve('.git/COMMIT_EDITMSG') 8 | const msg = readFileSync(msgPath, 'utf-8').trim() 9 | 10 | const commitRE = 11 | /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?: .{1,50}/ 12 | 13 | if (!commitRE.test(msg)) { 14 | console.log() 15 | console.error( 16 | ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red( 17 | `invalid commit message format.` 18 | )}\n\n` + 19 | chalk.red( 20 | ` Proper commit message format is required for automated changelog generation. Examples:\n\n` 21 | ) + 22 | ` ${chalk.green( 23 | `fix(view): handle keep-alive with aborted navigations` 24 | )}\n` + 25 | ` ${chalk.green( 26 | `fix(view): handle keep-alive with aborted navigations (close #28)` 27 | )}\n\n` + 28 | chalk.red(` See .github/commit-convention.md for more details.\n`) 29 | ) 30 | process.exit(1) 31 | } 32 | -------------------------------------------------------------------------------- /vitest.workspace.js: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from 'vitest/config' 2 | 3 | export default defineWorkspace([ 4 | { 5 | extends: './packages/router/vitest.config.ts', 6 | test: { 7 | name: 'router', 8 | root: './packages/router', 9 | }, 10 | }, 11 | ]) 12 | --------------------------------------------------------------------------------