├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.html ├── log.md ├── package.json ├── pnpm-lock.yaml ├── src ├── main.ts └── router.ts ├── tsconfig.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | js/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | index.html 2 | tsconfig.json 3 | vite.config.ts 4 | demo/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 欧阳鹏 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vanjs-router 2 | 3 | Simple Frontend Routing Component Based on Van.js. 4 | 5 | This is version 2. For version 1 documentation, please [click here](https://github.com/iuroc/vanjs-router/tree/79b190f56846bef9906de886ddf29f6c62b892db). 6 | 7 | ## Features 8 | 9 | - Supports both string and regex matching. 10 | - Supports setting page display delay. 11 | - Supports configuring events for the first route match (`onFirst`) and subsequent route matches (`onLoad`). 12 | - Implemented using TypeScript. 13 | - Simple API. 14 | 15 | ## Quick Start 16 | 17 | ```shell 18 | npm install vanjs-router 19 | ``` 20 | 21 | ```html 22 | 23 | 24 | 62 | ``` 63 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | VanJS Router V2 8 | 9 | 10 | 11 | 12 | 13 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /log.md: -------------------------------------------------------------------------------- 1 | ## Vanjs Router 的异常情况(2024 年 11 月 4 日) 2 | 3 | Vanjs Router 的基本原理是创建一个 `now` 状态,通过监听 Window 的 `hashchange` 事件来更新 `now` 的值。每个路由内部通过 `van.derive(() => now.val)` 来监听路由的改变,而不是直接使用 `hashchange`。此外,`onLoad` 和 `onFirst` 也是在 `van.derive` 内部被执行的。接下来我们看下面的代码: 4 | 5 | ```ts 6 | const count = van.state(0) 7 | 8 | // 可以监听到 count 值的变化 9 | van.derive(() => { 10 | return count.val * 2 11 | }) 12 | 13 | // 可以监听到 count 值的变化 14 | van.derive(() => { 15 | count.val 16 | 17 | setTimeout(() => { 18 | count.val * 2 19 | }, 1000) 20 | }) 21 | 22 | // 可以监听到 count 值的变化 23 | van.derive(async () => { 24 | count.val // 此处在同步上下文中保留了一个引用 25 | 26 | await new Promise((resolve) => setTimeout(resolve, 1000)) 27 | return count.val * 2 // 该处不属于同步上下文,所以是无效引用,但由于有上面的引用,这个 derive 可以监听 28 | }) 29 | 30 | // 无法监听到 count 值的变化 31 | van.derive(async () => { 32 | await new Promise((resolve) => setTimeout(resolve, 1000)) 33 | return count.val * 2 // 该处不属于同步上下文,且同步上下文中不存在状态值的引用,因此无法监听。 34 | }) 35 | 36 | const func = async () => { 37 | return count.val * 2 38 | } 39 | 40 | van.derive(async () => { 41 | return await func() // 可以监听到,因为 func 的同步上下文也是 derive 回调函数的同步上下文,因此可以被监听。 42 | }) 43 | ``` 44 | 45 | 可以看到,为了能监听到 `derive` 回调函数中的状态值变化,就必须让状态值位于回调函数的同步上下文中。 46 | 47 | 如果 `onLoad` 和 `onFirst` 的同步上下文中存在某些状态值的引用,当这些值被异步更新时,就会导致与 `onLoad` 和 `onFirst` 处于同一同步上下文的 `now` 值监听函数被触发,进而导致 `onLoad` 和 `onFirst` 被重复触发的 Bug。 48 | 49 | 因为函数的同步上下文会继承外部的同步上下文,而在继承上下文中的多个作用域中的状态值,都会被 `derive` 监听到。我们要解决的是确保 `onLoad` 和 `onFirst` 的同步上下文中存在的状态值引用不会触发外部的 `derive`,从而导致 `onLoad` 和 `onFirst` 被重复触发。 50 | 51 | 我们监听 `now` 的目的是实现对 `hashchange` 监听器的动态更新。只需创建一个全局的 `hashchange` 监听器,各个路由通过 `derive` 来共享 `hashchange` 的动态更新通知。期望 `derive` 的监听效果比 `hashchange` 更好,操作也更加便捷,且具备更好的可拓展性,比如可以将 `derive` 的返回值作为新的状态用于其他开发场景。 52 | 53 | 然而,我们要注意的是,写这样的 `derive` 来监听 `now` 的主要目的实际上就是监听 `hashchange`。创建多个 `hashchange` 监听器并不会对性能产生明显影响。如果确实需要用到 `Van State` 来监听路由变化,其实我们已经有 `now`,并且它被全局的 `hashchange` 自动更新。这个 `now` 本身就可以作为拓展性的接口状态值按需使用,而各个路由需要监听路由变化时,最好使用正常的独立 `hashchange` 监听器。这样可以实现最纯粹的路由变化监听,而无需担心 `derive` 在监听 `now` 的同时带来的数据和事件异常情况。 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "typescript": "^5.5.4", 4 | "vite": "^5.3.5" 5 | }, 6 | "dependencies": { 7 | "vanjs-core": "^1.5.1" 8 | }, 9 | "scripts": { 10 | "build": "vite build && tsc" 11 | }, 12 | "type": "module", 13 | "name": "vanjs-router", 14 | "version": "2.1.4", 15 | "main": "js/router.js", 16 | "types": "src/router.ts" 17 | } -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | vanjs-core: 12 | specifier: ^1.5.1 13 | version: 1.5.1 14 | devDependencies: 15 | typescript: 16 | specifier: ^5.5.4 17 | version: 5.5.4 18 | vite: 19 | specifier: ^5.3.5 20 | version: 5.3.5 21 | 22 | packages: 23 | 24 | '@esbuild/aix-ppc64@0.21.5': 25 | resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} 26 | engines: {node: '>=12'} 27 | cpu: [ppc64] 28 | os: [aix] 29 | 30 | '@esbuild/android-arm64@0.21.5': 31 | resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} 32 | engines: {node: '>=12'} 33 | cpu: [arm64] 34 | os: [android] 35 | 36 | '@esbuild/android-arm@0.21.5': 37 | resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} 38 | engines: {node: '>=12'} 39 | cpu: [arm] 40 | os: [android] 41 | 42 | '@esbuild/android-x64@0.21.5': 43 | resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} 44 | engines: {node: '>=12'} 45 | cpu: [x64] 46 | os: [android] 47 | 48 | '@esbuild/darwin-arm64@0.21.5': 49 | resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} 50 | engines: {node: '>=12'} 51 | cpu: [arm64] 52 | os: [darwin] 53 | 54 | '@esbuild/darwin-x64@0.21.5': 55 | resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} 56 | engines: {node: '>=12'} 57 | cpu: [x64] 58 | os: [darwin] 59 | 60 | '@esbuild/freebsd-arm64@0.21.5': 61 | resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} 62 | engines: {node: '>=12'} 63 | cpu: [arm64] 64 | os: [freebsd] 65 | 66 | '@esbuild/freebsd-x64@0.21.5': 67 | resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} 68 | engines: {node: '>=12'} 69 | cpu: [x64] 70 | os: [freebsd] 71 | 72 | '@esbuild/linux-arm64@0.21.5': 73 | resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} 74 | engines: {node: '>=12'} 75 | cpu: [arm64] 76 | os: [linux] 77 | 78 | '@esbuild/linux-arm@0.21.5': 79 | resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} 80 | engines: {node: '>=12'} 81 | cpu: [arm] 82 | os: [linux] 83 | 84 | '@esbuild/linux-ia32@0.21.5': 85 | resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} 86 | engines: {node: '>=12'} 87 | cpu: [ia32] 88 | os: [linux] 89 | 90 | '@esbuild/linux-loong64@0.21.5': 91 | resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} 92 | engines: {node: '>=12'} 93 | cpu: [loong64] 94 | os: [linux] 95 | 96 | '@esbuild/linux-mips64el@0.21.5': 97 | resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} 98 | engines: {node: '>=12'} 99 | cpu: [mips64el] 100 | os: [linux] 101 | 102 | '@esbuild/linux-ppc64@0.21.5': 103 | resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} 104 | engines: {node: '>=12'} 105 | cpu: [ppc64] 106 | os: [linux] 107 | 108 | '@esbuild/linux-riscv64@0.21.5': 109 | resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} 110 | engines: {node: '>=12'} 111 | cpu: [riscv64] 112 | os: [linux] 113 | 114 | '@esbuild/linux-s390x@0.21.5': 115 | resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} 116 | engines: {node: '>=12'} 117 | cpu: [s390x] 118 | os: [linux] 119 | 120 | '@esbuild/linux-x64@0.21.5': 121 | resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} 122 | engines: {node: '>=12'} 123 | cpu: [x64] 124 | os: [linux] 125 | 126 | '@esbuild/netbsd-x64@0.21.5': 127 | resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} 128 | engines: {node: '>=12'} 129 | cpu: [x64] 130 | os: [netbsd] 131 | 132 | '@esbuild/openbsd-x64@0.21.5': 133 | resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} 134 | engines: {node: '>=12'} 135 | cpu: [x64] 136 | os: [openbsd] 137 | 138 | '@esbuild/sunos-x64@0.21.5': 139 | resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} 140 | engines: {node: '>=12'} 141 | cpu: [x64] 142 | os: [sunos] 143 | 144 | '@esbuild/win32-arm64@0.21.5': 145 | resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} 146 | engines: {node: '>=12'} 147 | cpu: [arm64] 148 | os: [win32] 149 | 150 | '@esbuild/win32-ia32@0.21.5': 151 | resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} 152 | engines: {node: '>=12'} 153 | cpu: [ia32] 154 | os: [win32] 155 | 156 | '@esbuild/win32-x64@0.21.5': 157 | resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} 158 | engines: {node: '>=12'} 159 | cpu: [x64] 160 | os: [win32] 161 | 162 | '@rollup/rollup-android-arm-eabi@4.19.0': 163 | resolution: {integrity: sha512-JlPfZ/C7yn5S5p0yKk7uhHTTnFlvTgLetl2VxqE518QgyM7C9bSfFTYvB/Q/ftkq0RIPY4ySxTz+/wKJ/dXC0w==} 164 | cpu: [arm] 165 | os: [android] 166 | 167 | '@rollup/rollup-android-arm64@4.19.0': 168 | resolution: {integrity: sha512-RDxUSY8D1tWYfn00DDi5myxKgOk6RvWPxhmWexcICt/MEC6yEMr4HNCu1sXXYLw8iAsg0D44NuU+qNq7zVWCrw==} 169 | cpu: [arm64] 170 | os: [android] 171 | 172 | '@rollup/rollup-darwin-arm64@4.19.0': 173 | resolution: {integrity: sha512-emvKHL4B15x6nlNTBMtIaC9tLPRpeA5jMvRLXVbl/W9Ie7HhkrE7KQjvgS9uxgatL1HmHWDXk5TTS4IaNJxbAA==} 174 | cpu: [arm64] 175 | os: [darwin] 176 | 177 | '@rollup/rollup-darwin-x64@4.19.0': 178 | resolution: {integrity: sha512-fO28cWA1dC57qCd+D0rfLC4VPbh6EOJXrreBmFLWPGI9dpMlER2YwSPZzSGfq11XgcEpPukPTfEVFtw2q2nYJg==} 179 | cpu: [x64] 180 | os: [darwin] 181 | 182 | '@rollup/rollup-linux-arm-gnueabihf@4.19.0': 183 | resolution: {integrity: sha512-2Rn36Ubxdv32NUcfm0wB1tgKqkQuft00PtM23VqLuCUR4N5jcNWDoV5iBC9jeGdgS38WK66ElncprqgMUOyomw==} 184 | cpu: [arm] 185 | os: [linux] 186 | 187 | '@rollup/rollup-linux-arm-musleabihf@4.19.0': 188 | resolution: {integrity: sha512-gJuzIVdq/X1ZA2bHeCGCISe0VWqCoNT8BvkQ+BfsixXwTOndhtLUpOg0A1Fcx/+eA6ei6rMBzlOz4JzmiDw7JQ==} 189 | cpu: [arm] 190 | os: [linux] 191 | 192 | '@rollup/rollup-linux-arm64-gnu@4.19.0': 193 | resolution: {integrity: sha512-0EkX2HYPkSADo9cfeGFoQ7R0/wTKb7q6DdwI4Yn/ULFE1wuRRCHybxpl2goQrx4c/yzK3I8OlgtBu4xvted0ug==} 194 | cpu: [arm64] 195 | os: [linux] 196 | 197 | '@rollup/rollup-linux-arm64-musl@4.19.0': 198 | resolution: {integrity: sha512-GlIQRj9px52ISomIOEUq/IojLZqzkvRpdP3cLgIE1wUWaiU5Takwlzpz002q0Nxxr1y2ZgxC2obWxjr13lvxNQ==} 199 | cpu: [arm64] 200 | os: [linux] 201 | 202 | '@rollup/rollup-linux-powerpc64le-gnu@4.19.0': 203 | resolution: {integrity: sha512-N6cFJzssruDLUOKfEKeovCKiHcdwVYOT1Hs6dovDQ61+Y9n3Ek4zXvtghPPelt6U0AH4aDGnDLb83uiJMkWYzQ==} 204 | cpu: [ppc64] 205 | os: [linux] 206 | 207 | '@rollup/rollup-linux-riscv64-gnu@4.19.0': 208 | resolution: {integrity: sha512-2DnD3mkS2uuam/alF+I7M84koGwvn3ZVD7uG+LEWpyzo/bq8+kKnus2EVCkcvh6PlNB8QPNFOz6fWd5N8o1CYg==} 209 | cpu: [riscv64] 210 | os: [linux] 211 | 212 | '@rollup/rollup-linux-s390x-gnu@4.19.0': 213 | resolution: {integrity: sha512-D6pkaF7OpE7lzlTOFCB2m3Ngzu2ykw40Nka9WmKGUOTS3xcIieHe82slQlNq69sVB04ch73thKYIWz/Ian8DUA==} 214 | cpu: [s390x] 215 | os: [linux] 216 | 217 | '@rollup/rollup-linux-x64-gnu@4.19.0': 218 | resolution: {integrity: sha512-HBndjQLP8OsdJNSxpNIN0einbDmRFg9+UQeZV1eiYupIRuZsDEoeGU43NQsS34Pp166DtwQOnpcbV/zQxM+rWA==} 219 | cpu: [x64] 220 | os: [linux] 221 | 222 | '@rollup/rollup-linux-x64-musl@4.19.0': 223 | resolution: {integrity: sha512-HxfbvfCKJe/RMYJJn0a12eiOI9OOtAUF4G6ozrFUK95BNyoJaSiBjIOHjZskTUffUrB84IPKkFG9H9nEvJGW6A==} 224 | cpu: [x64] 225 | os: [linux] 226 | 227 | '@rollup/rollup-win32-arm64-msvc@4.19.0': 228 | resolution: {integrity: sha512-HxDMKIhmcguGTiP5TsLNolwBUK3nGGUEoV/BO9ldUBoMLBssvh4J0X8pf11i1fTV7WShWItB1bKAKjX4RQeYmg==} 229 | cpu: [arm64] 230 | os: [win32] 231 | 232 | '@rollup/rollup-win32-ia32-msvc@4.19.0': 233 | resolution: {integrity: sha512-xItlIAZZaiG/u0wooGzRsx11rokP4qyc/79LkAOdznGRAbOFc+SfEdfUOszG1odsHNgwippUJavag/+W/Etc6Q==} 234 | cpu: [ia32] 235 | os: [win32] 236 | 237 | '@rollup/rollup-win32-x64-msvc@4.19.0': 238 | resolution: {integrity: sha512-xNo5fV5ycvCCKqiZcpB65VMR11NJB+StnxHz20jdqRAktfdfzhgjTiJ2doTDQE/7dqGaV5I7ZGqKpgph6lCIag==} 239 | cpu: [x64] 240 | os: [win32] 241 | 242 | '@types/estree@1.0.5': 243 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 244 | 245 | esbuild@0.21.5: 246 | resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} 247 | engines: {node: '>=12'} 248 | hasBin: true 249 | 250 | fsevents@2.3.3: 251 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 252 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 253 | os: [darwin] 254 | 255 | nanoid@3.3.7: 256 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 257 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 258 | hasBin: true 259 | 260 | picocolors@1.0.1: 261 | resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} 262 | 263 | postcss@8.4.40: 264 | resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} 265 | engines: {node: ^10 || ^12 || >=14} 266 | 267 | rollup@4.19.0: 268 | resolution: {integrity: sha512-5r7EYSQIowHsK4eTZ0Y81qpZuJz+MUuYeqmmYmRMl1nwhdmbiYqt5jwzf6u7wyOzJgYqtCRMtVRKOtHANBz7rA==} 269 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 270 | hasBin: true 271 | 272 | source-map-js@1.2.0: 273 | resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 274 | engines: {node: '>=0.10.0'} 275 | 276 | typescript@5.5.4: 277 | resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} 278 | engines: {node: '>=14.17'} 279 | hasBin: true 280 | 281 | vanjs-core@1.5.1: 282 | resolution: {integrity: sha512-KT2XPqlwRdkyr7+pHc9BWOwmA6l7UqWrtt80xM1QkaUujc7Fq4Tgr6RhFo5wFO1u2sV1q7sR2PBKpVEGx4fEdA==} 283 | 284 | vite@5.3.5: 285 | resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==} 286 | engines: {node: ^18.0.0 || >=20.0.0} 287 | hasBin: true 288 | peerDependencies: 289 | '@types/node': ^18.0.0 || >=20.0.0 290 | less: '*' 291 | lightningcss: ^1.21.0 292 | sass: '*' 293 | stylus: '*' 294 | sugarss: '*' 295 | terser: ^5.4.0 296 | peerDependenciesMeta: 297 | '@types/node': 298 | optional: true 299 | less: 300 | optional: true 301 | lightningcss: 302 | optional: true 303 | sass: 304 | optional: true 305 | stylus: 306 | optional: true 307 | sugarss: 308 | optional: true 309 | terser: 310 | optional: true 311 | 312 | snapshots: 313 | 314 | '@esbuild/aix-ppc64@0.21.5': 315 | optional: true 316 | 317 | '@esbuild/android-arm64@0.21.5': 318 | optional: true 319 | 320 | '@esbuild/android-arm@0.21.5': 321 | optional: true 322 | 323 | '@esbuild/android-x64@0.21.5': 324 | optional: true 325 | 326 | '@esbuild/darwin-arm64@0.21.5': 327 | optional: true 328 | 329 | '@esbuild/darwin-x64@0.21.5': 330 | optional: true 331 | 332 | '@esbuild/freebsd-arm64@0.21.5': 333 | optional: true 334 | 335 | '@esbuild/freebsd-x64@0.21.5': 336 | optional: true 337 | 338 | '@esbuild/linux-arm64@0.21.5': 339 | optional: true 340 | 341 | '@esbuild/linux-arm@0.21.5': 342 | optional: true 343 | 344 | '@esbuild/linux-ia32@0.21.5': 345 | optional: true 346 | 347 | '@esbuild/linux-loong64@0.21.5': 348 | optional: true 349 | 350 | '@esbuild/linux-mips64el@0.21.5': 351 | optional: true 352 | 353 | '@esbuild/linux-ppc64@0.21.5': 354 | optional: true 355 | 356 | '@esbuild/linux-riscv64@0.21.5': 357 | optional: true 358 | 359 | '@esbuild/linux-s390x@0.21.5': 360 | optional: true 361 | 362 | '@esbuild/linux-x64@0.21.5': 363 | optional: true 364 | 365 | '@esbuild/netbsd-x64@0.21.5': 366 | optional: true 367 | 368 | '@esbuild/openbsd-x64@0.21.5': 369 | optional: true 370 | 371 | '@esbuild/sunos-x64@0.21.5': 372 | optional: true 373 | 374 | '@esbuild/win32-arm64@0.21.5': 375 | optional: true 376 | 377 | '@esbuild/win32-ia32@0.21.5': 378 | optional: true 379 | 380 | '@esbuild/win32-x64@0.21.5': 381 | optional: true 382 | 383 | '@rollup/rollup-android-arm-eabi@4.19.0': 384 | optional: true 385 | 386 | '@rollup/rollup-android-arm64@4.19.0': 387 | optional: true 388 | 389 | '@rollup/rollup-darwin-arm64@4.19.0': 390 | optional: true 391 | 392 | '@rollup/rollup-darwin-x64@4.19.0': 393 | optional: true 394 | 395 | '@rollup/rollup-linux-arm-gnueabihf@4.19.0': 396 | optional: true 397 | 398 | '@rollup/rollup-linux-arm-musleabihf@4.19.0': 399 | optional: true 400 | 401 | '@rollup/rollup-linux-arm64-gnu@4.19.0': 402 | optional: true 403 | 404 | '@rollup/rollup-linux-arm64-musl@4.19.0': 405 | optional: true 406 | 407 | '@rollup/rollup-linux-powerpc64le-gnu@4.19.0': 408 | optional: true 409 | 410 | '@rollup/rollup-linux-riscv64-gnu@4.19.0': 411 | optional: true 412 | 413 | '@rollup/rollup-linux-s390x-gnu@4.19.0': 414 | optional: true 415 | 416 | '@rollup/rollup-linux-x64-gnu@4.19.0': 417 | optional: true 418 | 419 | '@rollup/rollup-linux-x64-musl@4.19.0': 420 | optional: true 421 | 422 | '@rollup/rollup-win32-arm64-msvc@4.19.0': 423 | optional: true 424 | 425 | '@rollup/rollup-win32-ia32-msvc@4.19.0': 426 | optional: true 427 | 428 | '@rollup/rollup-win32-x64-msvc@4.19.0': 429 | optional: true 430 | 431 | '@types/estree@1.0.5': {} 432 | 433 | esbuild@0.21.5: 434 | optionalDependencies: 435 | '@esbuild/aix-ppc64': 0.21.5 436 | '@esbuild/android-arm': 0.21.5 437 | '@esbuild/android-arm64': 0.21.5 438 | '@esbuild/android-x64': 0.21.5 439 | '@esbuild/darwin-arm64': 0.21.5 440 | '@esbuild/darwin-x64': 0.21.5 441 | '@esbuild/freebsd-arm64': 0.21.5 442 | '@esbuild/freebsd-x64': 0.21.5 443 | '@esbuild/linux-arm': 0.21.5 444 | '@esbuild/linux-arm64': 0.21.5 445 | '@esbuild/linux-ia32': 0.21.5 446 | '@esbuild/linux-loong64': 0.21.5 447 | '@esbuild/linux-mips64el': 0.21.5 448 | '@esbuild/linux-ppc64': 0.21.5 449 | '@esbuild/linux-riscv64': 0.21.5 450 | '@esbuild/linux-s390x': 0.21.5 451 | '@esbuild/linux-x64': 0.21.5 452 | '@esbuild/netbsd-x64': 0.21.5 453 | '@esbuild/openbsd-x64': 0.21.5 454 | '@esbuild/sunos-x64': 0.21.5 455 | '@esbuild/win32-arm64': 0.21.5 456 | '@esbuild/win32-ia32': 0.21.5 457 | '@esbuild/win32-x64': 0.21.5 458 | 459 | fsevents@2.3.3: 460 | optional: true 461 | 462 | nanoid@3.3.7: {} 463 | 464 | picocolors@1.0.1: {} 465 | 466 | postcss@8.4.40: 467 | dependencies: 468 | nanoid: 3.3.7 469 | picocolors: 1.0.1 470 | source-map-js: 1.2.0 471 | 472 | rollup@4.19.0: 473 | dependencies: 474 | '@types/estree': 1.0.5 475 | optionalDependencies: 476 | '@rollup/rollup-android-arm-eabi': 4.19.0 477 | '@rollup/rollup-android-arm64': 4.19.0 478 | '@rollup/rollup-darwin-arm64': 4.19.0 479 | '@rollup/rollup-darwin-x64': 4.19.0 480 | '@rollup/rollup-linux-arm-gnueabihf': 4.19.0 481 | '@rollup/rollup-linux-arm-musleabihf': 4.19.0 482 | '@rollup/rollup-linux-arm64-gnu': 4.19.0 483 | '@rollup/rollup-linux-arm64-musl': 4.19.0 484 | '@rollup/rollup-linux-powerpc64le-gnu': 4.19.0 485 | '@rollup/rollup-linux-riscv64-gnu': 4.19.0 486 | '@rollup/rollup-linux-s390x-gnu': 4.19.0 487 | '@rollup/rollup-linux-x64-gnu': 4.19.0 488 | '@rollup/rollup-linux-x64-musl': 4.19.0 489 | '@rollup/rollup-win32-arm64-msvc': 4.19.0 490 | '@rollup/rollup-win32-ia32-msvc': 4.19.0 491 | '@rollup/rollup-win32-x64-msvc': 4.19.0 492 | fsevents: 2.3.3 493 | 494 | source-map-js@1.2.0: {} 495 | 496 | typescript@5.5.4: {} 497 | 498 | vanjs-core@1.5.1: {} 499 | 500 | vite@5.3.5: 501 | dependencies: 502 | esbuild: 0.21.5 503 | postcss: 8.4.40 504 | rollup: 4.19.0 505 | optionalDependencies: 506 | fsevents: 2.3.3 507 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as router from './router' 2 | 3 | Object.defineProperty(window, 'router', { value: router }) -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import van, { State } from 'vanjs-core' 2 | 3 | /** 返回当前的 `location.hash` 值的 `#/` 后面的部分,如果为空则返回 `home` */ 4 | export const nowHash = (): string => location.hash ? location.hash.slice(2) : 'home' 5 | 6 | /** 当前的 Hash 值,不含开头的 #,值通过 `nowHash()` 返回。 */ 7 | export const now: State = van.state(nowHash()) 8 | 9 | window.addEventListener('hashchange', () => { 10 | now.val = nowHash() 11 | }) 12 | 13 | /** 路由处理程序初始化配置项 */ 14 | export type HandlerConfig = { 15 | /** 当前路由的命中规则 16 | * - 如果传入字符串,则只匹配 Hash 以 `/` 分割后的首项,其余项作为 `args`。 17 | * - 如果传入正则,则匹配正则,捕获分组作为 `args`。 18 | * 19 | * 例如: 20 | * ```ts 21 | * { rule: 'about' } 22 | * { rule: /list_(\d+)/ } 23 | * ``` 24 | */ 25 | rule: Handler['rule'] 26 | /** 当前路由页面的构建方法 */ 27 | Loader: Handler['Loader'] 28 | /** 延迟加载页面,需要显示调用 `show()` 方法显示页面。 */ 29 | delayed?: Handler['delayed'] 30 | /** 首次路由命中事件,在 `onLoad` 之前执行。 */ 31 | onFirst?: Handler['onFirst'] 32 | /** 路由命中事件,在 `await onFirst()` 之后执行。 */ 33 | onLoad?: Handler['onLoad'] 34 | } 35 | 36 | /** 路由处理程序实例 */ 37 | export class Handler { 38 | private rule: string | RegExp 39 | /** 当前路由被命中后接收到的路由参数 */ 40 | public args: string[] = [] 41 | private Loader: (this: Handler) => E 42 | private delayed = false 43 | private onFirst: (this: Handler) => any 44 | private onLoad: (this: Handler) => any 45 | /** 路由根元素,对外导出,可直接添加到 DOM 树 */ 46 | public element: E 47 | /** 记录当前是否初次命中路由,决定是否执行 `onFirst` 事件。 */ 48 | private isFirstLoad = true 49 | 50 | constructor(config: HandlerConfig) { 51 | if (!config) throw new Error('config 不能为空') 52 | if (!config.rule) throw new Error('rule 不能为空') 53 | if (!config.Loader) throw new Error('Loader 不能为空') 54 | // 载入路由的基础配置 55 | this.rule = config.rule 56 | this.Loader = config.Loader 57 | this.delayed = config.delayed || false 58 | this.onFirst = config.onFirst || (async () => { }) 59 | this.onLoad = config.onLoad || (async () => { }) 60 | // 创建页面元素 61 | this.element = this.Loader() 62 | this.element.hidden = true 63 | // 根据 Hash 的变化,自动更新路由状态 64 | const func = async () => { 65 | // 获取当前路由的命中状态 66 | const match = this.matchHash() 67 | if (!match) { 68 | // 未被命中,刷新路由,页面隐藏。 69 | this.hide() 70 | } else { 71 | // 路由命中 72 | // 将接收到的路由参数保存起来 73 | this.args.splice(0) // 清空存储的旧参数 74 | this.args.push(...match.args) 75 | if (this.isFirstLoad) { 76 | this.isFirstLoad = false 77 | await this.onFirst() 78 | } 79 | await this.onLoad() 80 | if (!this.delayed) this.show() 81 | } 82 | } 83 | window.addEventListener('hashchange', func) 84 | func() 85 | } 86 | 87 | /** 判断当前 Hash 是否与本路由的规则匹配 */ 88 | private matchHash(): false | { hash: string, args: string[] } { 89 | if (this.rule instanceof RegExp) { 90 | const match = now.val.match(this.rule) 91 | if (!match) return false 92 | return { hash: now.val, args: [...match].slice(1) } 93 | } 94 | const parts = now.val.split('/').filter(i => i.length > 0) 95 | if (parts.length < 1) parts.push('home') 96 | return parts[0] == this.rule ? { hash: now.val, args: parts.slice(1) } : false 97 | } 98 | 99 | /** 显示当前路由元素 */ 100 | public show() { 101 | this.element.hidden = false 102 | } 103 | 104 | /** 隐藏当前路由元素 */ 105 | public hide() { 106 | this.element.hidden = true 107 | } 108 | } 109 | 110 | /** 创建一个自动管理路由状态的 DOM 元素 */ 111 | export const Route = (config: HandlerConfig): E => new Handler(config).element 112 | 113 | /** 114 | * 跳转到指定 Hash 115 | * @param name 需要前往的路由名称,对应字符串类型的 rule 116 | * @param args 路由参数 117 | */ 118 | export const goto = (name: string, ...args: any[]): void => { 119 | location.hash = name == 'home' && args.length == 0 ? '' : `/${[name, ...args].join('/')}` 120 | } 121 | 122 | /** 123 | * 路由重定向 124 | * @param from 来源路由规则,用于定义 `rule` 属性 125 | * @param to 目标路由规则,用于传入 `goto` 方法进行跳转 126 | */ 127 | export const redirect = (from: string | RegExp, to: string) => { 128 | Route({ 129 | rule: from, 130 | Loader: van.tags.div, 131 | onLoad() { 132 | goto(to) 133 | }, 134 | }) 135 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "js", 5 | "lib": [ 6 | "DOM", 7 | "ESNext" 8 | ], 9 | "module": "ESNext", 10 | "target": "ESNext", 11 | "moduleResolution": "Bundler" 12 | }, 13 | "include": [ 14 | "src" 15 | ] 16 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | export default defineConfig({ 4 | build: { 5 | lib: { 6 | entry: 'src/router.ts', 7 | name: 'router', 8 | fileName: 'vanjs-router', 9 | formats: ['iife'] 10 | }, 11 | rollupOptions: { 12 | } 13 | }, 14 | }) --------------------------------------------------------------------------------