├── .circleci └── config.yml ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh-cn.md ├── jest.config.js ├── package.json ├── rollup.config.js ├── scripts ├── build.js └── release.js ├── src └── index.ts ├── test └── index.spec.ts ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | step_restore_cache: &restore_cache 4 | restore_cache: 5 | keys: 6 | - v1-dependencies-{{ checksum "yarn.lock" }}-1 7 | - v1-dependencies- 8 | 9 | step_install_deps: &install_deps 10 | run: 11 | name: Install Dependencies 12 | command: yarn --frozen-lockfile 13 | 14 | step_save_cache: &save_cache 15 | save_cache: 16 | paths: 17 | - node_modules 18 | - ~/.cache/yarn 19 | key: v1-dependencies-{{ checksum "yarn.lock" }}-1 20 | 21 | jobs: 22 | test: 23 | docker: 24 | - image: circleci/node:12 25 | steps: 26 | - checkout 27 | - *restore_cache 28 | - *install_deps 29 | - *save_cache 30 | - run: yarn test 31 | 32 | workflows: 33 | version: 2 34 | ci: 35 | jobs: 36 | - test 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [14, 15] 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - name: install and lint 18 | run: | 19 | yarn install 20 | yarn lint:fail 21 | 22 | test: 23 | needs: [lint] 24 | runs-on: ubuntu-latest 25 | strategy: 26 | matrix: 27 | node-version: [14, 15] 28 | steps: 29 | - uses: actions/checkout@v1 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - name: install and test 35 | run: | 36 | yarn install 37 | yarn jest --coverage 38 | - name: upload coverage 39 | uses: codecov/codecov-action@v1 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | .DS_Store 5 | yarn-error.log 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 80 2 | semi: false 3 | singleQuote: true 4 | trailingComma: none 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [5.0.0](https://github.com/vuejs/vuex-router-sync/compare/v4.1.0...v5.0.0) (2017-10-12) 2 | 3 | ### Features 4 | 5 | * add API for unsync([#66](https://github.com/vuejs/vuex-router-sync/issues/66)) ([693aa76](https://github.com/vuejs/vuex-router-sync/commit/693aa761f9e47df7f07c49c799c627b86d094402)), closes [#64](https://github.com/vuejs/vuex-router-sync/issues/64) 6 | 7 | # [4.1.0](https://github.com/vuejs/vuex-router-sync/compare/v4.0.1...v4.1.0) (2017-01-02) 8 | 9 | ## [4.0.1](https://github.com/vuejs/vuex-router-sync/compare/v4.0.0...v4.0.1) (2016-12-14) 10 | 11 | # [4.0.0](https://github.com/vuejs/vuex-router-sync/compare/v2.0.0...v4.0.0) (2016-12-14) 12 | 13 | # 2.0.0 (2016-06-30) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-present Evan You 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vuex Router Sync 2 | 3 | [![npm](https://img.shields.io/npm/v/vuex-router-sync.svg)](https://npmjs.com/package/vuex-router-sync) 4 | [![ci status](https://github.com/vuejs/vuex-router-sync/workflows/test/badge.svg)](https://github.com/vuejs/vuex-router-sync/actions) 5 | [![coverage](https://codecov.io/gh/vuejs/vuex-router-sync/branch/master/graph/badge.svg?token=4KJug3I5do)](https://codecov.io/gh/vuejs/vuex-router-sync) 6 | [![license](https://img.shields.io/npm/l/vuex-router-sync.svg?sanitize=true)](http://opensource.org/licenses/MIT) 7 | 8 | Sync Vue Router's current `$route` as part of Vuex store's state. 9 | 10 | [中文版本 (Chinese Version)](README.zh-cn.md) 11 | 12 | ## Usage 13 | 14 | ``` bash 15 | # the latest version works only with vue-router >= 2.0 16 | npm install vuex-router-sync 17 | 18 | # for usage with vue-router < 2.0: 19 | npm install vuex-router-sync@2 20 | ``` 21 | 22 | ``` js 23 | import { sync } from 'vuex-router-sync' 24 | import store from './store' // vuex store instance 25 | import router from './router' // vue-router instance 26 | 27 | const unsync = sync(store, router) // done. Returns an unsync callback fn 28 | 29 | // bootstrap your app... 30 | 31 | // During app/Vue teardown (e.g., you only use Vue.js in a portion of your app 32 | // and you navigate away from that portion and want to release/destroy 33 | // Vue components/resources) 34 | unsync() // Unsyncs store from router 35 | ``` 36 | 37 | You can optionally set a custom vuex module name: 38 | 39 | ```js 40 | sync(store, router, { moduleName: 'RouteModule' } ) 41 | ``` 42 | 43 | ## How does it work? 44 | 45 | - It adds a `route` module into the store, which contains the state representing the current route: 46 | 47 | ``` js 48 | store.state.route.path // current path (string) 49 | store.state.route.params // current params (object) 50 | store.state.route.query // current query (object) 51 | ``` 52 | 53 | - When the router navigates to a new route, the store's state is updated. 54 | 55 | - **`store.state.route` is immutable, because it is derived state from the URL, which is the source of truth**. You should not attempt to trigger navigations by mutating the route object. Instead, just call `$router.push()` or `$router.go()`. Note that you can do `$router.push({ query: {...}})` to update the query string on the current path. 56 | 57 | ## License 58 | 59 | [MIT](http://opensource.org/licenses/MIT) 60 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | # Vuex Router Sync 2 | 3 | [![npm](https://img.shields.io/npm/v/vuex-router-sync.svg)](https://npmjs.com/package/vuex-router-sync) 4 | [![ci status](https://github.com/vuejs/vuex-router-sync/workflows/test/badge.svg)](https://github.com/vuejs/vuex-router-sync/actions) 5 | [![coverage](https://codecov.io/gh/vuejs/vuex-router-sync/branch/master/graph/badge.svg?token=4KJug3I5do)](https://codecov.io/gh/vuejs/vuex-router-sync) 6 | [![license](https://img.shields.io/npm/l/vuex-router-sync.svg?sanitize=true)](http://opensource.org/licenses/MIT) 7 | 8 | 把 Vue Router 当前的 `$route` 同步为 Vuex 状态的一部分。 9 | 10 | [English](README.md) 11 | 12 | ### 用法 13 | 14 | ``` 15 | # 最新版本需要配合 vue-router 2.0 及以上的版本使用 16 | npm install vuex-router-sync 17 | # 用于版本低于 2.0 的 vue-router 18 | npm install vuex-router-sync@2 19 | ``` 20 | 21 | ```javascript 22 | import { sync } from 'vuex-router-sync' 23 | import store from './vuex/store' // vuex store 实例 24 | import router from './router' // vue-router 实例 25 | 26 | const unsync = sync(store, router) // 返回值是 unsync 回调方法 27 | 28 | // 在这里写你的代码 29 | 30 | // 在 Vue 应用销毁时 (比如在仅部分场景使用 Vue 的应用中跳出该场景且希望销毁 Vue 的组件/资源时) 31 | unsync() // 取消 store 和 router 中间的同步 32 | ``` 33 | 34 | 你可以有选择地设定一个自定义的 vuex 模块名: 35 | 36 | ```javascript 37 | sync(store, router, { moduleName: 'RouteModule' } ) 38 | ``` 39 | 40 | ### 工作原理 41 | 42 | - 该库在 store 上增加了一个名为 `route` 的模块,用于表示当前路由的状态。 43 | 44 | ```javascript 45 | store.state.route.path // current path (字符串类型) 46 | store.state.route.params // current params (对象类型) 47 | store.state.route.query // current query (对象类型) 48 | ``` 49 | 50 | - 当被导航到一个新路由时,store 的状态会被更新。 51 | 52 | - **`store.state.route` 是不可变更的,因为该值取自 URL,是真实的来源**。你不应该通过修改该值去触发浏览器的导航行为。取而代之的是调用 `$router.push()` 或者 `$router.go()`。另外,你可以通过 `$router.push({ query: {...}})` 来更新当前路径的查询字符串。 53 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | rootDir: __dirname, 4 | moduleNameMapper: { 5 | '^@/(.*)$': '/src/$1', 6 | '^test/(.*)$': '/test/$1' 7 | }, 8 | testMatch: ['/test/**/*.spec.ts'], 9 | testPathIgnorePatterns: ['/node_modules/'], 10 | coverageDirectory: 'coverage', 11 | coverageReporters: ['html', 'json', 'lcov', 'text-summary'], 12 | collectCoverageFrom: [ 13 | 'src/**/*.ts' 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuex-router-sync", 3 | "version": "5.0.0", 4 | "description": "Effortlessly keep vue-router and vuex store in sync.", 5 | "main": "dist/vuex-router-sync.js", 6 | "browser": "dist/vuex-router-sync.esm-browser.js", 7 | "module": "dist/vuex-router-sync.esm-bundler.js", 8 | "unpkg": "dist/vuex-router-sync.global.js", 9 | "jsdelivr": "dist/vuex-router-sync.global.js", 10 | "typings": "dist/src/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "build": "node scripts/build.js", 16 | "lint": "prettier --check --write --parser typescript \"{src,test}/**/*.ts\"", 17 | "lint:fail": "prettier --check --parser typescript \"{src,test}/**/*.ts\"", 18 | "test": "jest", 19 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 20 | "release": "node scripts/release.js" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/vuejs/vuex-router-sync.git" 25 | }, 26 | "keywords": [ 27 | "vuex", 28 | "vue-router", 29 | "vue" 30 | ], 31 | "author": "Evan You", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/vuejs/vuex-router-sync/issues" 35 | }, 36 | "homepage": "https://github.com/vuejs/vuex-router-sync#readme", 37 | "peerDependencies": { 38 | "vue-router": "^3.0.0", 39 | "vuex": "^3.0.0" 40 | }, 41 | "devDependencies": { 42 | "@rollup/plugin-commonjs": "^17.1.0", 43 | "@rollup/plugin-node-resolve": "^11.1.1", 44 | "@types/jest": "^26.0.20", 45 | "brotli": "^1.3.2", 46 | "buble": "^0.20.0", 47 | "chalk": "^4.1.0", 48 | "conventional-changelog-cli": "^2.1.1", 49 | "enquirer": "^2.3.6", 50 | "execa": "^5.0.0", 51 | "fs-extra": "^9.1.0", 52 | "jest": "^26.6.3", 53 | "prettier": "^2.2.1", 54 | "rollup": "^2.38.4", 55 | "rollup-plugin-terser": "^7.0.2", 56 | "rollup-plugin-typescript2": "^0.29.0", 57 | "semver": "^7.3.4", 58 | "ts-jest": "^26.5.0", 59 | "tslib": "^2.1.0", 60 | "typescript": "3.9.7", 61 | "vue": "^2.6.12", 62 | "vue-router": "^3.5.1", 63 | "vuex": "^3.6.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import resolve from '@rollup/plugin-node-resolve' 3 | import commonjs from '@rollup/plugin-commonjs' 4 | import ts from 'rollup-plugin-typescript2' 5 | import { terser } from 'rollup-plugin-terser' 6 | import pkg from './package.json' 7 | 8 | const banner = `/*! 9 | /** 10 | * vuex-router-sync v${pkg.version} 11 | * (c) ${new Date().getFullYear()} Evan You 12 | * @license MIT 13 | */` 14 | 15 | const configs = [ 16 | { file: 'dist/vuex-router-sync.esm-browser.js', format: 'es', browser: true, env: 'development' }, 17 | { file: 'dist/vuex-router-sync.esm-browser.prod.js', format: 'es', browser: true, env: 'production' }, 18 | { file: 'dist/vuex-router-sync.esm-bundler.js', format: 'es', env: 'development' }, 19 | { file: 'dist/vuex-router-sync.global.js', format: 'iife', env: 'development' }, 20 | { file: 'dist/vuex-router-sync.global.prod.js', format: 'iife', minify: true, env: 'production' }, 21 | { file: 'dist/vuex-router-sync.cjs.js', format: 'cjs', env: 'development' } 22 | ] 23 | 24 | export function createEntries() { 25 | return configs.map((c) => createEntry(c)) 26 | } 27 | 28 | function createEntry(config) { 29 | const c = { 30 | input: 'src/index.ts', 31 | plugins: [], 32 | output: { 33 | banner, 34 | file: config.file, 35 | format: config.format 36 | }, 37 | onwarn: (msg, warn) => { 38 | if (!/Circular/.test(msg)) { 39 | warn(msg) 40 | } 41 | } 42 | } 43 | 44 | if (config.format === 'iife' || config.format === 'umd') { 45 | c.output.name = c.output.name || 'VuexRouterSync' 46 | } 47 | 48 | c.plugins.push(resolve()) 49 | c.plugins.push(commonjs()) 50 | 51 | c.plugins.push(ts({ 52 | check: config.format === 'es' && config.browser && config.env === 'development', 53 | tsconfig: path.resolve(__dirname, 'tsconfig.json'), 54 | cacheRoot: path.resolve(__dirname, 'node_modules/.rts2_cache'), 55 | tsconfigOverride: { 56 | compilerOptions: { 57 | declaration: config.format === 'es' && config.browser && config.env === 'development', 58 | target: config.format === 'iife' || config.format === 'cjs' ? 'es5' : 'es2018' 59 | }, 60 | exclude: ['test'] 61 | } 62 | })) 63 | 64 | if (config.minify) { 65 | c.plugins.push(terser({ module: config.format === 'es' })) 66 | } 67 | 68 | return c 69 | } 70 | 71 | export default createEntries() 72 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const chalk = require('chalk') 3 | const execa = require('execa') 4 | const { gzipSync } = require('zlib') 5 | const { compress } = require('brotli') 6 | 7 | const files = [ 8 | 'dist/vuex-router-sync.esm-browser.js', 9 | 'dist/vuex-router-sync.esm-browser.prod.js', 10 | 'dist/vuex-router-sync.esm-bundler.js', 11 | 'dist/vuex-router-sync.global.js', 12 | 'dist/vuex-router-sync.global.prod.js', 13 | 'dist/vuex-router-sync.cjs.js' 14 | ] 15 | 16 | async function run() { 17 | await build() 18 | checkAllSizes() 19 | } 20 | 21 | async function build(config) { 22 | await execa('rollup', ['-c', 'rollup.config.js'], { stdio: 'inherit' }) 23 | } 24 | 25 | function checkAllSizes() { 26 | console.log() 27 | files.map((f) => checkSize(f)) 28 | console.log() 29 | } 30 | 31 | function checkSize(file) { 32 | const f = fs.readFileSync(file) 33 | const minSize = (f.length / 1024).toFixed(2) + 'kb' 34 | const gzipped = gzipSync(f) 35 | const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb' 36 | const compressed = compress(f) 37 | const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb' 38 | console.log( 39 | `${chalk.gray( 40 | chalk.bold(file) 41 | )} size:${minSize} / gzip:${gzippedSize} / brotli:${compressedSize}` 42 | ) 43 | } 44 | 45 | run() 46 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const chalk = require('chalk') 4 | const semver = require('semver') 5 | const { prompt } = require('enquirer') 6 | const execa = require('execa') 7 | const currentVersion = require('../package.json').version 8 | 9 | const versionIncrements = [ 10 | 'patch', 11 | 'minor', 12 | 'major' 13 | ] 14 | 15 | const tags = [ 16 | 'latest', 17 | 'next' 18 | ] 19 | 20 | const inc = (i) => semver.inc(currentVersion, i) 21 | const bin = (name) => path.resolve(__dirname, `../node_modules/.bin/${name}`) 22 | const run = (bin, args, opts = {}) => execa(bin, args, { stdio: 'inherit', ...opts }) 23 | const step = (msg) => console.log(chalk.cyan(msg)) 24 | 25 | async function main() { 26 | let targetVersion 27 | 28 | const { release } = await prompt({ 29 | type: 'select', 30 | name: 'release', 31 | message: 'Select release type', 32 | choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom']) 33 | }) 34 | 35 | if (release === 'custom') { 36 | targetVersion = (await prompt({ 37 | type: 'input', 38 | name: 'version', 39 | message: 'Input custom version', 40 | initial: currentVersion 41 | })).version 42 | } else { 43 | targetVersion = release.match(/\((.*)\)/)[1] 44 | } 45 | 46 | if (!semver.valid(targetVersion)) { 47 | throw new Error(`Invalid target version: ${targetVersion}`) 48 | } 49 | 50 | const { tag } = await prompt({ 51 | type: 'select', 52 | name: 'tag', 53 | message: 'Select tag type', 54 | choices: tags 55 | }) 56 | 57 | console.log(tag) 58 | 59 | const { yes: tagOk } = await prompt({ 60 | type: 'confirm', 61 | name: 'yes', 62 | message: `Releasing v${targetVersion} with the "${tag}" tag. Confirm?` 63 | }) 64 | 65 | if (!tagOk) { 66 | return 67 | } 68 | 69 | // Run tests before release. 70 | step('\nRunning tests...') 71 | await run('yarn', ['test']) 72 | 73 | // Update the package version. 74 | step('\nUpdating the package version...') 75 | updatePackage(targetVersion) 76 | 77 | // Build the package. 78 | step('\nBuilding the package...') 79 | await run('yarn', ['build']) 80 | 81 | // Generate the changelog. 82 | step('\nGenerating the changelog...') 83 | await run('yarn', ['changelog']) 84 | 85 | const { yes: changelogOk } = await prompt({ 86 | type: 'confirm', 87 | name: 'yes', 88 | message: `Changelog generated. Does it look good?` 89 | }) 90 | 91 | if (!changelogOk) { 92 | return 93 | } 94 | 95 | // Commit changes to the Git. 96 | step('\nCommitting changes...') 97 | await run('git', ['add', '-A']) 98 | await run('git', ['commit', '-m', `release: v${targetVersion}`]) 99 | 100 | // Publish the package. 101 | step('\nPublishing the package...') 102 | await run ('yarn', [ 103 | 'publish', '--tag', tag, '--new-version', targetVersion, '--no-commit-hooks', 104 | '--no-git-tag-version' 105 | ]) 106 | 107 | // Push to GitHub. 108 | step('\nPushing to GitHub...') 109 | await run('git', ['tag', `v${targetVersion}`]) 110 | await run('git', ['push', 'origin', `refs/tags/v${targetVersion}`]) 111 | await run('git', ['push']) 112 | } 113 | 114 | function updatePackage(version) { 115 | const pkgPath = path.resolve(path.resolve(__dirname, '..'), 'package.json') 116 | const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) 117 | 118 | pkg.version = version 119 | 120 | fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n') 121 | } 122 | 123 | main().catch((err) => console.error(err)) 124 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex' 2 | import VueRouter, { Route } from 'vue-router' 3 | 4 | export interface SyncOptions { 5 | moduleName: string 6 | } 7 | 8 | export interface State { 9 | name?: string | null 10 | path: string 11 | hash: string 12 | query: Record 13 | params: Record 14 | fullPath: string 15 | meta?: any 16 | from?: Omit 17 | } 18 | 19 | export interface Transition { 20 | to: Route 21 | from: Route 22 | } 23 | 24 | export function sync( 25 | store: Store, 26 | router: VueRouter, 27 | options?: SyncOptions 28 | ): () => void { 29 | const moduleName = (options || {}).moduleName || 'route' 30 | 31 | store.registerModule(moduleName, { 32 | namespaced: true, 33 | state: cloneRoute(router.currentRoute), 34 | mutations: { 35 | ROUTE_CHANGED(_state: State, transition: Transition): void { 36 | store.state[moduleName] = cloneRoute(transition.to, transition.from) 37 | } 38 | } 39 | }) 40 | 41 | let isTimeTraveling: boolean = false 42 | let currentPath: string 43 | 44 | // sync router on store change 45 | const storeUnwatch = store.watch( 46 | (state) => state[moduleName], 47 | (route: Route) => { 48 | const { fullPath } = route 49 | if (fullPath === currentPath) { 50 | return 51 | } 52 | if (currentPath != null) { 53 | isTimeTraveling = true 54 | router.push(route as any) 55 | } 56 | currentPath = fullPath 57 | }, 58 | { sync: true } as any 59 | ) 60 | 61 | // sync store on router navigation 62 | const afterEachUnHook = router.afterEach((to, from) => { 63 | if (isTimeTraveling) { 64 | isTimeTraveling = false 65 | return 66 | } 67 | currentPath = to.fullPath 68 | store.commit(moduleName + '/ROUTE_CHANGED', { to, from }) 69 | }) 70 | 71 | return function unsync(): void { 72 | // On unsync, remove router hook 73 | if (afterEachUnHook != null) { 74 | afterEachUnHook() 75 | } 76 | 77 | // On unsync, remove store watch 78 | if (storeUnwatch != null) { 79 | storeUnwatch() 80 | } 81 | 82 | // On unsync, unregister Module with store 83 | store.unregisterModule(moduleName) 84 | } 85 | } 86 | 87 | function cloneRoute(to: Route, from?: Route): State { 88 | const clone: State = { 89 | name: to.name, 90 | path: to.path, 91 | hash: to.hash, 92 | query: to.query, 93 | params: to.params, 94 | fullPath: to.fullPath, 95 | meta: to.meta 96 | } 97 | 98 | if (from) { 99 | clone.from = cloneRoute(from) 100 | } 101 | 102 | return Object.freeze(clone) 103 | } 104 | -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex, { mapState } from 'vuex' 3 | import VueRouter from 'vue-router' 4 | import { sync } from '@/index' 5 | 6 | Vue.use(Vuex) 7 | Vue.use(VueRouter) 8 | 9 | function run(originalModuleName: string, done: Function): void { 10 | const moduleName: string = originalModuleName || 'route' 11 | 12 | const store = new Vuex.Store({ 13 | state: { msg: 'foo' } 14 | }) 15 | 16 | const Home = Vue.extend({ 17 | computed: mapState(moduleName, { 18 | path: (state: any) => state.fullPath, 19 | foo: (state: any) => state.params.foo, 20 | bar: (state: any) => state.params.bar 21 | }), 22 | render(h) { 23 | return h('div', [this.path, ' ', this.foo, ' ', this.bar]) 24 | } 25 | }) 26 | 27 | const router = new VueRouter({ 28 | mode: 'abstract', 29 | routes: [{ path: '/:foo/:bar', component: Home }] 30 | }) 31 | 32 | sync(store, router, { 33 | moduleName: originalModuleName 34 | }) 35 | 36 | router.push('/a/b') 37 | expect((store.state as any)[moduleName].fullPath).toBe('/a/b') 38 | expect((store.state as any)[moduleName].params).toEqual({ 39 | foo: 'a', 40 | bar: 'b' 41 | }) 42 | 43 | const app = new Vue({ 44 | store, 45 | router, 46 | render: (h) => h('router-view') 47 | }).$mount() 48 | 49 | expect(app.$el.textContent).toBe('/a/b a b') 50 | 51 | router.push('/c/d?n=1#hello') 52 | expect((store.state as any)[moduleName].fullPath).toBe('/c/d?n=1#hello') 53 | expect((store.state as any)[moduleName].params).toEqual({ 54 | foo: 'c', 55 | bar: 'd' 56 | }) 57 | expect((store.state as any)[moduleName].query).toEqual({ n: '1' }) 58 | expect((store.state as any)[moduleName].hash).toEqual('#hello') 59 | 60 | Vue.nextTick(() => { 61 | expect(app.$el.textContent).toBe('/c/d?n=1#hello c d') 62 | done() 63 | }) 64 | } 65 | 66 | test('default usage', (done) => { 67 | run('', done) 68 | }) 69 | 70 | test('with custom moduleName', (done) => { 71 | run('moduleName', done) 72 | }) 73 | 74 | test('unsync', (done) => { 75 | const store = new Vuex.Store({}) 76 | spyOn(store, 'watch').and.callThrough() 77 | 78 | const router = new VueRouter() 79 | 80 | const moduleName = 'testDesync' 81 | const unsync = sync(store, router, { 82 | moduleName: moduleName 83 | }) 84 | 85 | expect(unsync).toBeInstanceOf(Function) 86 | 87 | // Test module registered, store watched, router hooked 88 | expect((store as any).state[moduleName]).toBeDefined() 89 | expect((store as any).watch).toHaveBeenCalled() 90 | expect((store as any)._watcherVM).toBeDefined() 91 | expect((store as any)._watcherVM._watchers).toBeDefined() 92 | expect((store as any)._watcherVM._watchers.length).toBe(1) 93 | expect((router as any).afterHooks).toBeDefined() 94 | expect((router as any).afterHooks.length).toBe(1) 95 | 96 | // Now unsync vuex-router-sync 97 | unsync() 98 | 99 | // Ensure router unhooked, store-unwatched, module unregistered 100 | expect((router as any).afterHooks.length).toBe(0) 101 | expect((store as any)._watcherVm).toBeUndefined() 102 | expect((store as any).state[moduleName]).toBeUndefined() 103 | 104 | done() 105 | }) 106 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "importHelpers": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "baseUrl": ".", 11 | "rootDir": ".", 12 | "outDir": "dist", 13 | "sourceMap": true, 14 | "declaration": true, 15 | "declarationMap": true, 16 | "removeComments": false, 17 | "noImplicitAny": true, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "strictNullChecks": true, 23 | 24 | "lib": [ 25 | "esnext", 26 | "dom" 27 | ], 28 | 29 | "paths": { 30 | "@/*": ["src/*"], 31 | "test/*": ["test/*"] 32 | }, 33 | 34 | "types": [ 35 | "node", 36 | "jest" 37 | ] 38 | }, 39 | 40 | "include": [ 41 | "src", 42 | "test" 43 | ] 44 | } 45 | --------------------------------------------------------------------------------