├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── example ├── async-page.vue ├── index.html ├── index.jsx ├── nav.vue └── vite.config.ts ├── package.json ├── src ├── index.js ├── prefetch.js └── utils.js ├── types └── index.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: yarn 27 | - run: npm run test --if-present 28 | - run: yarn semantic-release 29 | env: 30 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | cypress/videos 4 | cypress/screenshots 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "bracketSpacing": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) EGOIST <0x142857@gmail.com> (https://egoist.sh) 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 | # vue-router-prefetch 2 | 3 | [![NPM version](https://badgen.net/npm/v/vue-router-prefetch)](https://npmjs.com/package/vue-router-prefetch) [![NPM downloads](https://badgen.net/npm/dm/vue-router-prefetch)](https://npmjs.com/package/vue-router-prefetch) [![CircleCI](https://badgen.net/circleci/github/egoist/vue-router-prefetch/master)](https://circleci.com/gh/egoist/vue-router-prefetch/tree/master) 4 | 5 | **Please consider [donating](https://www.patreon.com/egoist) to this project's author, [EGOIST](#author), to show your ❤️ and support.** 6 | 7 | ## Features 8 | 9 | - Prefetch links when they are visible in viewport. 10 | - You don't need to change your code base to make it work. 11 | - Tiny-size. 12 | 13 | ## Install 14 | 15 | ```bash 16 | yarn add vue-router-prefetch 17 | ``` 18 | 19 | If you're using Vue 2, you should install `vue-router-prefetch@1` instead. 20 | 21 | ## Usage 22 | 23 | You need to use this plugin after `vue-router`: 24 | 25 | ```js 26 | import { createApp } from 'vue' 27 | import { createRouter } from 'vue-router' 28 | import RouterPrefetch from 'vue-router-prefetch' 29 | 30 | const app = createApp() 31 | const router = createRouter() 32 | app.use(router) 33 | app.use(RouterPrefetch) 34 | ``` 35 | 36 | Then you can use `` without any changes, when this component is visible in viewport, it will automatically prefetch the (async) route component. 37 | 38 | **Check out the [live demo](https://stackblitz.com/edit/vue-nr9q5u).** 39 | 40 | You can also register it as a new component instead of overriding ``: 41 | 42 | ```js 43 | app.use(RouterPrefetch, { 44 | componentName: 'QuickLink' 45 | }) 46 | ``` 47 | 48 | Now you can use it as `` in your app. 49 | 50 | ## Browser Support 51 | 52 | - Without polyfills: Chrome, Firefox, Edge, Opera, Android Browser, Samsung Internet. 53 | - With [Intersection Observer polyfill](https://github.com/w3c/IntersectionObserver/tree/master/polyfill) ~6KB gzipped/minified: Safari, IE11 54 | 55 | ## Props 56 | 57 | All [props](https://router.vuejs.org/api/#router-link-props) of `` are still available, additional props are listed below. 58 | 59 | ### prefetch 60 | 61 | - Type: `boolean` 62 | - Default: `true` 63 | 64 | Whether to prefetch the matched route component. 65 | 66 | You can also set `meta.prefetch` on vue-router's `route` object to disable prefetching this route for all ``s: 67 | 68 | ```js 69 | createRouter({ 70 | routes: [ 71 | { 72 | path: '/some-async-page', 73 | meta: { prefetch: false }, 74 | component: () => import('./async-page.vue') 75 | } 76 | ] 77 | }) 78 | ``` 79 | 80 | It's also possible to turn of prefetching globally: 81 | 82 | ```js 83 | app.use(RouterPrefetch, { 84 | prefetch: false 85 | }) 86 | ``` 87 | 88 | ### prefetchFiles 89 | 90 | - Type: `string[]` 91 | - Examples: `['/foo.css']` 92 | 93 | A list of additional files to prefetch. By default we only prefetch the route component. 94 | 95 | You can also set `meta.prefetchFiles` on vue-router's `route` object, it will be merged with the prop value: 96 | 97 | ```js 98 | createRouter({ 99 | routes: [ 100 | { 101 | path: '/some-async-page', 102 | meta: { prefetchFiles: ['/foo.css'] }, 103 | component: () => import('./async-page.vue') 104 | } 105 | ] 106 | }) 107 | ``` 108 | 109 | ### timeout 110 | 111 | - Type: `number` 112 | - Default: `2000` (ms) 113 | 114 | Timeout after which prefetching will occur. 115 | 116 | ## Credits 117 | 118 | Inspired by [quicklink](https://github.com/GoogleChromeLabs/quicklink) and [`nuxt-link`](https://github.com/nuxt/nuxt.js/pull/4574/). 119 | 120 | ## Contributing 121 | 122 | 1. Fork it! 123 | 2. Create your feature branch: `git checkout -b my-new-feature` 124 | 3. Commit your changes: `git commit -am 'Add some feature'` 125 | 4. Push to the branch: `git push origin my-new-feature` 126 | 5. Submit a pull request :D 127 | 128 | ## Author 129 | 130 | **vue-router-prefetch** © EGOIST, Released under the [MIT](./LICENSE) License.
131 | Authored and maintained by EGOIST with help from contributors ([list](https://github.com/egoist/vue-router-prefetch/contributors)). 132 | 133 | > [Website](https://egoist.sh) · GitHub [@EGOIST](https://github.com/egoist) · Twitter [@\_egoistlily](https://twitter.com/_egoistlily) 134 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/integration/spec.js: -------------------------------------------------------------------------------- 1 | describe('My First Test', () => { 2 | it('Does not do much!', () => { 3 | cy.visit('http://localhost:3000') 4 | cy.wait(1000) 5 | cy.window().then(({ pages }) => { 6 | expect([...pages]).to.deep.equal([1, 2, 3]) 7 | }) 8 | cy.get('#bottom').then($ => { 9 | $[0].scrollIntoView() 10 | }) 11 | cy.wait(1000) 12 | cy.window().then(({ pages }) => { 13 | expect([...pages]).to.deep.equal([1, 2, 3, 4, 5, 6]) 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = () => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /example/async-page.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/index.jsx: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue' 2 | import { createRouter, createWebHashHistory, RouterView } from 'vue-router' 3 | import RoutePrefetch from '../src' 4 | import Nav from './nav.vue' 5 | 6 | const app = createApp({ 7 | setup() { 8 | return () => h(RouterView) 9 | } 10 | }) 11 | 12 | const router = createRouter({ 13 | history: createWebHashHistory(), 14 | routes: [ 15 | { 16 | path: '/', 17 | component: { 18 | render() { 19 | return ( 20 |
21 |

hi

22 |
24 | ) 25 | } 26 | } 27 | }, 28 | { 29 | path: '/async-page', 30 | meta: { 31 | prefetch(route) { 32 | console.log(route) 33 | } 34 | }, 35 | component: () => import('./async-page.vue') 36 | } 37 | ] 38 | }) 39 | 40 | app.use(router) 41 | app.use(RoutePrefetch) 42 | 43 | window.pages = new Set() 44 | 45 | const createPage = id => async () => { 46 | console.log(`fetching page ${id}`) 47 | window.pages.add(id) 48 | return { 49 | render() { 50 | return ( 51 |
52 |

page {id}

53 |
55 | ) 56 | } 57 | } 58 | } 59 | 60 | for (let i = 0; i < 6; i++) { 61 | router.addRoute({ 62 | path: `/page/${i + 1}`, 63 | component: createPage(i + 1) 64 | }) 65 | } 66 | 67 | app.mount('#app') 68 | -------------------------------------------------------------------------------- /example/nav.vue: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | plugins: [vue()], 6 | esbuild: { 7 | jsxFactory: 'h' 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-router-prefetch", 3 | "version": "1.0.0", 4 | "description": "Prefetch the route components of router-link in viewport.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "types": "types/index.d.ts", 8 | "files": [ 9 | "dist", 10 | "types" 11 | ], 12 | "scripts": { 13 | "test": "npm run lint && npm run test:e2e", 14 | "lint": "xo", 15 | "build": "bili --format esm,cjs,esm-min --minimal", 16 | "prepublishOnly": "npm run build", 17 | "example": "vite example", 18 | "commit": "git-cz", 19 | "cy:run": "cypress run", 20 | "cy:open": "cypress open", 21 | "test:e2e": "run-p --race example cy:run" 22 | }, 23 | "repository": { 24 | "url": "egoist/vue-router-prefetch", 25 | "type": "git" 26 | }, 27 | "author": "egoist<0x142857@gmail.com>", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "@vitejs/plugin-vue": "^1.4.0", 31 | "@vue/compiler-sfc": "^3.1.5", 32 | "babel-eslint": "^10.0.1", 33 | "bili": "^4.0.9", 34 | "commitizen": "^3.0.5", 35 | "cypress": "^8.2.0", 36 | "cz-conventional-changelog": "^2.1.0", 37 | "eslint-config-prettier": "^3.3.0", 38 | "eslint-config-rem": "^4.0.0", 39 | "eslint-plugin-cypress": "^2.2.1", 40 | "eslint-plugin-prettier": "^3.0.0", 41 | "eslint-plugin-vue": "^5.1.0", 42 | "husky": "^1.0.0-rc.13", 43 | "lint-staged": "^7.2.0", 44 | "npm-run-all": "^4.1.5", 45 | "prettier": "^1.15.2", 46 | "semantic-release": "^17.3.0", 47 | "vite": "^2.4.4", 48 | "vue": "^3.2.2", 49 | "vue-router": "^4.0.11", 50 | "xo": "^0.23.0" 51 | }, 52 | "peerDependencies": { 53 | "vue": "^3", 54 | "vue-router": "^4" 55 | }, 56 | "xo": { 57 | "parserOptions": { 58 | "parser": "babel-eslint", 59 | "ecmaVersion": 2017, 60 | "sourceType": "module" 61 | }, 62 | "extends": [ 63 | "rem", 64 | "plugin:vue/recommended", 65 | "plugin:prettier/recommended" 66 | ], 67 | "envs": [ 68 | "browser", 69 | "cypress/globals" 70 | ], 71 | "rules": { 72 | "no-new": "off", 73 | "no-unused-expressions": "off", 74 | "import/no-unassigned-import": "off" 75 | }, 76 | "plugins": [ 77 | "cypress" 78 | ] 79 | }, 80 | "husky": { 81 | "hooks": { 82 | "pre-commit": "lint-staged" 83 | } 84 | }, 85 | "lint-staged": { 86 | "*.js": [ 87 | "xo --fix", 88 | "git add" 89 | ], 90 | "*.{json,md}": [ 91 | "prettier --write", 92 | "git add" 93 | ] 94 | }, 95 | "release": { 96 | "branch": "master" 97 | }, 98 | "config": { 99 | "commitizen": { 100 | "path": "./node_modules/cz-conventional-changelog" 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import prefetch from './prefetch' 2 | import { canPrefetch, supportIntersectionObserver, inBrowser } from './utils' 3 | 4 | function installRouterPrefetch( 5 | /** @type {import('vue').App} */ 6 | app, 7 | { componentName = 'RouterLink', prefetch: enablePrefetch = true } = {} 8 | ) { 9 | const observer = 10 | supportIntersectionObserver && 11 | new window.IntersectionObserver(entries => { 12 | entries.forEach(entry => { 13 | if (entry.isIntersecting) { 14 | entry.target._linkPrefetch() 15 | } 16 | }) 17 | }) 18 | 19 | const requestIdleCallback = 20 | (inBrowser && window.requestIdleCallback) || 21 | function(cb, { timeout = 1 }) { 22 | const start = Date.now() 23 | return setTimeout(() => { 24 | cb({ 25 | didTimeout: false, 26 | timeRemaining() { 27 | return Math.max(0, 50 - (Date.now() - start)) 28 | } 29 | }) 30 | }, timeout) 31 | } 32 | 33 | const RouterLink = app.component('RouterLink') || app.component('router-link') 34 | 35 | if (process.env.NODE_ENV === 'development' && !RouterLink) { 36 | console.error( 37 | `[vue-router-prefetch] You need to call app.use(VueRouter) before this plugin!` 38 | ) 39 | } 40 | 41 | const Link = { 42 | name: componentName, 43 | props: { 44 | ...RouterLink.props, 45 | prefetch: { 46 | type: Boolean, 47 | default: enablePrefetch 48 | }, 49 | prefetchFiles: { 50 | type: Array 51 | }, 52 | timeout: { 53 | type: Number, 54 | default: 2000 55 | } 56 | }, 57 | setup(props, context) { 58 | return RouterLink.setup(props, context) 59 | }, 60 | mounted() { 61 | if (this.prefetch && observer && canPrefetch) { 62 | requestIdleCallback(this.observe, { timeout: this.timeout }) 63 | } 64 | }, 65 | beforeUnmount() { 66 | this.unobserve() 67 | }, 68 | methods: { 69 | observe() { 70 | observer.observe(this.$el) 71 | this.$el._linkPrefetch = this.linkPrefetch 72 | this._linkObserved = true 73 | }, 74 | unobserve() { 75 | if (this._linkObserved) { 76 | observer.unobserve(this.$el) 77 | } 78 | }, 79 | getRouteComponents(route) { 80 | return route.matched 81 | .map(record => { 82 | return Object.values(record.components) 83 | }) 84 | .flat() 85 | .filter(Component => { 86 | return ( 87 | typeof Component === 'function' && Component.cid === undefined 88 | ) 89 | }) 90 | }, 91 | linkPrefetch() { 92 | const route = this.$router.resolve(this.to) 93 | 94 | if (route.meta.__prefetched) return 95 | 96 | route.meta.__prefetched = true 97 | 98 | if (route.meta.prefetch !== false) { 99 | // Prefetch route component 100 | const components = this.getRouteComponents(route) 101 | for (const Component of components) { 102 | this.$emit('prefetch', this.to) 103 | Component() // eslint-disable-line new-cap 104 | } 105 | } 106 | 107 | if (typeof route.meta.prefetch === 'function') { 108 | route.meta.prefetch(route) 109 | } 110 | 111 | // Prefetch addtional files 112 | const prefetchFiles = [ 113 | ...(this.prefetchFiles || []), 114 | ...(route.meta.prefetchFiles || []) 115 | ] 116 | if (prefetchFiles.length > 0) { 117 | for (const file of prefetchFiles) { 118 | prefetch(file) 119 | } 120 | } 121 | 122 | this.unobserve() 123 | } 124 | } 125 | } 126 | 127 | // `app.component(Link.name, Link)` will emit a warning 128 | app._context.components[Link.name] = Link 129 | } 130 | 131 | export { 132 | prefetch, 133 | // Export as `install` to make `app.use(require('vue-router-prefetch'))` work 134 | installRouterPrefetch as install 135 | } 136 | 137 | export default installRouterPrefetch 138 | -------------------------------------------------------------------------------- /src/prefetch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Portions copyright 2018 Google Inc. 3 | * Inspired by Gatsby's prefetching logic, with those portions 4 | * remaining MIT. Additions include support for Fetch API, 5 | * XHR switching, SaveData and Effective Connection Type checking. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | **/ 19 | import { inBrowser, canPrefetch } from './utils' 20 | 21 | const preFetched = {} 22 | 23 | /** 24 | * Checks if a feature on `link` is natively supported. 25 | * Examples of features include `prefetch` and `preload`. 26 | * @param {string} feature - name of the feature to test 27 | * @return {Boolean} whether the feature is supported 28 | */ 29 | function support(feature) { 30 | if (!inBrowser) { 31 | return 32 | } 33 | const link = document.createElement('link') 34 | return link.relList && link.relList.supports && link.relList.supports(feature) 35 | } 36 | 37 | /** 38 | * Fetches a given URL using `` 39 | * @param {string} url - the URL to fetch 40 | * @return {Object} a Promise 41 | */ 42 | function linkPrefetchStrategy(url) { 43 | return new Promise((resolve, reject) => { 44 | const link = document.createElement(`link`) 45 | link.rel = `prefetch` 46 | link.href = url 47 | 48 | link.addEventListener('load', resolve) 49 | link.addEventListener('error', reject) 50 | 51 | document.head.appendChild(link) 52 | }) 53 | } 54 | 55 | /** 56 | * Fetches a given URL using XMLHttpRequest 57 | * @param {string} url - the URL to fetch 58 | * @return {Object} a Promise 59 | */ 60 | function xhrPrefetchStrategy(url) { 61 | return new Promise((resolve, reject) => { 62 | const req = new XMLHttpRequest() 63 | 64 | req.open(`GET`, url, (req.withCredentials = true)) 65 | 66 | req.addEventListener('load', () => { 67 | req.status === 200 ? resolve() : reject() 68 | }) 69 | 70 | req.send() 71 | }) 72 | } 73 | 74 | /** 75 | * Fetches a given URL using the Fetch API. Falls back 76 | * to XMLHttpRequest if the API is not supported. 77 | * @param {string} url - the URL to fetch 78 | * @return {Object} a Promise 79 | */ 80 | function highPriFetchStrategy(url) { 81 | // TODO: Investigate using preload for high-priority 82 | // fetches. May have to sniff file-extension to provide 83 | // valid 'as' values. In the future, we may be able to 84 | // use Priority Hints here. 85 | // 86 | // As of 2018, fetch() is high-priority in Chrome 87 | // and medium-priority in Safari. 88 | return self.fetch 89 | ? fetch(url, { credentials: `include` }) 90 | : xhrPrefetchStrategy(url) 91 | } 92 | 93 | const supportedPrefetchStrategy = support('prefetch') 94 | ? linkPrefetchStrategy 95 | : xhrPrefetchStrategy 96 | 97 | /** 98 | * Prefetch a given URL with an optional preferred fetch priority 99 | * @param {String} url - the URL to fetch 100 | * @param {Boolean} isPriority - if is "high" priority 101 | * @param {Object} conn - navigator.connection (internal) 102 | * @return {Object} a Promise 103 | */ 104 | function prefetcher(url, isPriority) { 105 | if (!canPrefetch || preFetched[url]) { 106 | return 107 | } 108 | 109 | // Wanna do something on catch()? 110 | return (isPriority ? highPriFetchStrategy : supportedPrefetchStrategy)( 111 | url 112 | ).then(() => { 113 | preFetched[url] = true 114 | }) 115 | } 116 | 117 | export default prefetcher 118 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const inBrowser = typeof window !== 'undefined' 2 | 3 | const conn = inBrowser && navigator.connection 4 | export const canPrefetch = 5 | inBrowser && 6 | (!conn || ((conn.effectiveType || '').indexOf('2g') === -1 && !conn.saveData)) 7 | 8 | export const supportIntersectionObserver = 9 | inBrowser && window.IntersectionObserver 10 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-router-prefetch' { 2 | export interface RouterPrefetchOptions { 3 | componentName?: string 4 | prefetch?: boolean 5 | } 6 | 7 | const install: ( 8 | app: import('vue').App, 9 | options?: RouterPrefetchOptions 10 | ) => void 11 | 12 | export default install 13 | } 14 | --------------------------------------------------------------------------------