├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── index.es.js ├── index.js ├── lib │ ├── adapter │ │ ├── wechatAdapter.js │ │ └── wechatAdapter.js.map │ ├── index.js │ ├── index.js.map │ └── utils │ │ ├── async.js │ │ ├── async.js.map │ │ ├── query.js │ │ └── query.js.map └── types │ ├── adapter │ └── wechatAdapter.d.ts │ ├── index.d.ts │ └── utils │ ├── async.d.ts │ └── query.d.ts ├── package.json ├── rollup.config.ts ├── src ├── adapter │ └── wechatAdapter.ts ├── index.ts └── utils │ ├── async.ts │ └── query.ts ├── test └── test.test.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | compiled 9 | .awcache 10 | .rpt2_cache 11 | docs 12 | tools 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | tools 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | notifications: 6 | email: false 7 | node_js: 8 | - '12' 9 | - '10' 10 | - '11' 11 | - '8' 12 | script: 13 | - npm run test:prod && npm run build 14 | after_success: 15 | - npm run travis-deploy-once "npm run report-coverage" 16 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run travis-deploy-once "npm run deploy-docs"; fi 17 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run travis-deploy-once "npm run semantic-release"; fi 18 | branches: 19 | except: 20 | - /^v\d+\.\d+\.\d+$/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 bigMeow 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cheers-mp-router 2 | 🚦精巧强大的小程序路由 3 | 4 | ## 催更、钉钉交流群: 5 | 6 | 钉钉交流群 7 | 8 | ## 使用 9 | 10 | #### 安装 11 | ``` bash 12 | npm i cheers-mp-router 13 | ``` 14 | 15 | #### typescript引入 16 | ``` typescript 17 | import Router, { RouteConfig } from "cheers-mp-router"; 18 | 19 | 20 | // 定义路由配置 21 | const routeConfigList: RouteConfig[] = [ 22 | { name: "test-tabbar", path: "pages/tabbar/test-tabbar/index", isTab: true }, 23 | { name: "testA", path: "test/pages/testA/index" }, 24 | { name: "testB", path: "test/pages/testB/index" }, 25 | { name: "product-details", path: "test/pages/product-details/index" } 26 | ]; 27 | 28 | // 实例化 29 | const router = new Router({ routes: routeConfigList }); 30 | 31 | // 注册全局 beforeEach 钩子;使用方式和 vue-router 的 beforeEach 基本一致 32 | router.beforeEach((to, from, next) => { 33 | console.log("当前路由", from); 34 | console.log("即将前往的路由", to); 35 | next(); 36 | // next({ name: "pageB" }); 37 | // next(false) 38 | }); 39 | 40 | // 注册全局 afterEach 钩子 41 | router.afterEach((current, from) => { 42 | console.log("跳转成功,当前路由:", current); 43 | console.log("之前路由:", from); 44 | }); 45 | 46 | // 调用路由方法 47 | router.push({ name: "testA" }) 48 | 49 | // 调用路由传参数 50 | router.push({ name: "product-details", query: { id: "sb" } }) 51 | ``` 52 | 53 | ## 实例API 54 | 55 | 具体[查看这里](./dist/types/index.d.ts) 56 | 57 | ## TODO 58 | - [x] 在 [cheers-mp](https://github.com/bigmeow/cheers-mp) 脚手架中使用时获得构建级别的路由支持、子包构建支持 59 | - [x] push 支持 [eventChannel](https://developers.weixin.qq.com/miniprogram/dev/api/route/wx.navigateTo.html) 事件监听 60 | -------------------------------------------------------------------------------- /dist/index.es.js: -------------------------------------------------------------------------------- 1 | function wechatAdapter(adapterconfig) { 2 | return new Promise((resolve, reject) => { 3 | let jumpMethod; 4 | if (adapterconfig.reLaunch) { 5 | jumpMethod = wx.reLaunch; 6 | } 7 | else if (adapterconfig.isTab) { 8 | jumpMethod = wx.switchTab; 9 | } 10 | else if (adapterconfig.replace) { 11 | jumpMethod = wx.redirectTo; 12 | } 13 | else { 14 | jumpMethod = wx.navigateTo; 15 | } 16 | const params = { 17 | url: adapterconfig.path, 18 | success: (res) => { 19 | resolve(res); 20 | }, 21 | fail: (res) => { 22 | reject(res); 23 | }, 24 | }; 25 | if (adapterconfig.events) { 26 | params.events = adapterconfig.events; 27 | } 28 | jumpMethod.bind(wx)(params); 29 | }); 30 | } 31 | 32 | function runQueue(queue, fn, cb) { 33 | const step = (index) => { 34 | if (index >= queue.length) { 35 | cb(); 36 | } 37 | else { 38 | if (queue[index]) { 39 | fn(queue[index], () => { 40 | step(index + 1); 41 | }); 42 | } 43 | else { 44 | step(index + 1); 45 | } 46 | } 47 | }; 48 | step(0); 49 | } 50 | 51 | const encodeReserveRE = /[!'()*]/g; 52 | const encodeReserveReplacer = (c) => '%' + c.charCodeAt(0).toString(16); 53 | const commaRE = /%2C/g; 54 | // fixed encodeURIComponent which is more conformant to RFC3986: 55 | // - escapes [!'()*] 56 | // - preserve commas 57 | const encode = (str) => encodeURIComponent(str) 58 | .replace(encodeReserveRE, encodeReserveReplacer) 59 | .replace(commaRE, ','); 60 | const decode = decodeURIComponent; 61 | function parseQuery(query) { 62 | const res = {}; 63 | query = query.trim().replace(/^(\?|#|&)/, ''); 64 | if (!query) { 65 | return res; 66 | } 67 | query.split('&').forEach(param => { 68 | const parts = param.replace(/\+/g, ' ').split('='); 69 | const key = decode(parts.shift()); 70 | const val = parts.length > 0 ? decode(parts.join('=')) : null; 71 | if (res[key] === undefined) { 72 | res[key] = val; 73 | } 74 | else if (Array.isArray(res[key])) { 75 | res[key].push(val); 76 | } 77 | else { 78 | res[key] = [res[key], val]; 79 | } 80 | }); 81 | return res; 82 | } 83 | /** 84 | * 将 'k1=v1&k2=v2' 格式的字符串转换成 query 对象 85 | * @param query (可选)要转换的字符串,格式类似于 '?name=admin&password=123' 或者 'name=admin&password=123' 86 | * @param extraQuery (可选)将转换后的 query 对象 附加到此 query 对象身上 87 | * @param _parseQuery (可选)自定义转换函数 88 | */ 89 | function resolveQuery(query, extraQuery = {}, _parseQuery) { 90 | const parse = _parseQuery || parseQuery; 91 | let parsedQuery; 92 | try { 93 | parsedQuery = parse(query || ''); 94 | } 95 | catch (e) { 96 | parsedQuery = {}; 97 | } 98 | for (const key in extraQuery) { 99 | parsedQuery[key] = extraQuery[key]; 100 | } 101 | return parsedQuery; 102 | } 103 | /** 104 | * 将 query 对象序列化成 'k1=v1&k2=v2' 格式化的字符串 105 | * @param obj 106 | */ 107 | function stringifyQuery(obj) { 108 | const res = obj 109 | ? Object.keys(obj) 110 | .map(key => { 111 | const val = obj[key]; 112 | if (val === undefined) { 113 | return ''; 114 | } 115 | if (val === null) { 116 | return encode(key); 117 | } 118 | if (Array.isArray(val)) { 119 | const result = []; 120 | val.forEach(val2 => { 121 | if (val2 === undefined) { 122 | return; 123 | } 124 | if (val2 === null) { 125 | result.push(encode(key)); 126 | } 127 | else { 128 | result.push(encode(key) + '=' + encode(val2)); 129 | } 130 | }); 131 | return result.join('&'); 132 | } 133 | return encode(key) + '=' + encode(val); 134 | }) 135 | .filter(x => x.length > 0) 136 | .join('&') 137 | : null; 138 | return res ? `?${res}` : ''; 139 | } 140 | 141 | /** 142 | * 生成路由对象 143 | * @param routeConfig 144 | * @param query 145 | */ 146 | function generateRoute(routeConfig, query) { 147 | const route = { 148 | name: routeConfig.name, 149 | path: routeConfig.path, 150 | fullPath: routeConfig.path + stringifyQuery(query), 151 | query: Object.assign({}, query), 152 | meta: routeConfig.meta || {}, 153 | }; 154 | return route; 155 | } 156 | function registerHook(list, fn) { 157 | list.push(fn); 158 | return () => { 159 | const i = list.indexOf(fn); 160 | if (i > -1) 161 | list.splice(i, 1); 162 | }; 163 | } 164 | class Router { 165 | constructor(options = {}) { 166 | /** 所有页面的路由配置 */ 167 | this.routeConfigList = []; 168 | /** 路由适配器,负责具体的路由跳转行为 */ 169 | this.adapter = wechatAdapter; 170 | if (options.routes) { 171 | // 统一替换处理,不以/前缀开头 172 | options.routes.forEach((route) => route.path.replace(/^\//, '')); 173 | this.routeConfigList = options.routes; 174 | } 175 | if (options.adapter) { 176 | this.adapter = options.adapter; 177 | } 178 | this.beforeHooks = []; 179 | this.afterHooks = []; 180 | this.stackLength = 0; 181 | } 182 | /** 183 | * 注册路由前置守卫钩子 184 | * @param hook 钩子函数 185 | */ 186 | beforeEach(hook) { 187 | return registerHook(this.beforeHooks, hook); 188 | } 189 | /** 190 | * 注册路由后置守卫钩子 191 | * @param hook 钩子函数 192 | */ 193 | afterEach(hook) { 194 | return registerHook(this.afterHooks, hook); 195 | } 196 | switchRoute(location) { 197 | return new Promise(async (resolve, reject) => { 198 | let routeConfig = this.routeConfigList.find((item) => item.name === location.name); 199 | if (!routeConfig) { 200 | reject(new Error('未找到该路由:' + location.name)); 201 | return; 202 | } 203 | const currentRoute = this.getCurrentRoute(); 204 | const toRoute = generateRoute(routeConfig, location.query); 205 | if (!location.replace && 206 | !location.reLaunch && 207 | !routeConfig.isTab && 208 | this.stackLength >= Router.MAX_STACK_LENGTH) { 209 | console.warn('超出navigateTo最大限制,改用redirectTo'); 210 | location.replace = true; 211 | } 212 | const iterator = (hook, next) => { 213 | hook(toRoute, currentRoute, async (v) => { 214 | if (v === false) 215 | return; 216 | else if (typeof v === 'object') { 217 | try { 218 | await this.switchRoute(v); 219 | } 220 | catch (error) { 221 | reject(error); 222 | } 223 | } 224 | else { 225 | next(); 226 | } 227 | }); 228 | }; 229 | runQueue(this.beforeHooks, iterator, async () => { 230 | try { 231 | const result = await this.adapter({ 232 | // 非插件页跳转前统一加上 "/" 前缀 233 | path: (/^(plugin-private|plugin):\/\//.test(toRoute.fullPath) ? '' : '/') + 234 | toRoute.fullPath, 235 | isTab: routeConfig.isTab || false, 236 | replace: location.replace, 237 | reLaunch: location.reLaunch, 238 | events: location.events, 239 | }); 240 | resolve(result); 241 | this.afterHooks.forEach((hook) => { 242 | hook && hook(toRoute, currentRoute); 243 | }); 244 | } 245 | catch (error) { 246 | reject(error); 247 | } 248 | }); 249 | }); 250 | } 251 | /** 252 | * 保留当前页面,跳转到应用内的某个页面, 对应小程序的 `wx.navigateTo` , 使用 `router.back()` 可以返回到原页面。 253 | * 如果页面是 tabbar 会自动切换成 `wx.switchTab` 跳转; 254 | * 如果小程序中页面栈超过十层,会自动切换成 `wx.redirectTo` 跳转。 255 | * @param location 路由跳转参数 256 | */ 257 | push(location) { 258 | return this.switchRoute(location); 259 | } 260 | /** 261 | * 关闭当前页面,跳转到应用内的某个页面; 262 | * 如果页面是 `tabbar` 会自动切换成 `wx.switchTab` 跳转,但是 `tabbar` 不支持传递参数。 263 | * @param location 路由跳转参数 264 | */ 265 | replace(location) { 266 | location.replace = true; 267 | return this.switchRoute(location); 268 | } 269 | /** 270 | * 关闭当前页面,返回上一页面或多级页面 271 | * @param delta 返回的页面数,如果 delta 大于现有页面数,则返回到首页。 272 | */ 273 | back(delta = 1) { 274 | if (delta < 1) 275 | delta = 1; 276 | return new Promise((resolve, reject) => { 277 | wx.navigateBack({ 278 | delta, 279 | success: (res) => { 280 | // const route = this.stack[targetIndex] 281 | resolve(res); 282 | }, 283 | fail: (res) => { 284 | // {"errMsg":"navigateBack:fail cannot navigate back at first page."} 285 | reject(res); 286 | }, 287 | }); 288 | }); 289 | } 290 | /** 291 | * 关闭所有页面,打开到应用内的某个页面 292 | * @param location 路由跳转参数 293 | */ 294 | reLaunch(location) { 295 | location.reLaunch = true; 296 | return this.switchRoute(location); 297 | } 298 | /** 299 | * 根据路径获取路由配置 300 | * @param path 小程序路径path(不以/开头) 301 | */ 302 | getRouteConfigByPath(path) { 303 | if (path.indexOf('?') > -1) { 304 | path = path.substring(0, path.indexOf('?')); 305 | } 306 | return this.routeConfigList.find((item) => item.path === path); 307 | } 308 | /** 309 | * 获取当前路由 310 | */ 311 | getCurrentRoute() { 312 | let pages = getCurrentPages(); 313 | let currentPage = pages[pages.length - 1]; 314 | this.stackLength = pages.length; 315 | let routeConfig = this.getRouteConfigByPath(currentPage.route); 316 | if (!routeConfig) 317 | throw new Error('当前页面' + currentPage.route + '对应的路由未配置'); 318 | return generateRoute(routeConfig, currentPage.options); 319 | } 320 | } 321 | /** 路由栈上限数 */ 322 | Router.MAX_STACK_LENGTH = 10; 323 | /** 将 query 对象序列化成 'k1=v1&k2=v2' 格式化的字符串 */ 324 | Router.stringifyQuery = stringifyQuery; 325 | /** 将 'k1=v1&k2=v2' 格式的字符串转换成 query 对象 */ 326 | Router.resolveQuery = resolveQuery; 327 | 328 | export default Router; 329 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function wechatAdapter(adapterconfig) { 4 | return new Promise((resolve, reject) => { 5 | let jumpMethod; 6 | if (adapterconfig.reLaunch) { 7 | jumpMethod = wx.reLaunch; 8 | } 9 | else if (adapterconfig.isTab) { 10 | jumpMethod = wx.switchTab; 11 | } 12 | else if (adapterconfig.replace) { 13 | jumpMethod = wx.redirectTo; 14 | } 15 | else { 16 | jumpMethod = wx.navigateTo; 17 | } 18 | const params = { 19 | url: adapterconfig.path, 20 | success: (res) => { 21 | resolve(res); 22 | }, 23 | fail: (res) => { 24 | reject(res); 25 | }, 26 | }; 27 | if (adapterconfig.events) { 28 | params.events = adapterconfig.events; 29 | } 30 | jumpMethod.bind(wx)(params); 31 | }); 32 | } 33 | 34 | function runQueue(queue, fn, cb) { 35 | const step = (index) => { 36 | if (index >= queue.length) { 37 | cb(); 38 | } 39 | else { 40 | if (queue[index]) { 41 | fn(queue[index], () => { 42 | step(index + 1); 43 | }); 44 | } 45 | else { 46 | step(index + 1); 47 | } 48 | } 49 | }; 50 | step(0); 51 | } 52 | 53 | const encodeReserveRE = /[!'()*]/g; 54 | const encodeReserveReplacer = (c) => '%' + c.charCodeAt(0).toString(16); 55 | const commaRE = /%2C/g; 56 | // fixed encodeURIComponent which is more conformant to RFC3986: 57 | // - escapes [!'()*] 58 | // - preserve commas 59 | const encode = (str) => encodeURIComponent(str) 60 | .replace(encodeReserveRE, encodeReserveReplacer) 61 | .replace(commaRE, ','); 62 | const decode = decodeURIComponent; 63 | function parseQuery(query) { 64 | const res = {}; 65 | query = query.trim().replace(/^(\?|#|&)/, ''); 66 | if (!query) { 67 | return res; 68 | } 69 | query.split('&').forEach(param => { 70 | const parts = param.replace(/\+/g, ' ').split('='); 71 | const key = decode(parts.shift()); 72 | const val = parts.length > 0 ? decode(parts.join('=')) : null; 73 | if (res[key] === undefined) { 74 | res[key] = val; 75 | } 76 | else if (Array.isArray(res[key])) { 77 | res[key].push(val); 78 | } 79 | else { 80 | res[key] = [res[key], val]; 81 | } 82 | }); 83 | return res; 84 | } 85 | /** 86 | * 将 'k1=v1&k2=v2' 格式的字符串转换成 query 对象 87 | * @param query (可选)要转换的字符串,格式类似于 '?name=admin&password=123' 或者 'name=admin&password=123' 88 | * @param extraQuery (可选)将转换后的 query 对象 附加到此 query 对象身上 89 | * @param _parseQuery (可选)自定义转换函数 90 | */ 91 | function resolveQuery(query, extraQuery = {}, _parseQuery) { 92 | const parse = _parseQuery || parseQuery; 93 | let parsedQuery; 94 | try { 95 | parsedQuery = parse(query || ''); 96 | } 97 | catch (e) { 98 | parsedQuery = {}; 99 | } 100 | for (const key in extraQuery) { 101 | parsedQuery[key] = extraQuery[key]; 102 | } 103 | return parsedQuery; 104 | } 105 | /** 106 | * 将 query 对象序列化成 'k1=v1&k2=v2' 格式化的字符串 107 | * @param obj 108 | */ 109 | function stringifyQuery(obj) { 110 | const res = obj 111 | ? Object.keys(obj) 112 | .map(key => { 113 | const val = obj[key]; 114 | if (val === undefined) { 115 | return ''; 116 | } 117 | if (val === null) { 118 | return encode(key); 119 | } 120 | if (Array.isArray(val)) { 121 | const result = []; 122 | val.forEach(val2 => { 123 | if (val2 === undefined) { 124 | return; 125 | } 126 | if (val2 === null) { 127 | result.push(encode(key)); 128 | } 129 | else { 130 | result.push(encode(key) + '=' + encode(val2)); 131 | } 132 | }); 133 | return result.join('&'); 134 | } 135 | return encode(key) + '=' + encode(val); 136 | }) 137 | .filter(x => x.length > 0) 138 | .join('&') 139 | : null; 140 | return res ? `?${res}` : ''; 141 | } 142 | 143 | /** 144 | * 生成路由对象 145 | * @param routeConfig 146 | * @param query 147 | */ 148 | function generateRoute(routeConfig, query) { 149 | const route = { 150 | name: routeConfig.name, 151 | path: routeConfig.path, 152 | fullPath: routeConfig.path + stringifyQuery(query), 153 | query: Object.assign({}, query), 154 | meta: routeConfig.meta || {}, 155 | }; 156 | return route; 157 | } 158 | function registerHook(list, fn) { 159 | list.push(fn); 160 | return () => { 161 | const i = list.indexOf(fn); 162 | if (i > -1) 163 | list.splice(i, 1); 164 | }; 165 | } 166 | class Router { 167 | constructor(options = {}) { 168 | /** 所有页面的路由配置 */ 169 | this.routeConfigList = []; 170 | /** 路由适配器,负责具体的路由跳转行为 */ 171 | this.adapter = wechatAdapter; 172 | if (options.routes) { 173 | // 统一替换处理,不以/前缀开头 174 | options.routes.forEach((route) => route.path.replace(/^\//, '')); 175 | this.routeConfigList = options.routes; 176 | } 177 | if (options.adapter) { 178 | this.adapter = options.adapter; 179 | } 180 | this.beforeHooks = []; 181 | this.afterHooks = []; 182 | this.stackLength = 0; 183 | } 184 | /** 185 | * 注册路由前置守卫钩子 186 | * @param hook 钩子函数 187 | */ 188 | beforeEach(hook) { 189 | return registerHook(this.beforeHooks, hook); 190 | } 191 | /** 192 | * 注册路由后置守卫钩子 193 | * @param hook 钩子函数 194 | */ 195 | afterEach(hook) { 196 | return registerHook(this.afterHooks, hook); 197 | } 198 | switchRoute(location) { 199 | return new Promise(async (resolve, reject) => { 200 | let routeConfig = this.routeConfigList.find((item) => item.name === location.name); 201 | if (!routeConfig) { 202 | reject(new Error('未找到该路由:' + location.name)); 203 | return; 204 | } 205 | const currentRoute = this.getCurrentRoute(); 206 | const toRoute = generateRoute(routeConfig, location.query); 207 | if (!location.replace && 208 | !location.reLaunch && 209 | !routeConfig.isTab && 210 | this.stackLength >= Router.MAX_STACK_LENGTH) { 211 | console.warn('超出navigateTo最大限制,改用redirectTo'); 212 | location.replace = true; 213 | } 214 | const iterator = (hook, next) => { 215 | hook(toRoute, currentRoute, async (v) => { 216 | if (v === false) 217 | return; 218 | else if (typeof v === 'object') { 219 | try { 220 | await this.switchRoute(v); 221 | } 222 | catch (error) { 223 | reject(error); 224 | } 225 | } 226 | else { 227 | next(); 228 | } 229 | }); 230 | }; 231 | runQueue(this.beforeHooks, iterator, async () => { 232 | try { 233 | const result = await this.adapter({ 234 | // 非插件页跳转前统一加上 "/" 前缀 235 | path: (/^(plugin-private|plugin):\/\//.test(toRoute.fullPath) ? '' : '/') + 236 | toRoute.fullPath, 237 | isTab: routeConfig.isTab || false, 238 | replace: location.replace, 239 | reLaunch: location.reLaunch, 240 | events: location.events, 241 | }); 242 | resolve(result); 243 | this.afterHooks.forEach((hook) => { 244 | hook && hook(toRoute, currentRoute); 245 | }); 246 | } 247 | catch (error) { 248 | reject(error); 249 | } 250 | }); 251 | }); 252 | } 253 | /** 254 | * 保留当前页面,跳转到应用内的某个页面, 对应小程序的 `wx.navigateTo` , 使用 `router.back()` 可以返回到原页面。 255 | * 如果页面是 tabbar 会自动切换成 `wx.switchTab` 跳转; 256 | * 如果小程序中页面栈超过十层,会自动切换成 `wx.redirectTo` 跳转。 257 | * @param location 路由跳转参数 258 | */ 259 | push(location) { 260 | return this.switchRoute(location); 261 | } 262 | /** 263 | * 关闭当前页面,跳转到应用内的某个页面; 264 | * 如果页面是 `tabbar` 会自动切换成 `wx.switchTab` 跳转,但是 `tabbar` 不支持传递参数。 265 | * @param location 路由跳转参数 266 | */ 267 | replace(location) { 268 | location.replace = true; 269 | return this.switchRoute(location); 270 | } 271 | /** 272 | * 关闭当前页面,返回上一页面或多级页面 273 | * @param delta 返回的页面数,如果 delta 大于现有页面数,则返回到首页。 274 | */ 275 | back(delta = 1) { 276 | if (delta < 1) 277 | delta = 1; 278 | return new Promise((resolve, reject) => { 279 | wx.navigateBack({ 280 | delta, 281 | success: (res) => { 282 | // const route = this.stack[targetIndex] 283 | resolve(res); 284 | }, 285 | fail: (res) => { 286 | // {"errMsg":"navigateBack:fail cannot navigate back at first page."} 287 | reject(res); 288 | }, 289 | }); 290 | }); 291 | } 292 | /** 293 | * 关闭所有页面,打开到应用内的某个页面 294 | * @param location 路由跳转参数 295 | */ 296 | reLaunch(location) { 297 | location.reLaunch = true; 298 | return this.switchRoute(location); 299 | } 300 | /** 301 | * 根据路径获取路由配置 302 | * @param path 小程序路径path(不以/开头) 303 | */ 304 | getRouteConfigByPath(path) { 305 | if (path.indexOf('?') > -1) { 306 | path = path.substring(0, path.indexOf('?')); 307 | } 308 | return this.routeConfigList.find((item) => item.path === path); 309 | } 310 | /** 311 | * 获取当前路由 312 | */ 313 | getCurrentRoute() { 314 | let pages = getCurrentPages(); 315 | let currentPage = pages[pages.length - 1]; 316 | this.stackLength = pages.length; 317 | let routeConfig = this.getRouteConfigByPath(currentPage.route); 318 | if (!routeConfig) 319 | throw new Error('当前页面' + currentPage.route + '对应的路由未配置'); 320 | return generateRoute(routeConfig, currentPage.options); 321 | } 322 | } 323 | /** 路由栈上限数 */ 324 | Router.MAX_STACK_LENGTH = 10; 325 | /** 将 query 对象序列化成 'k1=v1&k2=v2' 格式化的字符串 */ 326 | Router.stringifyQuery = stringifyQuery; 327 | /** 将 'k1=v1&k2=v2' 格式的字符串转换成 query 对象 */ 328 | Router.resolveQuery = resolveQuery; 329 | 330 | module.exports = Router; 331 | module.exports.default = Router; 332 | -------------------------------------------------------------------------------- /dist/lib/adapter/wechatAdapter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | function wechatAdapter(adapterconfig) { 4 | return new Promise((resolve, reject) => { 5 | let jumpMethod; 6 | if (adapterconfig.reLaunch) { 7 | jumpMethod = wx.reLaunch; 8 | } 9 | else if (adapterconfig.isTab) { 10 | jumpMethod = wx.switchTab; 11 | } 12 | else if (adapterconfig.replace) { 13 | jumpMethod = wx.redirectTo; 14 | } 15 | else { 16 | jumpMethod = wx.navigateTo; 17 | } 18 | const params = { 19 | url: adapterconfig.path, 20 | success: (res) => { 21 | resolve(res); 22 | }, 23 | fail: (res) => { 24 | reject(res); 25 | }, 26 | }; 27 | if (adapterconfig.events) { 28 | params.events = adapterconfig.events; 29 | } 30 | jumpMethod.bind(wx)(params); 31 | }); 32 | } 33 | exports.default = wechatAdapter; 34 | //# sourceMappingURL=wechatAdapter.js.map -------------------------------------------------------------------------------- /dist/lib/adapter/wechatAdapter.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"wechatAdapter.js","sourceRoot":"","sources":["../../../src/adapter/wechatAdapter.ts"],"names":[],"mappings":";;AAEA,SAAwB,aAAa,CACnC,aAA4B;IAE5B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,UAAe,CAAA;QACnB,IAAI,aAAa,CAAC,QAAQ,EAAE;YAC1B,UAAU,GAAG,EAAE,CAAC,QAAQ,CAAA;SACzB;aAAM,IAAI,aAAa,CAAC,KAAK,EAAE;YAC9B,UAAU,GAAG,EAAE,CAAC,SAAS,CAAA;SAC1B;aAAM,IAAI,aAAa,CAAC,OAAO,EAAE;YAChC,UAAU,GAAG,EAAE,CAAC,UAAU,CAAA;SAC3B;aAAM;YACL,UAAU,GAAG,EAAE,CAAC,UAAU,CAAA;SAC3B;QACD,MAAM,MAAM,GAAuC;YACjD,GAAG,EAAE,aAAa,CAAC,IAAI;YACvB,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;gBACf,OAAO,CAAC,GAAG,CAAC,CAAA;YACd,CAAC;YACD,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE;gBACZ,MAAM,CAAC,GAAG,CAAC,CAAA;YACb,CAAC;SACF,CAAA;QAED,IAAI,aAAa,CAAC,MAAM,EAAE;YACxB,MAAM,CAAC,MAAM,GAAG,aAAa,CAAC,MAAM,CAAA;SACrC;QACD,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAA;IAC7B,CAAC,CAAC,CAAA;AACJ,CAAC;AA7BD,gCA6BC"} -------------------------------------------------------------------------------- /dist/lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const wechatAdapter_1 = require("./adapter/wechatAdapter"); 4 | const async_1 = require("./utils/async"); 5 | const query_1 = require("./utils/query"); 6 | /** 7 | * 生成路由对象 8 | * @param routeConfig 9 | * @param query 10 | */ 11 | function generateRoute(routeConfig, query) { 12 | const route = { 13 | name: routeConfig.name, 14 | path: routeConfig.path, 15 | fullPath: routeConfig.path + query_1.stringifyQuery(query), 16 | query: Object.assign({}, query), 17 | meta: routeConfig.meta || {}, 18 | }; 19 | return route; 20 | } 21 | function registerHook(list, fn) { 22 | list.push(fn); 23 | return () => { 24 | const i = list.indexOf(fn); 25 | if (i > -1) 26 | list.splice(i, 1); 27 | }; 28 | } 29 | class Router { 30 | constructor(options = {}) { 31 | /** 所有页面的路由配置 */ 32 | this.routeConfigList = []; 33 | /** 路由适配器,负责具体的路由跳转行为 */ 34 | this.adapter = wechatAdapter_1.default; 35 | if (options.routes) { 36 | // 统一替换处理,不以/前缀开头 37 | options.routes.forEach((route) => route.path.replace(/^\//, '')); 38 | this.routeConfigList = options.routes; 39 | } 40 | if (options.adapter) { 41 | this.adapter = options.adapter; 42 | } 43 | this.beforeHooks = []; 44 | this.afterHooks = []; 45 | this.stackLength = 0; 46 | } 47 | /** 48 | * 注册路由前置守卫钩子 49 | * @param hook 钩子函数 50 | */ 51 | beforeEach(hook) { 52 | return registerHook(this.beforeHooks, hook); 53 | } 54 | /** 55 | * 注册路由后置守卫钩子 56 | * @param hook 钩子函数 57 | */ 58 | afterEach(hook) { 59 | return registerHook(this.afterHooks, hook); 60 | } 61 | switchRoute(location) { 62 | return new Promise(async (resolve, reject) => { 63 | let routeConfig = this.routeConfigList.find((item) => item.name === location.name); 64 | if (!routeConfig) { 65 | reject(new Error('未找到该路由:' + location.name)); 66 | return; 67 | } 68 | const currentRoute = this.getCurrentRoute(); 69 | const toRoute = generateRoute(routeConfig, location.query); 70 | if (!location.replace && 71 | !location.reLaunch && 72 | !routeConfig.isTab && 73 | this.stackLength >= Router.MAX_STACK_LENGTH) { 74 | console.warn('超出navigateTo最大限制,改用redirectTo'); 75 | location.replace = true; 76 | } 77 | const iterator = (hook, next) => { 78 | hook(toRoute, currentRoute, async (v) => { 79 | if (v === false) 80 | return; 81 | else if (typeof v === 'object') { 82 | try { 83 | await this.switchRoute(v); 84 | } 85 | catch (error) { 86 | reject(error); 87 | } 88 | } 89 | else { 90 | next(); 91 | } 92 | }); 93 | }; 94 | async_1.runQueue(this.beforeHooks, iterator, async () => { 95 | try { 96 | const result = await this.adapter({ 97 | // 非插件页跳转前统一加上 "/" 前缀 98 | path: (/^(plugin-private|plugin):\/\//.test(toRoute.fullPath) ? '' : '/') + 99 | toRoute.fullPath, 100 | isTab: routeConfig.isTab || false, 101 | replace: location.replace, 102 | reLaunch: location.reLaunch, 103 | events: location.events, 104 | }); 105 | resolve(result); 106 | this.afterHooks.forEach((hook) => { 107 | hook && hook(toRoute, currentRoute); 108 | }); 109 | } 110 | catch (error) { 111 | reject(error); 112 | } 113 | }); 114 | }); 115 | } 116 | /** 117 | * 保留当前页面,跳转到应用内的某个页面, 对应小程序的 `wx.navigateTo` , 使用 `router.back()` 可以返回到原页面。 118 | * 如果页面是 tabbar 会自动切换成 `wx.switchTab` 跳转; 119 | * 如果小程序中页面栈超过十层,会自动切换成 `wx.redirectTo` 跳转。 120 | * @param location 路由跳转参数 121 | */ 122 | push(location) { 123 | return this.switchRoute(location); 124 | } 125 | /** 126 | * 关闭当前页面,跳转到应用内的某个页面; 127 | * 如果页面是 `tabbar` 会自动切换成 `wx.switchTab` 跳转,但是 `tabbar` 不支持传递参数。 128 | * @param location 路由跳转参数 129 | */ 130 | replace(location) { 131 | ; 132 | location.replace = true; 133 | return this.switchRoute(location); 134 | } 135 | /** 136 | * 关闭当前页面,返回上一页面或多级页面 137 | * @param delta 返回的页面数,如果 delta 大于现有页面数,则返回到首页。 138 | */ 139 | back(delta = 1) { 140 | if (delta < 1) 141 | delta = 1; 142 | return new Promise((resolve, reject) => { 143 | wx.navigateBack({ 144 | delta, 145 | success: (res) => { 146 | // const route = this.stack[targetIndex] 147 | resolve(res); 148 | }, 149 | fail: (res) => { 150 | // {"errMsg":"navigateBack:fail cannot navigate back at first page."} 151 | reject(res); 152 | }, 153 | }); 154 | }); 155 | } 156 | /** 157 | * 关闭所有页面,打开到应用内的某个页面 158 | * @param location 路由跳转参数 159 | */ 160 | reLaunch(location) { 161 | ; 162 | location.reLaunch = true; 163 | return this.switchRoute(location); 164 | } 165 | /** 166 | * 根据路径获取路由配置 167 | * @param path 小程序路径path(不以/开头) 168 | */ 169 | getRouteConfigByPath(path) { 170 | if (path.indexOf('?') > -1) { 171 | path = path.substring(0, path.indexOf('?')); 172 | } 173 | return this.routeConfigList.find((item) => item.path === path); 174 | } 175 | /** 176 | * 获取当前路由 177 | */ 178 | getCurrentRoute() { 179 | let pages = getCurrentPages(); 180 | let currentPage = pages[pages.length - 1]; 181 | this.stackLength = pages.length; 182 | let routeConfig = this.getRouteConfigByPath(currentPage.route); 183 | if (!routeConfig) 184 | throw new Error('当前页面' + currentPage.route + '对应的路由未配置'); 185 | return generateRoute(routeConfig, currentPage.options); 186 | } 187 | } 188 | exports.default = Router; 189 | /** 路由栈上限数 */ 190 | Router.MAX_STACK_LENGTH = 10; 191 | /** 将 query 对象序列化成 'k1=v1&k2=v2' 格式化的字符串 */ 192 | Router.stringifyQuery = query_1.stringifyQuery; 193 | /** 将 'k1=v1&k2=v2' 格式的字符串转换成 query 对象 */ 194 | Router.resolveQuery = query_1.resolveQuery; 195 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/lib/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/lib/utils/async.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.runQueue = void 0; 4 | function runQueue(queue, fn, cb) { 5 | const step = (index) => { 6 | if (index >= queue.length) { 7 | cb(); 8 | } 9 | else { 10 | if (queue[index]) { 11 | fn(queue[index], () => { 12 | step(index + 1); 13 | }); 14 | } 15 | else { 16 | step(index + 1); 17 | } 18 | } 19 | }; 20 | step(0); 21 | } 22 | exports.runQueue = runQueue; 23 | //# sourceMappingURL=async.js.map -------------------------------------------------------------------------------- /dist/lib/utils/async.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"async.js","sourceRoot":"","sources":["../../../src/utils/async.ts"],"names":[],"mappings":";;;AAEA,SAAgB,QAAQ,CAAC,KAAwB,EAAE,EAAY,EAAE,EAAY;IAC3E,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,EAAE;QAC7B,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE;YACzB,EAAE,EAAE,CAAA;SACL;aAAM;YACL,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE;gBAChB,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE;oBACpB,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;gBACjB,CAAC,CAAC,CAAA;aACH;iBAAM;gBACL,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;aAChB;SACF;IACH,CAAC,CAAA;IACD,IAAI,CAAC,CAAC,CAAC,CAAA;AACT,CAAC;AAfD,4BAeC"} -------------------------------------------------------------------------------- /dist/lib/utils/query.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.stringifyQuery = exports.resolveQuery = void 0; 4 | const encodeReserveRE = /[!'()*]/g; 5 | const encodeReserveReplacer = (c) => '%' + c.charCodeAt(0).toString(16); 6 | const commaRE = /%2C/g; 7 | // fixed encodeURIComponent which is more conformant to RFC3986: 8 | // - escapes [!'()*] 9 | // - preserve commas 10 | const encode = (str) => encodeURIComponent(str) 11 | .replace(encodeReserveRE, encodeReserveReplacer) 12 | .replace(commaRE, ','); 13 | const decode = decodeURIComponent; 14 | function parseQuery(query) { 15 | const res = {}; 16 | query = query.trim().replace(/^(\?|#|&)/, ''); 17 | if (!query) { 18 | return res; 19 | } 20 | query.split('&').forEach(param => { 21 | const parts = param.replace(/\+/g, ' ').split('='); 22 | const key = decode(parts.shift()); 23 | const val = parts.length > 0 ? decode(parts.join('=')) : null; 24 | if (res[key] === undefined) { 25 | res[key] = val; 26 | } 27 | else if (Array.isArray(res[key])) { 28 | res[key].push(val); 29 | } 30 | else { 31 | res[key] = [res[key], val]; 32 | } 33 | }); 34 | return res; 35 | } 36 | /** 37 | * 将 'k1=v1&k2=v2' 格式的字符串转换成 query 对象 38 | * @param query (可选)要转换的字符串,格式类似于 '?name=admin&password=123' 或者 'name=admin&password=123' 39 | * @param extraQuery (可选)将转换后的 query 对象 附加到此 query 对象身上 40 | * @param _parseQuery (可选)自定义转换函数 41 | */ 42 | function resolveQuery(query, extraQuery = {}, _parseQuery) { 43 | const parse = _parseQuery || parseQuery; 44 | let parsedQuery; 45 | try { 46 | parsedQuery = parse(query || ''); 47 | } 48 | catch (e) { 49 | parsedQuery = {}; 50 | } 51 | for (const key in extraQuery) { 52 | parsedQuery[key] = extraQuery[key]; 53 | } 54 | return parsedQuery; 55 | } 56 | exports.resolveQuery = resolveQuery; 57 | /** 58 | * 将 query 对象序列化成 'k1=v1&k2=v2' 格式化的字符串 59 | * @param obj 60 | */ 61 | function stringifyQuery(obj) { 62 | const res = obj 63 | ? Object.keys(obj) 64 | .map(key => { 65 | const val = obj[key]; 66 | if (val === undefined) { 67 | return ''; 68 | } 69 | if (val === null) { 70 | return encode(key); 71 | } 72 | if (Array.isArray(val)) { 73 | const result = []; 74 | val.forEach(val2 => { 75 | if (val2 === undefined) { 76 | return; 77 | } 78 | if (val2 === null) { 79 | result.push(encode(key)); 80 | } 81 | else { 82 | result.push(encode(key) + '=' + encode(val2)); 83 | } 84 | }); 85 | return result.join('&'); 86 | } 87 | return encode(key) + '=' + encode(val); 88 | }) 89 | .filter(x => x.length > 0) 90 | .join('&') 91 | : null; 92 | return res ? `?${res}` : ''; 93 | } 94 | exports.stringifyQuery = stringifyQuery; 95 | //# sourceMappingURL=query.js.map -------------------------------------------------------------------------------- /dist/lib/utils/query.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"query.js","sourceRoot":"","sources":["../../../src/utils/query.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/types/adapter/wechatAdapter.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { AdapterConfig } from '../index'; 3 | export default function wechatAdapter(adapterconfig: AdapterConfig): Promise; 4 | -------------------------------------------------------------------------------- /dist/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { stringifyQuery, resolveQuery } from './utils/query'; 3 | /** 4 | * Route 构造实例选项 5 | */ 6 | interface RouterOptions { 7 | routes?: RouteConfig[]; 8 | adapter?: RouteAdapter; 9 | } 10 | /** 11 | * 路由定义时的配置 12 | */ 13 | export interface RouteConfig { 14 | /** 路由name,需保证唯一 */ 15 | name: string; 16 | /** 路由对应的小程序页面path(和app.json保持一致,路径不以/开头) */ 17 | path: string; 18 | /** 是否是tab页面,默认false */ 19 | isTab?: boolean; 20 | /** 携带的额外的参数 */ 21 | meta?: { 22 | [key: string]: any; 23 | }; 24 | } 25 | interface Dictionary { 26 | [key: string]: T; 27 | } 28 | export interface BaseLocation { 29 | /** 页面对应的路由名称 */ 30 | name: string; 31 | /** 传参(对`tabbar`页面无效) */ 32 | query?: Dictionary; 33 | } 34 | /** 35 | * 路由push函数调用时的传参 36 | */ 37 | export interface PushLocation extends BaseLocation { 38 | /** 是否使用重定向,默认false */ 39 | replace?: boolean; 40 | /** 页面间通信接口,用于监听被打开页面发送到当前页面的数据。基础库 2.7.3 开始支持。仅对 router.push 支持 */ 41 | events?: WechatMiniprogram.IAnyObject; 42 | } 43 | /** 44 | * 路由函数调用时的完整传参 45 | */ 46 | export interface Location extends PushLocation { 47 | /** 是否使用重定向 */ 48 | replace?: boolean; 49 | /** 是否关闭所有页面,打开到应用内的某个页面 */ 50 | reLaunch?: boolean; 51 | } 52 | /** 53 | * 路由对象 54 | */ 55 | export interface Route { 56 | path: string; 57 | name?: string; 58 | query: Dictionary; 59 | fullPath: string; 60 | meta?: any; 61 | } 62 | /** 63 | * 导航卫士 64 | */ 65 | export declare type NavigationGuard = ( 66 | /** 即将要进入的目标 路由对象 */ 67 | to: Route, 68 | /** 当前导航正要离开的路由 */ 69 | from: Route, next: (to?: Location | false) => void) => any; 70 | export interface AdapterConfig { 71 | /** 带参数的小程序完整路径 */ 72 | path: string; 73 | /** 是否是 tab 页面 */ 74 | isTab: boolean; 75 | /** 是否使用重定向 */ 76 | replace?: boolean; 77 | /** 是否关闭所有页面,打开到应用内的某个页面 */ 78 | reLaunch?: boolean; 79 | /** 页面间通信接口,用于监听被打开页面发送到当前页面的数据。基础库 2.7.3 开始支持。仅对 router.push 支持 */ 80 | events?: WechatMiniprogram.IAnyObject; 81 | } 82 | /** 83 | * 路由适配器 84 | */ 85 | export interface RouteAdapter { 86 | (config: AdapterConfig): Promise; 87 | } 88 | /** 89 | * 后置导航卫士 90 | */ 91 | export declare type AfterNavigationHook = (to: Route, from: Route) => any; 92 | export default class Router { 93 | /** 所有页面的路由配置 */ 94 | private routeConfigList; 95 | /** 全局前置守卫钩子 */ 96 | private beforeHooks; 97 | /** 全局后置钩子 */ 98 | private afterHooks; 99 | /** 当前路由堆栈数量 */ 100 | private stackLength; 101 | /** 路由适配器,负责具体的路由跳转行为 */ 102 | private adapter; 103 | /** 路由栈上限数 */ 104 | static MAX_STACK_LENGTH: number; 105 | /** 将 query 对象序列化成 'k1=v1&k2=v2' 格式化的字符串 */ 106 | static stringifyQuery: typeof stringifyQuery; 107 | /** 将 'k1=v1&k2=v2' 格式的字符串转换成 query 对象 */ 108 | static resolveQuery: typeof resolveQuery; 109 | constructor(options?: RouterOptions); 110 | /** 111 | * 注册路由前置守卫钩子 112 | * @param hook 钩子函数 113 | */ 114 | beforeEach(hook: NavigationGuard): Function; 115 | /** 116 | * 注册路由后置守卫钩子 117 | * @param hook 钩子函数 118 | */ 119 | afterEach(hook: AfterNavigationHook): Function; 120 | private switchRoute; 121 | /** 122 | * 保留当前页面,跳转到应用内的某个页面, 对应小程序的 `wx.navigateTo` , 使用 `router.back()` 可以返回到原页面。 123 | * 如果页面是 tabbar 会自动切换成 `wx.switchTab` 跳转; 124 | * 如果小程序中页面栈超过十层,会自动切换成 `wx.redirectTo` 跳转。 125 | * @param location 路由跳转参数 126 | */ 127 | push(location: PushLocation): Promise; 128 | /** 129 | * 关闭当前页面,跳转到应用内的某个页面; 130 | * 如果页面是 `tabbar` 会自动切换成 `wx.switchTab` 跳转,但是 `tabbar` 不支持传递参数。 131 | * @param location 路由跳转参数 132 | */ 133 | replace(location: BaseLocation): Promise; 134 | /** 135 | * 关闭当前页面,返回上一页面或多级页面 136 | * @param delta 返回的页面数,如果 delta 大于现有页面数,则返回到首页。 137 | */ 138 | back(delta?: number): Promise; 139 | /** 140 | * 关闭所有页面,打开到应用内的某个页面 141 | * @param location 路由跳转参数 142 | */ 143 | reLaunch(location: BaseLocation): Promise; 144 | /** 145 | * 根据路径获取路由配置 146 | * @param path 小程序路径path(不以/开头) 147 | */ 148 | getRouteConfigByPath(path: string): RouteConfig | undefined; 149 | /** 150 | * 获取当前路由 151 | */ 152 | getCurrentRoute(): Route; 153 | } 154 | export {}; 155 | -------------------------------------------------------------------------------- /dist/types/utils/async.d.ts: -------------------------------------------------------------------------------- 1 | import { NavigationGuard } from '../index'; 2 | export declare function runQueue(queue: NavigationGuard[], fn: Function, cb: Function): void; 3 | -------------------------------------------------------------------------------- /dist/types/utils/query.d.ts: -------------------------------------------------------------------------------- 1 | interface Dictionary { 2 | [index: string]: any; 3 | } 4 | /** 5 | * 将 'k1=v1&k2=v2' 格式的字符串转换成 query 对象 6 | * @param query (可选)要转换的字符串,格式类似于 '?name=admin&password=123' 或者 'name=admin&password=123' 7 | * @param extraQuery (可选)将转换后的 query 对象 附加到此 query 对象身上 8 | * @param _parseQuery (可选)自定义转换函数 9 | */ 10 | export declare function resolveQuery(query?: string, extraQuery?: Dictionary, _parseQuery?: Function): Dictionary; 11 | /** 12 | * 将 query 对象序列化成 'k1=v1&k2=v2' 格式化的字符串 13 | * @param obj 14 | */ 15 | export declare function stringifyQuery(obj: Dictionary): string; 16 | export {}; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cheers-mp-router", 3 | "version": "1.2.2", 4 | "description": "小程序命名路由", 5 | "keywords": [], 6 | "main": "dist/index.js", 7 | "module": "dist/index.es.js", 8 | "typings": "dist/types/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "author": "bigMeow ", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/bigmeow/cheers-mp-router.git" 16 | }, 17 | "license": "MIT", 18 | "engines": { 19 | "node": ">=10.0.0" 20 | }, 21 | "scripts": { 22 | "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", 23 | "prebuild": "rimraf dist", 24 | "build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --out docs --target es6 --theme minimal --mode file src", 25 | "start": "rollup -c rollup.config.ts -w", 26 | "test": "jest --coverage", 27 | "test:watch": "jest --coverage --watch", 28 | "test:prod": "npm run lint && npm run test -- --no-cache", 29 | "deploy-docs": "ts-node tools/gh-pages-publish", 30 | "report-coverage": "cat ./coverage/lcov.info | coveralls", 31 | "commit": "git-cz", 32 | "semantic-release": "semantic-release", 33 | "semantic-release-prepare": "ts-node tools/semantic-release-prepare", 34 | "precommit": "lint-staged", 35 | "travis-deploy-once": "travis-deploy-once" 36 | }, 37 | "lint-staged": { 38 | "{src,test}/**/*.ts": [ 39 | "prettier --write", 40 | "git add" 41 | ] 42 | }, 43 | "config": { 44 | "commitizen": { 45 | "path": "node_modules/cz-conventional-changelog" 46 | } 47 | }, 48 | "jest": { 49 | "transform": { 50 | ".(ts|tsx)": "ts-jest" 51 | }, 52 | "testEnvironment": "node", 53 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 54 | "moduleFileExtensions": [ 55 | "ts", 56 | "tsx", 57 | "js" 58 | ], 59 | "coveragePathIgnorePatterns": [ 60 | "/node_modules/", 61 | "/test/" 62 | ], 63 | "coverageThreshold": { 64 | "global": { 65 | "branches": 90, 66 | "functions": 95, 67 | "lines": 95, 68 | "statements": 95 69 | } 70 | }, 71 | "collectCoverageFrom": [ 72 | "src/*.{js,ts}" 73 | ] 74 | }, 75 | "prettier": { 76 | "semi": false, 77 | "singleQuote": true 78 | }, 79 | "commitlint": { 80 | "extends": [ 81 | "@commitlint/config-conventional" 82 | ] 83 | }, 84 | "devDependencies": { 85 | "@commitlint/cli": "^7.1.2", 86 | "@commitlint/config-conventional": "^7.1.2", 87 | "@rollup/plugin-commonjs": "^11.1.0", 88 | "@rollup/plugin-json": "^4.0.2", 89 | "@rollup/plugin-node-resolve": "^7.1.3", 90 | "@types/jest": "^23.3.2", 91 | "@types/node": "^10.11.0", 92 | "colors": "^1.3.2", 93 | "commitizen": "^3.0.0", 94 | "coveralls": "^3.0.2", 95 | "cross-env": "^5.2.0", 96 | "cz-conventional-changelog": "^2.1.0", 97 | "husky": "^1.0.1", 98 | "jest": "^23.6.0", 99 | "jest-config": "^23.6.0", 100 | "lint-staged": "^8.0.0", 101 | "lodash.camelcase": "^4.3.0", 102 | "miniprogram-api-typings": "^3.3.1", 103 | "prettier": "^2.0.4", 104 | "prompt": "^1.0.0", 105 | "replace-in-file": "^3.4.2", 106 | "rimraf": "^3.0.2", 107 | "rollup": "^2.6.0", 108 | "rollup-plugin-sourcemaps": "^0.5.0", 109 | "rollup-plugin-typescript2": "^0.27.0", 110 | "semantic-release": "^15.9.16", 111 | "shelljs": "^0.8.3", 112 | "travis-deploy-once": "^5.0.9", 113 | "ts-jest": "^25.3.1", 114 | "ts-node": "^8.8.2", 115 | "tslint": "^6.1.1", 116 | "tslint-config-prettier": "^1.18.0", 117 | "tslint-config-standard": "^9.0.0", 118 | "typedoc": "^0.17.4", 119 | "typescript": "^3.8.3" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import camelCase from 'lodash.camelcase' 4 | import typescript from 'rollup-plugin-typescript2' 5 | import json from '@rollup/plugin-json' 6 | 7 | const pkg = require('./package.json') 8 | 9 | const libraryName = 'index' 10 | 11 | export default { 12 | input: `src/${libraryName}.ts`, 13 | sourceMap: false, 14 | output: [ 15 | { file: pkg.main, name: camelCase(libraryName), format: 'cjs', sourcemap: false, exports: 'default' }, 16 | { file: pkg.module, format: 'es', sourcemap: false }, 17 | ], 18 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 19 | external: [], 20 | watch: { 21 | include: 'src/**', 22 | }, 23 | plugins: [ 24 | // Allow json resolution 25 | json(), 26 | // Compile TypeScript files 27 | typescript({ useTsconfigDeclarationDir: true }), 28 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 29 | commonjs(), 30 | // Allow node_modules resolution, so you can use 'external' to control 31 | // which external modules to include in the bundle 32 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 33 | resolve() 34 | ], 35 | } 36 | -------------------------------------------------------------------------------- /src/adapter/wechatAdapter.ts: -------------------------------------------------------------------------------- 1 | import { AdapterConfig } from '../index' 2 | 3 | export default function wechatAdapter( 4 | adapterconfig: AdapterConfig 5 | ): Promise { 6 | return new Promise((resolve, reject) => { 7 | let jumpMethod: any 8 | if (adapterconfig.reLaunch) { 9 | jumpMethod = wx.reLaunch 10 | } else if (adapterconfig.isTab) { 11 | jumpMethod = wx.switchTab 12 | } else if (adapterconfig.replace) { 13 | jumpMethod = wx.redirectTo 14 | } else { 15 | jumpMethod = wx.navigateTo 16 | } 17 | const params: WechatMiniprogram.NavigateToOption = { 18 | url: adapterconfig.path, 19 | success: (res) => { 20 | resolve(res) 21 | }, 22 | fail: (res) => { 23 | reject(res) 24 | }, 25 | } 26 | 27 | if (adapterconfig.events) { 28 | params.events = adapterconfig.events 29 | } 30 | jumpMethod.bind(wx)(params) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import defaultAdapter from './adapter/wechatAdapter' 2 | import { runQueue } from './utils/async' 3 | import { stringifyQuery, resolveQuery } from './utils/query' 4 | 5 | /** 6 | * Route 构造实例选项 7 | */ 8 | interface RouterOptions { 9 | routes?: RouteConfig[] 10 | adapter?: RouteAdapter 11 | } 12 | 13 | /** 14 | * 路由定义时的配置 15 | */ 16 | export interface RouteConfig { 17 | /** 路由name,需保证唯一 */ 18 | name: string 19 | /** 路由对应的小程序页面path(和app.json保持一致,路径不以/开头) */ 20 | path: string 21 | /** 是否是tab页面,默认false */ 22 | isTab?: boolean 23 | /** 携带的额外的参数 */ 24 | meta?: { 25 | [key: string]: any 26 | } 27 | } 28 | 29 | interface Dictionary { 30 | [key: string]: T 31 | } 32 | 33 | export interface BaseLocation { 34 | /** 页面对应的路由名称 */ 35 | name: string 36 | /** 传参(对`tabbar`页面无效) */ 37 | query?: Dictionary 38 | } 39 | 40 | /** 41 | * 路由push函数调用时的传参 42 | */ 43 | export interface PushLocation extends BaseLocation { 44 | /** 是否使用重定向,默认false */ 45 | replace?: boolean 46 | /** 页面间通信接口,用于监听被打开页面发送到当前页面的数据。基础库 2.7.3 开始支持。仅对 router.push 支持 */ 47 | events?: WechatMiniprogram.IAnyObject 48 | } 49 | 50 | /** 51 | * 路由函数调用时的完整传参 52 | */ 53 | export interface Location extends PushLocation { 54 | /** 是否使用重定向 */ 55 | replace?: boolean 56 | /** 是否关闭所有页面,打开到应用内的某个页面 */ 57 | reLaunch?: boolean 58 | } 59 | 60 | /** 61 | * 路由对象 62 | */ 63 | export interface Route { 64 | path: string 65 | name?: string 66 | query: Dictionary 67 | fullPath: string 68 | meta?: any 69 | } 70 | 71 | /** 72 | * 导航卫士 73 | */ 74 | export type NavigationGuard = ( 75 | /** 即将要进入的目标 路由对象 */ 76 | to: Route, 77 | /** 当前导航正要离开的路由 */ 78 | from: Route, 79 | next: (to?: Location | false) => void 80 | ) => any 81 | 82 | export interface AdapterConfig { 83 | /** 带参数的小程序完整路径 */ 84 | path: string 85 | /** 是否是 tab 页面 */ 86 | isTab: boolean 87 | /** 是否使用重定向 */ 88 | replace?: boolean 89 | /** 是否关闭所有页面,打开到应用内的某个页面 */ 90 | reLaunch?: boolean 91 | /** 页面间通信接口,用于监听被打开页面发送到当前页面的数据。基础库 2.7.3 开始支持。仅对 router.push 支持 */ 92 | events?: WechatMiniprogram.IAnyObject 93 | } 94 | /** 95 | * 路由适配器 96 | */ 97 | export interface RouteAdapter { 98 | (config: AdapterConfig): Promise 99 | } 100 | 101 | /** 102 | * 后置导航卫士 103 | */ 104 | export declare type AfterNavigationHook = (to: Route, from: Route) => any 105 | 106 | /** 107 | * 生成路由对象 108 | * @param routeConfig 109 | * @param query 110 | */ 111 | function generateRoute(routeConfig: RouteConfig, query: any) { 112 | const route: Route = { 113 | name: routeConfig.name, 114 | path: routeConfig.path, 115 | fullPath: routeConfig.path + stringifyQuery(query), 116 | query: Object.assign({}, query), 117 | meta: routeConfig.meta || {}, 118 | } 119 | return route 120 | } 121 | 122 | function registerHook(list: any[], fn: Function): Function { 123 | list.push(fn) 124 | return () => { 125 | const i = list.indexOf(fn) 126 | if (i > -1) list.splice(i, 1) 127 | } 128 | } 129 | 130 | export default class Router { 131 | /** 所有页面的路由配置 */ 132 | private routeConfigList: RouteConfig[] = [] 133 | 134 | /** 全局前置守卫钩子 */ 135 | private beforeHooks: NavigationGuard[] 136 | 137 | /** 全局后置钩子 */ 138 | private afterHooks: AfterNavigationHook[] 139 | 140 | /** 当前路由堆栈数量 */ 141 | private stackLength: number 142 | 143 | /** 路由适配器,负责具体的路由跳转行为 */ 144 | private adapter: RouteAdapter = defaultAdapter 145 | 146 | /** 路由栈上限数 */ 147 | public static MAX_STACK_LENGTH = 10 148 | 149 | /** 将 query 对象序列化成 'k1=v1&k2=v2' 格式化的字符串 */ 150 | public static stringifyQuery = stringifyQuery 151 | 152 | /** 将 'k1=v1&k2=v2' 格式的字符串转换成 query 对象 */ 153 | public static resolveQuery = resolveQuery 154 | 155 | constructor(options: RouterOptions = {}) { 156 | if (options.routes) { 157 | // 统一替换处理,不以/前缀开头 158 | options.routes.forEach((route) => route.path.replace(/^\//, '')) 159 | this.routeConfigList = options.routes 160 | } 161 | if (options.adapter) { 162 | this.adapter = options.adapter 163 | } 164 | this.beforeHooks = [] 165 | this.afterHooks = [] 166 | this.stackLength = 0 167 | } 168 | 169 | /** 170 | * 注册路由前置守卫钩子 171 | * @param hook 钩子函数 172 | */ 173 | beforeEach(hook: NavigationGuard): Function { 174 | return registerHook(this.beforeHooks, hook) 175 | } 176 | 177 | /** 178 | * 注册路由后置守卫钩子 179 | * @param hook 钩子函数 180 | */ 181 | afterEach(hook: AfterNavigationHook): Function { 182 | return registerHook(this.afterHooks, hook) 183 | } 184 | 185 | private switchRoute(location: Location): Promise { 186 | return new Promise(async (resolve, reject) => { 187 | let routeConfig = this.routeConfigList.find((item) => item.name === location.name) 188 | if (!routeConfig) { 189 | reject(new Error('未找到该路由:' + location.name)) 190 | return 191 | } 192 | const currentRoute = this.getCurrentRoute() 193 | const toRoute = generateRoute(routeConfig, location.query) 194 | if ( 195 | !location.replace && 196 | !location.reLaunch && 197 | !routeConfig.isTab && 198 | this.stackLength >= Router.MAX_STACK_LENGTH 199 | ) { 200 | console.warn('超出navigateTo最大限制,改用redirectTo') 201 | location.replace = true 202 | } 203 | 204 | const iterator = (hook: NavigationGuard, next: Function) => { 205 | hook(toRoute, currentRoute, async (v) => { 206 | if (v === false) return 207 | else if (typeof v === 'object') { 208 | try { 209 | await this.switchRoute(v) 210 | } catch (error) { 211 | reject(error) 212 | } 213 | } else { 214 | next() 215 | } 216 | }) 217 | } 218 | runQueue(this.beforeHooks, iterator, async () => { 219 | try { 220 | const result = await this.adapter({ 221 | // 非插件页跳转前统一加上 "/" 前缀 222 | path: 223 | (/^(plugin-private|plugin):\/\//.test(toRoute.fullPath) ? '' : '/') + 224 | toRoute.fullPath, 225 | isTab: routeConfig!.isTab || false, 226 | replace: location.replace, 227 | reLaunch: location.reLaunch, 228 | events: location.events, 229 | }) 230 | resolve(result) 231 | this.afterHooks.forEach((hook) => { 232 | hook && hook(toRoute, currentRoute) 233 | }) 234 | } catch (error) { 235 | reject(error) 236 | } 237 | }) 238 | }) 239 | } 240 | 241 | /** 242 | * 保留当前页面,跳转到应用内的某个页面, 对应小程序的 `wx.navigateTo` , 使用 `router.back()` 可以返回到原页面。 243 | * 如果页面是 tabbar 会自动切换成 `wx.switchTab` 跳转; 244 | * 如果小程序中页面栈超过十层,会自动切换成 `wx.redirectTo` 跳转。 245 | * @param location 路由跳转参数 246 | */ 247 | push(location: PushLocation): Promise { 248 | return this.switchRoute(location) 249 | } 250 | 251 | /** 252 | * 关闭当前页面,跳转到应用内的某个页面; 253 | * 如果页面是 `tabbar` 会自动切换成 `wx.switchTab` 跳转,但是 `tabbar` 不支持传递参数。 254 | * @param location 路由跳转参数 255 | */ 256 | replace(location: BaseLocation): Promise { 257 | ;(location as Location).replace = true 258 | return this.switchRoute(location) 259 | } 260 | 261 | /** 262 | * 关闭当前页面,返回上一页面或多级页面 263 | * @param delta 返回的页面数,如果 delta 大于现有页面数,则返回到首页。 264 | */ 265 | back(delta = 1): Promise { 266 | if (delta < 1) delta = 1 267 | return new Promise((resolve, reject) => { 268 | wx.navigateBack({ 269 | delta, 270 | success: (res) => { 271 | // const route = this.stack[targetIndex] 272 | resolve(res) 273 | }, 274 | fail: (res) => { 275 | // {"errMsg":"navigateBack:fail cannot navigate back at first page."} 276 | reject(res) 277 | }, 278 | }) 279 | }) 280 | } 281 | 282 | /** 283 | * 关闭所有页面,打开到应用内的某个页面 284 | * @param location 路由跳转参数 285 | */ 286 | reLaunch(location: BaseLocation): Promise { 287 | ;(location as Location).reLaunch = true 288 | return this.switchRoute(location) 289 | } 290 | 291 | /** 292 | * 根据路径获取路由配置 293 | * @param path 小程序路径path(不以/开头) 294 | */ 295 | getRouteConfigByPath(path: string) { 296 | if (path.indexOf('?') > -1) { 297 | path = path.substring(0, path.indexOf('?')) 298 | } 299 | return this.routeConfigList.find((item) => item.path === path) 300 | } 301 | 302 | /** 303 | * 获取当前路由 304 | */ 305 | getCurrentRoute() { 306 | let pages = getCurrentPages() 307 | let currentPage = pages[pages.length - 1] 308 | this.stackLength = pages.length 309 | let routeConfig = this.getRouteConfigByPath(currentPage.route) 310 | if (!routeConfig) throw new Error('当前页面' + currentPage.route + '对应的路由未配置') 311 | return generateRoute(routeConfig, currentPage.options) 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/utils/async.ts: -------------------------------------------------------------------------------- 1 | import { NavigationGuard } from '../index' 2 | 3 | export function runQueue(queue: NavigationGuard[], fn: Function, cb: Function) { 4 | const step = (index: number) => { 5 | if (index >= queue.length) { 6 | cb() 7 | } else { 8 | if (queue[index]) { 9 | fn(queue[index], () => { 10 | step(index + 1) 11 | }) 12 | } else { 13 | step(index + 1) 14 | } 15 | } 16 | } 17 | step(0) 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/query.ts: -------------------------------------------------------------------------------- 1 | const encodeReserveRE = /[!'()*]/g 2 | const encodeReserveReplacer = (c: string) => '%' + c.charCodeAt(0).toString(16) 3 | const commaRE = /%2C/g 4 | 5 | interface Dictionary { 6 | [index: string]: any 7 | } 8 | // fixed encodeURIComponent which is more conformant to RFC3986: 9 | // - escapes [!'()*] 10 | // - preserve commas 11 | const encode = (str: string) => 12 | encodeURIComponent(str) 13 | .replace(encodeReserveRE, encodeReserveReplacer) 14 | .replace(commaRE, ',') 15 | 16 | const decode = decodeURIComponent 17 | 18 | function parseQuery(query: string): Dictionary { 19 | const res: any = {} 20 | 21 | query = query.trim().replace(/^(\?|#|&)/, '') 22 | 23 | if (!query) { 24 | return res 25 | } 26 | 27 | query.split('&').forEach(param => { 28 | const parts = param.replace(/\+/g, ' ').split('=') 29 | const key = decode(parts.shift()!) 30 | const val = parts.length > 0 ? decode(parts.join('=')) : null 31 | 32 | if (res[key] === undefined) { 33 | res[key] = val 34 | } else if (Array.isArray(res[key])) { 35 | res[key].push(val) 36 | } else { 37 | res[key] = [res[key], val] 38 | } 39 | }) 40 | 41 | return res 42 | } 43 | 44 | /** 45 | * 将 'k1=v1&k2=v2' 格式的字符串转换成 query 对象 46 | * @param query (可选)要转换的字符串,格式类似于 '?name=admin&password=123' 或者 'name=admin&password=123' 47 | * @param extraQuery (可选)将转换后的 query 对象 附加到此 query 对象身上 48 | * @param _parseQuery (可选)自定义转换函数 49 | */ 50 | export function resolveQuery( 51 | query?: string, 52 | extraQuery: Dictionary = {}, 53 | _parseQuery?: Function 54 | ): Dictionary { 55 | const parse = _parseQuery || parseQuery 56 | let parsedQuery 57 | try { 58 | parsedQuery = parse(query || '') 59 | } catch (e) { 60 | parsedQuery = {} 61 | } 62 | for (const key in extraQuery) { 63 | parsedQuery[key] = extraQuery[key] 64 | } 65 | return parsedQuery 66 | } 67 | 68 | /** 69 | * 将 query 对象序列化成 'k1=v1&k2=v2' 格式化的字符串 70 | * @param obj 71 | */ 72 | export function stringifyQuery(obj: Dictionary): string { 73 | const res = obj 74 | ? Object.keys(obj) 75 | .map(key => { 76 | const val = obj[key] 77 | 78 | if (val === undefined) { 79 | return '' 80 | } 81 | 82 | if (val === null) { 83 | return encode(key) 84 | } 85 | 86 | if (Array.isArray(val)) { 87 | const result: any = [] 88 | val.forEach(val2 => { 89 | if (val2 === undefined) { 90 | return 91 | } 92 | if (val2 === null) { 93 | result.push(encode(key)) 94 | } else { 95 | result.push(encode(key) + '=' + encode(val2)) 96 | } 97 | }) 98 | return result.join('&') 99 | } 100 | 101 | return encode(key) + '=' + encode(val) 102 | }) 103 | .filter(x => x.length > 0) 104 | .join('&') 105 | : null 106 | return res ? `?${res}` : '' 107 | } 108 | -------------------------------------------------------------------------------- /test/test.test.ts: -------------------------------------------------------------------------------- 1 | import Router from "../src/index" 2 | 3 | describe('自定义适配器', function() { 4 | it('', async function(done) { 5 | let called = false 6 | const route = new Router({ 7 | routes: [ 8 | { 9 | name: "test-page-a", 10 | path: "pages/test-page-a" 11 | } 12 | ], 13 | adapter: async (adapterConfig) => { 14 | called = true 15 | } 16 | }) 17 | await route.push({ 18 | name: "test-page-a" 19 | }) 20 | expect(called).toEqual(true); 21 | done() 22 | }) 23 | 24 | }) 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "esnext", 5 | "module":"esnext", 6 | "lib": ["es2015", "es2016", "es2017"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "declarationDir": "dist/types", 14 | "outDir": "dist/lib", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "types": [ 19 | "miniprogram-api-typings" 20 | ] 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-config-prettier" 5 | ] 6 | } --------------------------------------------------------------------------------