├── .eslintignore ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .prettierrc.yml ├── public ├── test1.html ├── test2.html ├── test3.html └── index.html ├── LICENSE ├── rollup.config.js ├── package.json ├── .eslintrc.yml ├── types └── index.d.ts ├── src └── index.js ├── README.md └── README_EN.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | public/dist 3 | node_modules -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .husky 2 | node_modules 3 | public 4 | src 5 | 6 | .eslintignore 7 | .eslintrc.yml 8 | .gitignore 9 | .prettierrc.yml 10 | package-lock.json 11 | rollup.config.js 12 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | # .prettierrc or .prettierrc.yaml 2 | trailingComma: none # 对象或数组最后是否添加逗号 es5|none|all 3 | tabWidth: 2 # 缩进 4 | printWidth: 160 # 多少个字符后换行 5 | semi: false # 结尾处是否添加分号 6 | singleQuote: true # 是否使用单引号 7 | # always:(x) => x 8 | # avoid:x => x 9 | arrowParens: always # 箭头函数在只有一个参数时 10 | -------------------------------------------------------------------------------- /public/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | test page 1 8 | 9 | 10 | test page 1 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | test page 2 8 | 9 | 10 | test page 2 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | test page 3 8 | 9 | 10 | test page 3 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Prefetch-Page 8 | 14 | 15 | 20 | 21 | 22 | javascript:void(0); 23 | test page 1 24 | test page 2 25 | test page 3 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lete乐特 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. -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import serve from 'rollup-plugin-serve' 2 | import { terser } from 'rollup-plugin-terser' 3 | import { babel } from '@rollup/plugin-babel' 4 | 5 | const production = !process.env.ROLLUP_WATCH 6 | const plugins = [ 7 | // Start a local service 8 | !production && serve({ contentBase: 'public', port: 6870 }), 9 | 10 | // building minify 11 | production && terser(), 12 | 13 | // building babel 14 | babel({ babelHelpers: 'bundled', presets: ['@babel/preset-env'] }) 15 | ] 16 | 17 | export default [ 18 | { 19 | input: 'src/index.js', 20 | output: { 21 | format: 'iife', 22 | name: 'prefetch', 23 | file: production ? 'dist/prefetch.js' : 'public/dist/prefetch.js' 24 | }, 25 | plugins 26 | }, 27 | { 28 | input: 'src/index.js', 29 | output: { 30 | format: 'esm', 31 | name: 'prefetch', 32 | file: production ? 'dist/prefetchm.js' : 'public/dist/prefetchm.js' 33 | } 34 | }, 35 | { 36 | input: 'src/index.js', 37 | output: { 38 | exports: 'auto', 39 | format: 'cjs', 40 | name: 'prefetch', 41 | file: production ? 'dist/prefetch.cjs' : 'public/dist/prefetch.cjs' 42 | } 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prefetch-page", 3 | "version": "1.0.0-beta.6", 4 | "description": "浏览器在空闲时预加载可见区域的超链接,以加速后续页面的加载速度 | Browser prefetchs visible area hyperlinks at idle time to speed up subsequent page loads", 5 | "main": "dist/prefetch.cjs.js", 6 | "module": "dist/prefetch.esm.js", 7 | "unpkg": "dist/prefetch.js", 8 | "jsdelivr": "dist/prefetch.js", 9 | "typings": "types/index.d.ts", 10 | "scripts": { 11 | "dev": "rollup -c -w", 12 | "build": "rollup -c", 13 | "prepare": "husky install", 14 | "lint": "npx eslint src && npx prettier --check src", 15 | "lint:fix": "npx eslint --fix src && npx prettier --check --write src" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/Lete114/prefetch-page.git" 20 | }, 21 | "keywords": [ 22 | "precache", 23 | "prefetch", 24 | "prefetcher", 25 | "IntersectionObserver" 26 | ], 27 | "author": "Lete乐特", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/Lete114/prefetch-page/issues" 31 | }, 32 | "homepage": "https://github.com/Lete114/prefetch-page#readme", 33 | "devDependencies": { 34 | "@babel/core": "^7.17.12", 35 | "@babel/preset-env": "^7.17.12", 36 | "@rollup/plugin-babel": "^5.3.1", 37 | "eslint": "^8.15.0", 38 | "husky": "^8.0.1", 39 | "lint-staged": "^12.4.1", 40 | "prettier": "^2.6.2", 41 | "rollup": "^2.73.0", 42 | "rollup-plugin-serve": "^1.1.0", 43 | "rollup-plugin-terser": "^7.0.2" 44 | }, 45 | "lint-staged": { 46 | "*.js": "npx eslint --fix", 47 | "*": "npx prettier --check --write" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | es6: true 4 | browser: true 5 | extends: eslint:recommended 6 | parserOptions: 7 | ecmaVersion: latest 8 | sourceType: module 9 | 10 | rules: 11 | # off 或 0 - 关闭规则 12 | # warn 或 1 - 开启规则, 使用警告 程序不会退出 13 | # error 或 2 - 开启规则, 使用错误 程序退出 14 | 15 | no-console: error # 禁用console 16 | no-var: error ## 禁止使用var 17 | quotes: [error, single, { avoidEscape: true }] ## 使用单引号 18 | indent: [error, 2, { SwitchCase: 1 }] ## 缩进必须为2格 19 | comma-dangle: [error, never] ## 禁止末尾逗号 20 | semi: [error, never] ## 禁止末尾有分号 21 | arrow-parens: [error, always] ## 箭头函数的参数使用圆括号 22 | array-bracket-spacing: [error, never] ## 禁止在数组括号内出现空格 23 | brace-style: error ## 强制 one true brace style 24 | camelcase: warn ## 强制属性名称为驼峰风格 25 | computed-property-spacing: [error, never] ## 禁止在计算属性内使用空格 26 | curly: [error, multi-line] ## https://cn.eslint.org/docs/rules/curly#multi 27 | eol-last: [error, always] ## 强制使用换行 (LF) 28 | eqeqeq: [error, smart] ## https://cn.eslint.org/docs/rules/eqeqeq#smart 29 | max-depth: [error, 3] ## 强制块语句的最大可嵌套深度 30 | max-len: [warn, 160] ## 强制行的最大长度 31 | max-statements: [warn, 20] ## 限制函数块中的语句的最大数量 32 | new-cap: [warn, { capIsNew: false }] ## 要求构造函数首字母大写 33 | no-extend-native: error ## 禁止扩展原生类型 34 | no-mixed-spaces-and-tabs: error ## 禁止空格和 tab 的混合缩进 35 | no-trailing-spaces: error ## 禁用行尾空格 36 | no-unused-vars: warn ## 禁止出现未使用过的变量 37 | no-use-before-define: [error, nofunc] ## 禁止在变量定义之前使用它们 38 | object-curly-spacing: [error, always] ## 不允许花括号中有空格 39 | keyword-spacing: [error, { before: true, after: true }] ## 强制在关键字前后使用一致的空格 40 | space-unary-ops: error ## 禁止在一元操作符之前或之后存在空格 41 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/Lete114/prefetch-page 3 | * Example: 4 | * prefetch({ el: '#box' }) // Select all a tag links under the box tag whose id attribute is 5 | * prefetch({ el: document.body }) 6 | * prefetch({ origins: '*' }) // Allows all origins to be prefetched 7 | * prefetch({ origins: ['example.com', 'www.example.com', 'blog.example.com'] }) 8 | * prefetch({ limit: 10 }) 9 | * prefetch({ threshold: 25 }) // Trigger prefetching after entering 25% of the visible area 10 | * prefetch({ delay: 1000 }) // Prefetch is triggered after staying in the visible area for 1 second 11 | * prefetch({ customs: ['markdown.js', '404.html', 'https://www.example.com'] }) 12 | * @param {String|Element} [options.el] - All a tag links under the specified selector (default whole page) 13 | * @param {Array} [options.origins] - Allowed origins to prefetch, Asterisk (*) allows all origins to be prefetched (default [location.hostname]) 14 | * @param {Number} [options.limit] - The total number of prefetches to allow (default unlimited) 15 | * @param {Number} [options.threshold] - Percentage of access to the visible area (default 1 pixel appears in the visible area is prefetched) 16 | * @param {Number} [options.delay] - Time each link needs to stay inside viewport before prefetching (milliseconds) (default no delay) 17 | * @param {Array} [options.customs] - Custom prefetch resource (default []) 18 | * @returns {Function} - Executing this function will stop watching all the `` tag hyperlinks that are not prefetched 19 | */ 20 | export default function prefetch(options?: Object): Function; 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { hostname, href } = location 2 | 3 | // Store the prefetched url 4 | let isFetch = [] 5 | 6 | /** 7 | * Create a prefetched link tag to insert into the head 8 | * @param {String} href Prefetched url 9 | */ 10 | function createLink(href) { 11 | const link = document.createElement('link') 12 | link.rel = 'prefetch' 13 | link.href = href 14 | document.head.appendChild(link) 15 | } 16 | 17 | /** 18 | * Perform prefetching 19 | * @param {String} url Prefetched url 20 | */ 21 | function prefetch(url) { 22 | const nav = navigator 23 | const { saveData, effectiveType } = nav.connection || nav.mozConnection || nav.webkitConnection || {} 24 | // If 2G is used or the save-data function is enabled, prefetch is not performed 25 | if (saveData || /2g/.test(effectiveType)) return 26 | // Preventing duplicate prefetching 27 | if (!isFetch.includes(url)) { 28 | isFetch.push(url) 29 | createLink(url) 30 | } 31 | } 32 | 33 | /** 34 | * Whether to support prefetch and IntersectionObserver 35 | * @returns {Boolean} 36 | */ 37 | function isSupports() { 38 | const link = document.createElement('link') 39 | return link.relList && link.relList.supports && link.relList.supports('prefetch') && IntersectionObserver 40 | } 41 | 42 | function getEl(el) { 43 | try { 44 | if (typeof el === 'string') { 45 | const _el = document.querySelector(el) 46 | if (_el.nodeType === 1) return _el 47 | } 48 | if (el.nodeType === 1) return el 49 | // eslint-disable-next-line no-empty 50 | } catch (error) {} 51 | return document 52 | } 53 | 54 | /** 55 | * https://github.com/Lete114/prefetch-page 56 | * Example: 57 | * prefetch({ el: '#box' }) // Select all a tag links under the box tag whose id attribute is 58 | * prefetch({ el: document.body }) 59 | * prefetch({ origins: '*' }) // Allows all origins to be prefetched 60 | * prefetch({ origins: ['example.com', 'www.example.com', 'blog.example.com'] }) 61 | * prefetch({ limit: 10 }) 62 | * prefetch({ threshold: 25 }) // Trigger prefetching after entering 25% of the visible area 63 | * prefetch({ delay: 1000 }) // Prefetch is triggered after staying in the visible area for 1 second 64 | * prefetch({ customs: ['markdown.js', '404.html', 'https://www.example.com'] }) 65 | * @param {String|Element} [options.el] - All a tag links under the specified selector (default whole page) 66 | * @param {Array} [options.origins] - Allowed origins to prefetch, Asterisk (*) allows all origins to be prefetched (default [location.hostname]) 67 | * @param {Number} [options.limit] - The total number of prefetches to allow (default unlimited) 68 | * @param {Number} [options.threshold] - Percentage of access to the visible area (default 1 pixel appears in the visible area is prefetched) 69 | * @param {Number} [options.delay] - Time each link needs to stay inside viewport before prefetching (milliseconds) (default no delay) 70 | * @param {Array} [options.customs] - Custom prefetch resource (default []) 71 | * @returns {Function} - Executing this function will stop watching all the `` tag hyperlinks that are not prefetched 72 | */ 73 | export default (options) => { 74 | if (!isSupports()) return 75 | if (!options) options = {} 76 | const el = getEl(options.el) 77 | const origins = options.origins || [hostname] 78 | const limit = options.limit || 1 / 0 79 | const threshold = (options.threshold || 0) / 100 80 | const delay = options.delay || 0 81 | const customs = options.customs || [] 82 | 83 | const observer = new IntersectionObserver(observerCallback, { threshold }) 84 | 85 | function setDelay(callback, delay) { 86 | if (!delay) return callback() 87 | return setTimeout(callback, delay) 88 | } 89 | 90 | function observerCallback(entries) { 91 | entries.forEach(({ isIntersecting, target }) => { 92 | // Delayed prefetching 93 | function delayFn() { 94 | observer.unobserve(target) // Stop watching 95 | if (isFetch.length < limit) prefetch(target.href) 96 | } 97 | 98 | // Whether the currently observed target appears in the visible window area 99 | // Clear the delay if you leave the visible window during the delay time 100 | if (isIntersecting) target.prefetch = setDelay(delayFn, delay) 101 | else clearTimeout(target.prefetch) 102 | }) 103 | } 104 | 105 | el.querySelectorAll('a:not([target=_blank])').forEach((node) => { 106 | // If no origins are set, then all origins are prefetch 107 | // and vice versa only prefetch the set origins 108 | if (origins === '*' || origins.includes(node.hostname)) { 109 | // Avoid watching the current page 110 | if (node.href === href) return 111 | // Filtering hash, Avoid unnecessary observations 112 | const { origin, pathname } = new URL(node.href, href) 113 | origin + pathname === node.href ? observer.observe(node) : '' 114 | } 115 | }) 116 | // custom prefetch 117 | for (let i = 0; i < customs.length; i++) prefetch(customs[i]) 118 | 119 | return function () { 120 | isFetch = [] 121 | observer.disconnect() 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 语言: 3 | 中文 4 | English 5 |
6 | 7 |

Prefetch-Page

8 |

浏览器在空闲时预加载可见区域的超链接,以加速后续页面的速度

9 | 10 |

11 | Version 12 | visitor-badge 13 | MIT License 14 |

15 | 16 | ## 安装 17 | 18 | 使用 npm: 19 | 20 | ```bash 21 | npm install prefetch-page --save 22 | ``` 23 | 24 | 使用 CDN: 25 | 26 | ```html 27 | 28 | ``` 29 | 30 | ## 使用方法 31 | 32 | 在浏览器中使用 33 | 34 | ```html 35 | 36 | 47 | ``` 48 | 49 | ESModule 模块 50 | 51 | ```js 52 | import prefetch from 'prefetch-page' 53 | 54 | // 页面 dom 加载完成时触发 55 | addEventListener('DOMContentLoaded', function () { 56 | prefetch({ threshold: 25, delay: 3000, limit: 10 }) 57 | }) 58 | 59 | // 或者在浏览器空闲时执行 60 | requestIdleCallback(function () { 61 | prefetch({ threshold: 25, delay: 3000, limit: 10 }) 62 | }) 63 | ``` 64 | 65 | CommonJS 模块 66 | 67 | ```js 68 | const prefetch = require('prefetch-page') 69 | 70 | // 页面 dom 加载完成时触发 71 | addEventListener('DOMContentLoaded', function () { 72 | prefetch({ threshold: 25, delay: 3000, limit: 10 }) 73 | }) 74 | 75 | // 或者在浏览器空闲时执行 76 | requestIdleCallback(function () { 77 | prefetch({ threshold: 25, delay: 3000, limit: 10 }) 78 | }) 79 | ``` 80 | 81 | ## 选项 API 82 | 83 | ### prefetch(options) 84 | 85 | 返回类型: `Function` 86 | 87 | 返回一个函数,执行这个函数会停止观察所有未预加载的``标签超链接 88 | 89 | ```html 90 | 91 | 97 | ``` 98 | 99 | ### options.el 100 | 101 | 类型: `String|Element` 102 | 103 | 默认值: `document` 104 | 105 | 监听指定 DOM 元素下的超链接 106 | 107 | ```html 108 |
109 | test page 1 110 | test page 2 111 | test page 3 112 |
113 | test page 4 114 | 115 | 123 | ``` 124 | 125 | ### options.origins 126 | 127 | 类型: `Array` 128 | 129 | 默认值: `[location.hostname]` 130 | 131 | 只对指定的 origin 进行预加载,星号(\*)允许所有源预加载 132 | 133 | ```html 134 | test page 1 135 | test page 2 136 | test page 3 137 | 138 | 146 | ``` 147 | 148 | ### options.limit 149 | 150 | 类型: `Number` 151 | 152 | 默认值: `Infinity` 153 | 154 | 限制预加载总数 155 | 156 | ```html 157 | test page 1 158 | test page 2 159 | test page 3 160 | 161 | 168 | ``` 169 | 170 | ### options.threshold 171 | 172 | 类型: `Number` 173 | 174 | 默认值: `0` 175 | 176 | 当目标超链接出现在浏览器可视区域大于等于 25% 时触发预加载 177 | 178 | ### options.delay 179 | 180 | 类型: `Number` 181 | 182 | 默认值: `0` 183 | 184 | 当目标超链接出现在浏览器可视区域时延迟 3 秒触发预加载 185 | 186 | 如果未到 3 秒,假设在延迟到 2 秒的时候滚动到其它位置,导致目标超链接不再可视范围时,则终止触发预加载函数 187 | 188 | 如果目标元素再次出现唉可视区域,则重新延迟 3 秒 189 | 190 | ### options.customs 191 | 192 | 类型: `Array` 193 | 194 | 默认值: `undefined` 195 | 196 | 自定义预加载资源,可以是 img、mp3、mp4、json 任何资源 197 | 198 | ```html 199 | 200 | 205 | ``` 206 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 |
2 | Language: 3 | English 4 | 中文 5 |
6 | 7 |

Prefetch-Page

8 |

Browser prefetchs visible area hyperlinks at idle time to speed up subsequent page loads

9 | 10 |

11 | Version 12 | visitor-badge 13 | MIT License 14 | 15 |

16 | 17 | ## Installing 18 | 19 | Using npm: 20 | 21 | ```bash 22 | npm install prefetch-page --save 23 | ``` 24 | 25 | Using CDN: 26 | 27 | ````html 28 | 29 | 30 | ## Using Use in your browser ```html 31 | 32 | 43 | ```` 44 | 45 | ESModule Modules 46 | 47 | ```js 48 | import prefetch from 'prefetch-page' 49 | 50 | // Triggered when page dom loading is complete 51 | addEventListener('DOMContentLoaded', function () { 52 | prefetch({ threshold: 25, delay: 3000, limit: 10 }) 53 | }) 54 | 55 | // Or execute it when the browser is idle 56 | requestIdleCallback(function () { 57 | prefetch({ threshold: 25, delay: 3000, limit: 10 }) 58 | }) 59 | ``` 60 | 61 | CommonJS Modules 62 | 63 | ```js 64 | const prefetch = require('prefetch-page') 65 | 66 | // Triggered when page dom loading is complete 67 | addEventListener('DOMContentLoaded', function () { 68 | prefetch({ threshold: 25, delay: 3000, limit: 10 }) 69 | }) 70 | 71 | // Or execute it when the browser is idle 72 | requestIdleCallback(function () { 73 | prefetch({ threshold: 25, delay: 3000, limit: 10 }) 74 | }) 75 | ``` 76 | 77 | ## Options API 78 | 79 | ### prefetch(options) 80 | 81 | ReturnType: `Function` 82 | 83 | Returns a function executing this function will stop watching all the `` tag hyperlinks that are not prefetched 84 | 85 | ```html 86 | 87 | 93 | ``` 94 | 95 | ### options.el 96 | 97 | Type: `String|Element` 98 | 99 | Default: `document` 100 | 101 | Listens for hyperlinks under a given DOM element 102 | 103 | ```html 104 |
105 | test page 1 106 | test page 2 107 | test page 3 108 |
109 | test page 4 110 | 111 | 119 | ``` 120 | 121 | ### options.origins 122 | 123 | Type: `Array` 124 | 125 | Default: `[location.hostname]` 126 | 127 | Prefetch only the specified origins, Asterisk (\*) allows all origins to be prefetched 128 | 129 | ```html 130 | test page 1 131 | test page 2 132 | test page 3 133 | 134 | 142 | ``` 143 | 144 | ### options.limit 145 | 146 | Type: `Number` 147 | 148 | Default: `Infinity` 149 | 150 | Limit the total number of prefetch 151 | 152 | ```html 153 | test page 1 154 | test page 2 155 | test page 3 156 | 157 | 164 | ``` 165 | 166 | ### options.threshold 167 | 168 | Type: `Number` 169 | 170 | Default: `0` 171 | 172 | Preload is triggered when the target hyperlink appears in more than 25% of the browser's viewable area 173 | 174 | ### options.delay 175 | 176 | Type: `Number` 177 | 178 | Default: `0` 179 | 180 | Delay 3 seconds to trigger the preload when the target hyperlink appears in the viewable area of the browser 181 | 182 | If the delay is less than 3 seconds, the preload function is terminated if the target hyperlink is no longer visible, assuming that it scrolls to another location when the delay reaches 2 seconds 183 | 184 | If the target element reappears in the visible area, the preload is delayed for 3 seconds again 185 | 186 | ### options.customs 187 | 188 | Type: `Array` 189 | 190 | Default: `undefined` 191 | 192 | Custom preloaded resources, can be img, mp3, mp4, json any resource 193 | 194 | ```html 195 | 196 | 201 | ``` 202 | --------------------------------------------------------------------------------