完成hash路由的开发
parent
34ac7ab8ed
commit
aed4ecf4cd
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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>`
|
||||||
|
|
16
src/utils.js
16
src/utils.js
|
@ -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 = []
|
||||||
|
|
Loading…
Reference in New Issue