完成hash路由的开发

master
yutent 2023-08-14 17:56:00 +08:00
parent 34ac7ab8ed
commit aed4ecf4cd
5 changed files with 165 additions and 101 deletions

View File

@ -31,6 +31,7 @@ export function createApp({
} }
this.mount = function () { this.mount = function () {
let $router = window.__wkitd__.get('$router') let $router = window.__wkitd__.get('$router')
window.foo = $router
if (render) { if (render) {
App.prototype.render = render App.prototype.render = render
} else { } else {
@ -72,8 +73,8 @@ export function createApp({
App.prototype.mounted = function (...args) { App.prototype.mounted = function (...args) {
let $view = window.__wkitd__.get('ROUTER_VIEW') let $view = window.__wkitd__.get('ROUTER_VIEW')
if ($view) { if ($view) {
$view.sync($router.getViews()) $view.sync($router.views)
$router.init() $router.init() // mounted 时正式初始化路由
mounted.call(this, ...args) mounted.call(this, ...args)
} else { } else {
throw new Error( throw new Error(

View File

@ -5,17 +5,11 @@
* *
*/ */
import { bind, fire } from 'wkit' import { bind, fire } from 'wkit'
import { query2object } from '../utils.js' import { noop, query2object, object2query } from '../utils.js'
//hash前缀正则 //hash前缀正则
const PREFIX_REGEXP = /^(#!|#)[\/]?/ const PREFIX_REGEXP = /^(#!|#)[\/]+?/
const TRIM_REGEXP = /(^[/]+)|([/]+$)/g const RULE_REGEXP = /(\/[^/]*)(:[A-Za-z0-9_]+)(\?)?/g
const DEFAULT_OPTIONS = {
allowReload: true //连续点击同一个链接是否重新加载
}
const RULE_REGEXP =
/(:id)|(\{id\})|(\{id:([A-z\d\,\[\]\{\}\-\+\*\?\!:\^\$]*)\})/g
class Router { class Router {
type = 'hash' type = 'hash'
@ -23,71 +17,72 @@ class Router {
#tables = new Map() #tables = new Map()
#views = new Set() #views = new Set()
#targets = new Set() #targets = new Map()
#ready = false #ready = false
#options = Object.create(null)
#route = Object.create(null) #route = Object.create(null)
constructor(options = {}) { #beforeEach
Object.assign(this.#options, DEFAULT_OPTIONS, options)
bind(window, 'load, popstate', this.#hashchange.bind(this)) constructor() {
bind(window, 'popstate', this.#hashchange.bind(this))
} }
// 只读route
get route() { get route() {
return this.#route return this.#route
} }
#hashchange(ev) { // 只读views
if (ev?.type === 'load') { get views() {
if (this.#ready) { return Array.from(this.#views)
return
}
this.#ready = true
} }
if (ev?.type === 'load') { #hashchange(ev) {
// this.go() if (!this.#ready) {
// hash模式要手动触发一下路由检测 return
this.#check()
} else {
this.#check()
} }
this.#check()
} }
#parseRule(route) { #parseRule(route) {
if (route.path === '!') { if (route.path === '!') {
route.regexp = null route.regexp = null
} else { } else {
let re = route.path.replace(RULE_REGEXP, function (m, p1, p2, p3, p4) { let vars = []
let w = '([\\w.-]' let re
if (p1 || p2) { if (route.path.includes('?') && route.path.at(-1) !== '?') {
return w + '+)' throw new SyntaxError(
} else { `The exp "?" can only be used in the last.\n\n ${JSON.stringify(
if (!/^\{[\d\,]+\}$/.test(p4)) { route
w = '(' )}\n`
)
} }
return w + p4 + ')'
re = route.path.replace(
RULE_REGEXP,
function (m, _prefix, _var, _require) {
vars.push(_var.slice(1))
if (_prefix === '/') {
_prefix = '/?'
} }
}) return _prefix + '([A-Za-z0-9_]+)' + _require
re = re }
.replace(/(([^\\])([\/]+))/g, '$2\\/') )
.replace(/(([^\\])([\.]+))/g, '$2\\.')
.replace(/(([^\\])([\-]+))/g, '$2\\-')
.replace(/(\(.*)(\\[\-]+)(.*\))/g, '$1-$3')
re = '^' + re + '$' re = '^' + re + '$'
route.regexp = new RegExp(re) route.regexp = new RegExp(re)
route.vars = vars
} }
return route return route
} }
#add(route) { #add(route) {
if (route.path !== '!' && route.path[0] !== '/') { if (route.path !== '!' && route.path[0] !== '/') {
console.error('路由规则必须以"/"开头') console.error('route path must start with "/"')
return return
} }
route.path = route.path.replace(/^[\/]+|[\/]+$|\s+/g, '') route.path = route.path.replace(/^[\/]+|[\/]+$|\s+/g, '/')
this.#tables.set(route.path, this.#parseRule(route)) this.#tables.set(route.path, this.#parseRule(route))
this.#views.add(route.name) this.#views.add(route.name)
@ -95,7 +90,6 @@ class Router {
// 路由检测 // 路由检测
#check() { #check() {
let { allowReload } = this.#options
let $view = window.__wkitd__.get('ROUTER_VIEW') let $view = window.__wkitd__.get('ROUTER_VIEW')
let path = location.hash let path = location.hash
let query let query
@ -103,67 +97,74 @@ class Router {
if (path.includes('?')) { if (path.includes('?')) {
;[path, query] = path.split('?') ;[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 || path === this.#route.path) {
if (!$view || (!allowReload && path === this.#route.path)) {
return return
} }
for (let [k, route] of this.#tables) { for (let [k, route] of this.#tables) {
let args = path.match(route.regexp) let args = path.match(route.regexp)
if (args) { if (args) {
args.shift() let params = Object.create(null)
$view.current = route.name for (let i = 1; i < args.length; i++) {
this.#route.path = path params[[route.vars[i - 1]]] = args[i]
this.#route.name = route.name }
this.#route.params = args let next = {
this.#route.query = {} path,
this.#rsync() name: route.name,
return params,
query: query2object(query)
}
if (this.#beforeEach) {
return this.#beforeEach(this.route, next, () => {
this.#exec(next)
})
}
return this.#exec(next)
} }
} }
if (this.#tables.get('!')) { if (this.#tables.get('!')) {
route = this.#tables.get('!') let route = this.#tables.get('!')
$view.current = route.name $view.current = route.name
this.#route = { path, name: route.name, params: {}, query: {} } 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() { #rsync() {
for (let callback of this.#targets) { for (let [target, callback] of this.#targets) {
callback(this.route) callback.call(target, this.route)
} }
} }
// 正式初始化路由监听, web components的特性, router-view未加载之前
// 无法对路由进行操作, 所以需要另外在路由mounted时触发
init() { init() {
this.#ready = true
this.#hashchange() this.#hashchange()
} }
rsync(callback) { /**
this.#targets.add(callback) * 用于同步路由到组件的
*/
rsync(target, callback) {
this.#targets.set(target, callback)
// 路由已经初始化完成时, 还有新的同步请求则立刻执行
if (this.#ready) {
this.#rsync()
}
} }
getViews() { beforeEach(callback = noop) {
return Array.from(this.#views) this.#beforeEach = callback
}
beforeEach(callback) {
callback(prev, next, function () {
//
})
}
// 跳转到路由
go(path) {
path = path.trim().replace(TRIM_REGEXP, '')
// 页面刷新时, 不主动添加空hash, 避免执行2次noMatch回调
if (!path && path === location.hash) {
return
}
location.hash = '!/' + path
} }
// 绑定路由事件 // 绑定路由事件
@ -175,17 +176,52 @@ class Router {
} else { } else {
this.#add(routes) this.#add(routes)
} }
// 因为先初始化,才开始监听路由规则 // 初始化后再添加路由, 手动执行一次回调
// 所以会导致wondow load的时候, 规则还没生效, 而生效之后,load已经结束
// 所以这里需要手动再触发一次load
if (this.#ready) { if (this.#ready) {
this.#hashchange() 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 () { export default function () {
return new Router() return () => new Router()
} }

View File

@ -10,20 +10,21 @@ import './router-components.js'
export { createWebHashHistory, createWebHistory } export { createWebHashHistory, createWebHistory }
export function createRouter( export function createRouter({
{ history = createWebHashHistory, routes = [] } = {}, history = createWebHashHistory(),
options routes = []
) { } = {}) {
let $router = history(options) let $router = history()
window.__wkitd__.set('$router', $router) window.__wkitd__.set('$router', $router)
Component.prototype.$router = $router Component.prototype.$router = $router
$router.addRoute(routes) $router.addRoute(routes)
function wrapper() {
return function () {
return $router return $router
} }
wrapper.beforeEach = $router.beforeEach.bind($router)
return wrapper
} }
export function getRouter() { export function getRouter() {

View File

@ -15,6 +15,7 @@ class RouterView extends Component {
if (old && this.$refs[old]) { if (old && this.$refs[old]) {
this.$refs[old].deactivated() this.$refs[old].deactivated()
} }
this.$refs[v]?.$requestUpdate()
this.$refs[v]?.$animate() this.$refs[v]?.$animate()
this.$refs[v]?.activated() this.$refs[v]?.activated()
} }
@ -101,6 +102,7 @@ class RouterLink extends Component {
if (this.disabled) { if (this.disabled) {
return return
} }
if (type === 'hash') { if (type === 'hash') {
location.hash = this.#href location.hash = this.#href
} else { } else {
@ -111,16 +113,28 @@ class RouterLink extends Component {
#parsePath() { #parsePath() {
let type = this.$router.type let type = this.$router.type
let { path = '', query = {} } = this.to 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(/^\//, '') 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() { render() {
this.#href = this.#parsePath() this.#href = this.#parsePath()
return html`<a title=${this.#href} @click=${this.#navigate}> return html`<a title=${this.#href} @click=${this.#navigate}>
<slot></slot <slot></slot
></a>` ></a>`

View File

@ -85,8 +85,13 @@ export function query2object(str = '') {
if (output[k]) { if (output[k]) {
if (isArray & 2) { if (isArray & 2) {
output[k] = output[k].concat(v) output[k] = output[k].concat(v)
} else { } else if (isArray & 1) {
Object.assign(output[k], v) Object.assign(output[k], v)
} else {
if (!Array.isArray(output[v])) {
output[k] = [output[k]]
}
output[k].push(v)
} }
} else { } else {
output[k] = v output[k] = v
@ -99,7 +104,14 @@ export function query2object(str = '') {
* 将json数据转成 url query字符串 * 将json数据转成 url query字符串
*/ */
export function object2query(obj = {}) { 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 return obj
} }
let output = [] let output = []