diff --git a/src/index.js b/src/index.js index a325f3c..1c6af69 100644 --- a/src/index.js +++ b/src/index.js @@ -31,6 +31,7 @@ export function createApp({ } this.mount = function () { let $router = window.__wkitd__.get('$router') + window.foo = $router if (render) { App.prototype.render = render } else { @@ -72,8 +73,8 @@ export function createApp({ App.prototype.mounted = function (...args) { let $view = window.__wkitd__.get('ROUTER_VIEW') if ($view) { - $view.sync($router.getViews()) - $router.init() + $view.sync($router.views) + $router.init() // mounted 时正式初始化路由 mounted.call(this, ...args) } else { throw new Error( diff --git a/src/router/hash-router.js b/src/router/hash-router.js index d01a279..e3665ba 100644 --- a/src/router/hash-router.js +++ b/src/router/hash-router.js @@ -5,17 +5,11 @@ * */ import { bind, fire } from 'wkit' -import { query2object } from '../utils.js' +import { noop, query2object, object2query } from '../utils.js' //hash前缀正则 -const PREFIX_REGEXP = /^(#!|#)[\/]?/ -const TRIM_REGEXP = /(^[/]+)|([/]+$)/g -const DEFAULT_OPTIONS = { - allowReload: true //连续点击同一个链接是否重新加载 -} - -const RULE_REGEXP = - /(:id)|(\{id\})|(\{id:([A-z\d\,\[\]\{\}\-\+\*\?\!:\^\$]*)\})/g +const PREFIX_REGEXP = /^(#!|#)[\/]+?/ +const RULE_REGEXP = /(\/[^/]*)(:[A-Za-z0-9_]+)(\?)?/g class Router { type = 'hash' @@ -23,71 +17,72 @@ class Router { #tables = new Map() #views = new Set() - #targets = new Set() + #targets = new Map() #ready = false - #options = Object.create(null) #route = Object.create(null) - constructor(options = {}) { - Object.assign(this.#options, DEFAULT_OPTIONS, options) + #beforeEach - bind(window, 'load, popstate', this.#hashchange.bind(this)) + constructor() { + bind(window, 'popstate', this.#hashchange.bind(this)) } + // 只读route get route() { return this.#route } - #hashchange(ev) { - if (ev?.type === 'load') { - if (this.#ready) { - return - } - this.#ready = true - } + // 只读views + get views() { + return Array.from(this.#views) + } - if (ev?.type === 'load') { - // this.go() - // hash模式要手动触发一下路由检测 - this.#check() - } else { - this.#check() + #hashchange(ev) { + if (!this.#ready) { + return } + this.#check() } #parseRule(route) { if (route.path === '!') { route.regexp = null } else { - let re = route.path.replace(RULE_REGEXP, function (m, p1, p2, p3, p4) { - let w = '([\\w.-]' - if (p1 || p2) { - return w + '+)' - } else { - if (!/^\{[\d\,]+\}$/.test(p4)) { - w = '(' + let vars = [] + let re + if (route.path.includes('?') && route.path.at(-1) !== '?') { + throw new SyntaxError( + `The exp "?" can only be used in the last.\n\n ${JSON.stringify( + route + )}\n` + ) + } + + re = route.path.replace( + RULE_REGEXP, + function (m, _prefix, _var, _require) { + vars.push(_var.slice(1)) + if (_prefix === '/') { + _prefix = '/?' } - return w + p4 + ')' + return _prefix + '([A-Za-z0-9_]+)' + _require } - }) - re = re - .replace(/(([^\\])([\/]+))/g, '$2\\/') - .replace(/(([^\\])([\.]+))/g, '$2\\.') - .replace(/(([^\\])([\-]+))/g, '$2\\-') - .replace(/(\(.*)(\\[\-]+)(.*\))/g, '$1-$3') + ) + re = '^' + re + '$' route.regexp = new RegExp(re) + route.vars = vars } return route } #add(route) { if (route.path !== '!' && route.path[0] !== '/') { - console.error('路由规则必须以"/"开头') + console.error('route path must start with "/"') return } - route.path = route.path.replace(/^[\/]+|[\/]+$|\s+/g, '') + route.path = route.path.replace(/^[\/]+|[\/]+$|\s+/g, '/') this.#tables.set(route.path, this.#parseRule(route)) this.#views.add(route.name) @@ -95,7 +90,6 @@ class Router { // 路由检测 #check() { - let { allowReload } = this.#options let $view = window.__wkitd__.get('ROUTER_VIEW') let path = location.hash let query @@ -103,67 +97,74 @@ class Router { if (path.includes('?')) { ;[path, query] = path.split('?') } - path = path.replace(PREFIX_REGEXP, '').replace(TRIM_REGEXP, '') + path = path.replace(PREFIX_REGEXP, '/') - // console.log(path, query, query2object(query)) - - if (!$view || (!allowReload && path === this.#route.path)) { + if (!$view || path === this.#route.path) { return } for (let [k, route] of this.#tables) { let args = path.match(route.regexp) if (args) { - args.shift() - $view.current = route.name - this.#route.path = path - this.#route.name = route.name - this.#route.params = args - this.#route.query = {} - this.#rsync() - return + let params = Object.create(null) + for (let i = 1; i < args.length; i++) { + params[[route.vars[i - 1]]] = args[i] + } + let next = { + path, + name: route.name, + params, + query: query2object(query) + } + if (this.#beforeEach) { + return this.#beforeEach(this.route, next, () => { + this.#exec(next) + }) + } + + return this.#exec(next) } } if (this.#tables.get('!')) { - route = this.#tables.get('!') + let route = this.#tables.get('!') $view.current = route.name this.#route = { path, name: route.name, params: {}, query: {} } } } + #exec(route) { + let $view = window.__wkitd__.get('ROUTER_VIEW') + $view.current = route.name + this.#route = route + this.#rsync() + } + #rsync() { - for (let callback of this.#targets) { - callback(this.route) + for (let [target, callback] of this.#targets) { + callback.call(target, this.route) } } + // 正式初始化路由监听, web components的特性, router-view未加载之前 + // 无法对路由进行操作, 所以需要另外在路由mounted时触发 init() { + this.#ready = true this.#hashchange() } - rsync(callback) { - this.#targets.add(callback) - } - - getViews() { - return Array.from(this.#views) - } - - beforeEach(callback) { - callback(prev, next, function () { - // - }) - } - - // 跳转到路由 - go(path) { - path = path.trim().replace(TRIM_REGEXP, '') - - // 页面刷新时, 不主动添加空hash, 避免执行2次noMatch回调 - if (!path && path === location.hash) { - return + /** + * 用于同步路由到组件的 + */ + rsync(target, callback) { + this.#targets.set(target, callback) + // 路由已经初始化完成时, 还有新的同步请求则立刻执行 + if (this.#ready) { + this.#rsync() } - location.hash = '!/' + path + } + + beforeEach(callback = noop) { + this.#beforeEach = callback } // 绑定路由事件 @@ -175,17 +176,52 @@ class Router { } else { this.#add(routes) } - // 因为先初始化,才开始监听路由规则 - // 所以会导致wondow load的时候, 规则还没生效, 而生效之后,load已经结束 - // 所以这里需要手动再触发一次load + // 初始化后再添加路由, 手动执行一次回调 if (this.#ready) { this.#hashchange() - } else { - fire(window, 'load') } } + + go(delta = 0) { + history.go(delta) + } + + back() { + this.go(-1) + } + + forward() { + this.go(1) + } + + // + push(obj = { path: '', query: {} }, replace = false) { + let path = '' + if (typeof path === 'string') { + path = obj.trim() + } else { + let query = object2query(obj.query || '') + path = obj.path + (query ? `?${query}` : '') + } + + // 空路径及相同路径, 不重复执行 + if (!path && path === location.hash.slice(1)) { + return + } + + if (replace) { + location.replace(path.replace(/^\//, '#/')) + } else { + location.hash = path + } + } + + // + replace(obj = { path: '', query: {} }) { + this.push(obj, true) + } } export default function () { - return new Router() + return () => new Router() } diff --git a/src/router/index.js b/src/router/index.js index e54eac4..de93b84 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -10,20 +10,21 @@ import './router-components.js' export { createWebHashHistory, createWebHistory } -export function createRouter( - { history = createWebHashHistory, routes = [] } = {}, - options -) { - let $router = history(options) +export function createRouter({ + history = createWebHashHistory(), + routes = [] +} = {}) { + let $router = history() window.__wkitd__.set('$router', $router) Component.prototype.$router = $router $router.addRoute(routes) - - return function () { + function wrapper() { return $router } + wrapper.beforeEach = $router.beforeEach.bind($router) + return wrapper } export function getRouter() { diff --git a/src/router/router-components.js b/src/router/router-components.js index b226e02..758267b 100644 --- a/src/router/router-components.js +++ b/src/router/router-components.js @@ -15,6 +15,7 @@ class RouterView extends Component { if (old && this.$refs[old]) { this.$refs[old].deactivated() } + this.$refs[v]?.$requestUpdate() this.$refs[v]?.$animate() this.$refs[v]?.activated() } @@ -101,6 +102,7 @@ class RouterLink extends Component { if (this.disabled) { return } + if (type === 'hash') { location.hash = this.#href } else { @@ -111,16 +113,28 @@ class RouterLink extends Component { #parsePath() { let type = this.$router.type let { path = '', query = {} } = this.to - let params = typeof query === 'string' ? query : object2query(query) + let params = + typeof query === 'string' + ? query.replaceAll('?', '') + : object2query(query) path = path.replace(/^\//, '') - return '/' + path + '?' + params + if (params) { + path += '?' + params + } + + return '/' + path + } + + mounted() { + this.$router.rsync(this, route => { + this.classList.toggle('active', route.path === this.to.path) + }) } render() { this.#href = this.#parsePath() - return html` ` diff --git a/src/utils.js b/src/utils.js index 9bcd6fd..0bc829a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -85,8 +85,13 @@ export function query2object(str = '') { if (output[k]) { if (isArray & 2) { output[k] = output[k].concat(v) - } else { + } else if (isArray & 1) { Object.assign(output[k], v) + } else { + if (!Array.isArray(output[v])) { + output[k] = [output[k]] + } + output[k].push(v) } } else { output[k] = v @@ -99,7 +104,14 @@ export function query2object(str = '') { * 将json数据转成 url query字符串 */ export function object2query(obj = {}) { - if (!obj || typeof obj === 'string' || typeof obj === 'number') { + if (obj === null) { + return '' + } + if ( + typeof obj === 'string' || + typeof obj === 'number' || + typeof obj === 'boolean' + ) { return obj } let output = []