Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
yutent | a9ff9f0323 | |
yutent | d3a00a319f | |
yutent | 37dcc51e05 | |
yutent | a1c1740166 | |
yutent | b7567a15fc | |
yutent | ba92054633 | |
yutent | a24ab67694 | |
yutent | 9c9561cb1c | |
yutent | fc20ea24d4 | |
yutent | 137c79fce7 | |
yutent | 5136605322 | |
yutent | 1e08003ba7 | |
yutent | d68ecadb4e | |
yutent | e38f1c51dc | |
yutent | 66971278a2 | |
yutent | 1cddfb6731 |
12
Readme.md
12
Readme.md
|
@ -19,8 +19,9 @@
|
|||
|
||||
- 路由不支持嵌套, 即`<router-view></router-view>`只能出现`1`次。
|
||||
- `$router`对象, 只注入到使用`wkit`创建的组件, 其他地方可以使用`getRouter()`获取`$router`对象。
|
||||
- 所有路由页面和组件, 均可使用`getCurrentPage()`获取当前的页面的信息; 也可以用`$router.route`获取。
|
||||
- 所有路由页面和组件, 均可使用`getCurrentPage()`获取当前的页面的信息; 也可以用`$route`或`$router.route`获取。
|
||||
- `$store`对象, 只注入到使用`wkit`创建的组件, 其他组件可使用`getStore()`获取。
|
||||
- `watch()`方法, 可用于监听`$store`和`$route`的变化。
|
||||
|
||||
|
||||
|
||||
|
@ -91,10 +92,11 @@ index.html
|
|||
<script type="importmap">
|
||||
{
|
||||
"imports":{
|
||||
"es.shim":"https://jscdn.ink/lib/es.shim.js",
|
||||
"wkit":"https://jscdn.ink/lib/wkit.js",
|
||||
"fetch":"https://jscdn.ink/lib/fetch.js",
|
||||
"@bd/ui/":"https://jscdn.ink/@bd/ui/latest/"
|
||||
"es.shim":"//jscdn.ink/es.shim/latest/index.js",
|
||||
"wkit":"//jscdn.ink/wkit/latest/index.js",
|
||||
"wkitd":"//jscdn.ink/wkitd/latest/index.js",
|
||||
"fetch":"//jscdn.ink/@bytedo/fetch/latest/next.js",
|
||||
"@bd/ui/":"//jscdn.ink/@bd/ui/latest/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "wkitd",
|
||||
"version": "1.1.3",
|
||||
"version": "1.3.10",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
|
|
|
@ -8,3 +8,5 @@ export const __ROUTER__ = Symbol('router')
|
|||
export const __ROUTER_VIEW__ = Symbol('router-view')
|
||||
export const __STORE__ = Symbol('store')
|
||||
export const WKITD_COMPONENTS = new Set()
|
||||
export const STORE_CALLBACKS = new Map()
|
||||
export const ROUTE_CALLBACKS = new Set()
|
||||
|
|
|
@ -10,7 +10,7 @@ import { noop, readonlyProp } from './utils.js'
|
|||
import { __ROUTER__, __STORE__, __ROUTER_VIEW__ } from './constants.js'
|
||||
|
||||
export * from './router/index.js'
|
||||
export { createStore } from './store.js'
|
||||
export { createStore, watch } from './store.js'
|
||||
|
||||
class App extends Component {}
|
||||
|
||||
|
|
|
@ -13,20 +13,26 @@ class Wkitd extends WeakMap {
|
|||
*/
|
||||
broadcast() {
|
||||
for (let it of WKITD_COMPONENTS) {
|
||||
if (it.removed) {
|
||||
this.deassign(it)
|
||||
continue
|
||||
}
|
||||
it.$requestUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册缓存组件
|
||||
*/
|
||||
assign(target) {
|
||||
WKITD_COMPONENTS.add(target)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册
|
||||
*/
|
||||
deassign(target) {
|
||||
WKITD_COMPONENTS.add(target)
|
||||
WKITD_COMPONENTS.delete(target)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,12 +22,13 @@ export function createRouter({
|
|||
function wrapper() {
|
||||
Object.defineProperty(Component.prototype, '$router', {
|
||||
get() {
|
||||
return window.wkitd.get(__ROUTER__)
|
||||
},
|
||||
set(val) {
|
||||
console.error('Can not set readonly property $router of Component')
|
||||
},
|
||||
enumerable: false
|
||||
return $router
|
||||
}
|
||||
})
|
||||
Object.defineProperty(Component.prototype, '$route', {
|
||||
get() {
|
||||
return $router.route
|
||||
}
|
||||
})
|
||||
}
|
||||
wrapper.beforeEach = $router.beforeEach.bind($router)
|
||||
|
|
|
@ -1,26 +1,12 @@
|
|||
//
|
||||
import { Component, html, css, raw } from 'wkit'
|
||||
import { object2query } from '../utils.js'
|
||||
import { __ROUTER_VIEW__ } from '../constants.js'
|
||||
import { object2query, query2object } from '../utils.js'
|
||||
import { __ROUTER_VIEW__, ROUTE_CALLBACKS } from '../constants.js'
|
||||
import { watch } from '../store.js'
|
||||
|
||||
class RouterView extends Component {
|
||||
static props = {
|
||||
transition: false,
|
||||
current: {
|
||||
type: String,
|
||||
default: '',
|
||||
attribute: false,
|
||||
observer(v, old) {
|
||||
if (this.keepAlive && v) {
|
||||
if (old && this.$refs[old]) {
|
||||
this.$refs[old].deactivated()
|
||||
}
|
||||
this.$refs[v]?.$requestUpdate()
|
||||
this.$refs[v]?.$animate()
|
||||
this.$refs[v]?.activated()
|
||||
}
|
||||
}
|
||||
}
|
||||
transition: false
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
@ -29,6 +15,45 @@ class RouterView extends Component {
|
|||
}
|
||||
`
|
||||
|
||||
get current() {
|
||||
return this.#current
|
||||
}
|
||||
|
||||
set current(v) {
|
||||
let old = this.#current
|
||||
this.#current = v
|
||||
if (this.keepAlive) {
|
||||
if (old) {
|
||||
if (this.$refs[old]) {
|
||||
this.$refs[old].removed = true
|
||||
this.$refs[old].deactivated()
|
||||
this.$refs[old].remove()
|
||||
} else {
|
||||
this.$requestUpdate()
|
||||
}
|
||||
} else {
|
||||
this.$requestUpdate()
|
||||
}
|
||||
if (v) {
|
||||
if (this.$refs[v]) {
|
||||
this.root.appendChild(this.$refs[v])
|
||||
this.$refs[v].$requestUpdate()
|
||||
if (this.transition) {
|
||||
this.$refs[v].$animate()
|
||||
}
|
||||
this.$refs[v].removed = false
|
||||
this.$refs[v].activated()
|
||||
} else {
|
||||
this.$requestUpdate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.$requestUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
#current = ''
|
||||
|
||||
#views = []
|
||||
|
||||
created() {
|
||||
|
@ -47,42 +72,28 @@ class RouterView extends Component {
|
|||
{ transform: 'translateX(0)', opacity: 1 }
|
||||
]
|
||||
}
|
||||
if (this.keepAlive) {
|
||||
let template = this.#views.map(it =>
|
||||
this.transition
|
||||
? [
|
||||
`<${it} ref="${it}" keep-alive #animation="%s" style="%s"></${it}>`,
|
||||
[
|
||||
{ ...option, immediate: this.current === it },
|
||||
this.current === it ? '' : 'display:none'
|
||||
]
|
||||
]
|
||||
: [
|
||||
`<${it} ref="${it}" keep-alive style=%s></${it}>`,
|
||||
[this.current === it ? '' : 'display:none']
|
||||
]
|
||||
)
|
||||
|
||||
return raw(
|
||||
template.map(it => it[0]).join(''),
|
||||
template.map(it => it[1]).flat()
|
||||
)
|
||||
} else {
|
||||
if (this.current) {
|
||||
if (this.transition) {
|
||||
return raw(`<${this.current} #animation="%s"></${this.current}>`, [
|
||||
option
|
||||
])
|
||||
}
|
||||
return raw(`<${this.current}></${this.current}>`)
|
||||
if (this.current) {
|
||||
if (this.transition) {
|
||||
return raw(
|
||||
`<${this.current} ref="${this.current}" ${
|
||||
this.keepAlive ? 'keep-alive' : ''
|
||||
} #animation="%s"></${this.current}>`,
|
||||
[option]
|
||||
)
|
||||
}
|
||||
return raw(
|
||||
`<${this.current} ref="${this.current}" ${
|
||||
this.keepAlive ? 'keep-alive' : ''
|
||||
}></${this.current}>`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RouterLink extends Component {
|
||||
static props = {
|
||||
to: Object,
|
||||
to: { type: null },
|
||||
disabled: false
|
||||
}
|
||||
|
||||
|
@ -96,6 +107,8 @@ class RouterLink extends Component {
|
|||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--router-link-gap, 0);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: inherit;
|
||||
|
@ -108,12 +121,12 @@ class RouterLink extends Component {
|
|||
cursor: not-allowed;
|
||||
}
|
||||
`
|
||||
|
||||
#to = { path: '' }
|
||||
#href = ''
|
||||
|
||||
#navigate() {
|
||||
let type = this.$router.type
|
||||
let { path } = this.to
|
||||
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
|
@ -121,37 +134,59 @@ class RouterLink extends Component {
|
|||
if (type === 'hash') {
|
||||
location.hash = this.#href
|
||||
} else {
|
||||
this.$router.push(this.to)
|
||||
this.$router.push(this.#to)
|
||||
}
|
||||
}
|
||||
|
||||
#parsePath() {
|
||||
let type = this.$router.type
|
||||
let { path = '', query = {} } = this.to
|
||||
let params =
|
||||
typeof query === 'string'
|
||||
? query.replaceAll('?', '')
|
||||
: object2query(query)
|
||||
let path, query, params
|
||||
if (typeof this.to === 'string') {
|
||||
let tmp = this.to.split('?')
|
||||
path = tmp[0]
|
||||
params = tmp[1] || ''
|
||||
query = query2object(params)
|
||||
} else {
|
||||
path = this.to.path || ''
|
||||
query = this.to.query || {}
|
||||
params =
|
||||
typeof query === 'string'
|
||||
? query.replaceAll('?', '')
|
||||
: object2query(query)
|
||||
}
|
||||
|
||||
path = path.replace(/^\//, '')
|
||||
path = '/' + path.replace(/^\/+/, '')
|
||||
this.#to = { path, query }
|
||||
|
||||
if (params) {
|
||||
path += '?' + params
|
||||
}
|
||||
this.#href = path
|
||||
}
|
||||
|
||||
return '/' + path
|
||||
activated() {
|
||||
this.mounted()
|
||||
}
|
||||
deactivated() {
|
||||
this.unmounted()
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.$router.rsync(this, route => {
|
||||
this.classList.toggle('active', route.path === this.to.path)
|
||||
watch('$route', route => {
|
||||
if (this.removed) {
|
||||
return
|
||||
}
|
||||
this.classList.toggle('active', route.path === this.#to.path)
|
||||
})
|
||||
}
|
||||
|
||||
unmounted() {
|
||||
ROUTE_CALLBACKS.delete(this)
|
||||
}
|
||||
|
||||
render() {
|
||||
this.#href = this.#parsePath()
|
||||
return html`<a title=${this.#href} @click=${this.#navigate}>
|
||||
<slot></slot
|
||||
this.#parsePath()
|
||||
return html`<a title=${this.#href} @click=${this.#navigate}
|
||||
><slot></slot
|
||||
></a>`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
*/
|
||||
import { bind, fire } from 'wkit'
|
||||
import { noop, query2object, object2query } from '../utils.js'
|
||||
import { __ROUTER_VIEW__ } from '../constants.js'
|
||||
import { __ROUTER_VIEW__, ROUTE_CALLBACKS } from '../constants.js'
|
||||
|
||||
//hash前缀正则
|
||||
const PREFIX_REGEXP = /^(#!|#)[\/]+?/
|
||||
const RULE_REGEXP = /(\/[^/]*)(:[A-Za-z0-9_]+)(\?)?/g
|
||||
const RULE_REGEXP = /(\/[^/]*)(:[\$@~\\!A-Za-z0-9_=\-]+)(\?)?/g
|
||||
|
||||
const MODE_HASH = 'hash'
|
||||
const MODE_HISTORY = 'history'
|
||||
|
@ -21,10 +21,9 @@ class Router {
|
|||
#tables = new Map()
|
||||
#views = new Set()
|
||||
|
||||
#targets = new Map()
|
||||
|
||||
#ready = false
|
||||
#route = Object.create(null)
|
||||
#tmp = null
|
||||
|
||||
#beforeEach
|
||||
|
||||
|
@ -66,12 +65,12 @@ class Router {
|
|||
|
||||
re = route.path.replace(
|
||||
RULE_REGEXP,
|
||||
function (m, _prefix, _var, _require) {
|
||||
function (m, _prefix, _var, _require = '') {
|
||||
vars.push(_var.slice(1))
|
||||
if (_prefix === '/') {
|
||||
_prefix = '/?'
|
||||
}
|
||||
return _prefix + '([A-Za-z0-9_]+)' + _require
|
||||
return _prefix + '([\\$\\!@~A-Za-z0-9_=\\-]+)' + _require
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -101,14 +100,23 @@ class Router {
|
|||
let path = isHash
|
||||
? hash
|
||||
: location.href.replace(location.origin, '').replace(hash, '')
|
||||
let query
|
||||
let query = ''
|
||||
|
||||
if (path.includes('?')) {
|
||||
;[path, query] = path.split('?')
|
||||
}
|
||||
path = path.replace(PREFIX_REGEXP, '/')
|
||||
// 修正默认主页,以支持带路径访问的首页
|
||||
if (path === '/index.html') {
|
||||
path = '/'
|
||||
}
|
||||
|
||||
if (!$view || path === this.#route.path) {
|
||||
// query不同, 只更新query
|
||||
if (query !== object2query(this.#route.query)) {
|
||||
this.#route.query = query2object(query)
|
||||
return this.#broadcast()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -125,12 +133,12 @@ class Router {
|
|||
params,
|
||||
query: query2object(query)
|
||||
}
|
||||
Object.defineProperty(next, 'raw', { value: route.path })
|
||||
if (this.#beforeEach) {
|
||||
return this.#beforeEach(this.route, next, () => {
|
||||
this.#exec(next)
|
||||
})
|
||||
}
|
||||
|
||||
return this.#exec(next)
|
||||
}
|
||||
}
|
||||
|
@ -138,19 +146,36 @@ class Router {
|
|||
let route = this.#tables.get('!')
|
||||
$view.current = route.name
|
||||
this.#route = { path, name: route.name, params: {}, query: {} }
|
||||
this.#exec(this.#route)
|
||||
} else {
|
||||
if (this.#tmp) {
|
||||
this.#exec(this.#tmp)
|
||||
this.#tmp = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#exec(route) {
|
||||
let $view = window.wkitd.get(__ROUTER_VIEW__)
|
||||
let table = this.#tables.get(route.raw)
|
||||
$view.current = route.name
|
||||
this.#route = route
|
||||
this.#rsync()
|
||||
|
||||
if (table && typeof table.component === 'function') {
|
||||
if (!customElements.get(route.name)) {
|
||||
table.component()
|
||||
delete table.component //避免多次请求
|
||||
}
|
||||
}
|
||||
this.#broadcast()
|
||||
}
|
||||
|
||||
#rsync() {
|
||||
for (let [target, callback] of this.#targets) {
|
||||
callback.call(target, this.route)
|
||||
// 广播通知
|
||||
#broadcast() {
|
||||
if (this.#ready) {
|
||||
for (let callback of ROUTE_CALLBACKS) {
|
||||
callback(this.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,17 +186,6 @@ class Router {
|
|||
this.#hashchange()
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于同步路由到组件的
|
||||
*/
|
||||
rsync(target, callback) {
|
||||
this.#targets.set(target, callback)
|
||||
// 路由已经初始化完成时, 还有新的同步请求则立刻执行
|
||||
if (this.#ready) {
|
||||
this.#rsync()
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(callback = noop) {
|
||||
this.#beforeEach = callback
|
||||
}
|
||||
|
@ -219,6 +233,9 @@ class Router {
|
|||
return
|
||||
}
|
||||
|
||||
// 缓存当前路由信息, 当没有匹配到正确的路由时, 回调此缓存
|
||||
this.#tmp = obj
|
||||
|
||||
if (this.type === MODE_HASH) {
|
||||
if (replace) {
|
||||
location.replace(path.replace(/^\//, '#/'))
|
||||
|
|
43
src/store.js
43
src/store.js
|
@ -5,9 +5,15 @@
|
|||
*/
|
||||
|
||||
import { Component } from 'wkit'
|
||||
import { __STORE__ } from './constants.js'
|
||||
import {
|
||||
__STORE__,
|
||||
__ROUTER__,
|
||||
STORE_CALLBACKS,
|
||||
ROUTE_CALLBACKS
|
||||
} from './constants.js'
|
||||
import { noop } from './utils.js'
|
||||
|
||||
function observe(obj) {
|
||||
function observe(obj, paths = ['$store']) {
|
||||
if (obj === null) {
|
||||
return obj
|
||||
}
|
||||
|
@ -17,24 +23,52 @@ function observe(obj) {
|
|||
let value = Reflect.get(target, key, receiver)
|
||||
// 当访问的值是对象时,需要对这个对象也进行代理
|
||||
if (typeof value === 'object') {
|
||||
return observe(value)
|
||||
return observe(value, paths.concat(key))
|
||||
}
|
||||
return value
|
||||
},
|
||||
set(target, key, value, receiver) {
|
||||
let full = paths.concat(key).join('.')
|
||||
if (target[key] === value) {
|
||||
return true
|
||||
}
|
||||
Reflect.set(target, key, value, receiver)
|
||||
if (STORE_CALLBACKS.get(full)) {
|
||||
STORE_CALLBACKS.get(full).forEach(callback => {
|
||||
callback(value)
|
||||
})
|
||||
}
|
||||
window.wkitd.broadcast()
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function watch(key, callback = noop) {
|
||||
if (key.startsWith('$store.')) {
|
||||
let list = STORE_CALLBACKS.get(key)
|
||||
if (list) {
|
||||
list.add(callback)
|
||||
} else {
|
||||
list = new Set()
|
||||
list.add(callback)
|
||||
STORE_CALLBACKS.set(key, list)
|
||||
}
|
||||
} else if (key.startsWith('$route')) {
|
||||
ROUTE_CALLBACKS.add(callback)
|
||||
callback(window.wkitd.get(__ROUTER__).route)
|
||||
} else {
|
||||
return console.error('watch() only work on $store and $route')
|
||||
}
|
||||
}
|
||||
|
||||
export function createStore(obj = {}) {
|
||||
let defined = false
|
||||
|
||||
return function () {
|
||||
Object.defineProperty(Component.prototype, '$store', {
|
||||
get() {
|
||||
window.wkitd.assign(this)
|
||||
return window.wkitd.get(__STORE__)
|
||||
},
|
||||
set(val) {
|
||||
|
@ -45,8 +79,7 @@ export function createStore(obj = {}) {
|
|||
}
|
||||
window.wkitd.set(__STORE__, observe(val))
|
||||
defined = true
|
||||
},
|
||||
enumerable: false
|
||||
}
|
||||
})
|
||||
Component.prototype.$store = obj
|
||||
}
|
||||
|
|
|
@ -13,9 +13,7 @@ export function readonlyProp(host, name, value) {
|
|||
Object.defineProperty(host, name, {
|
||||
get() {
|
||||
return value
|
||||
},
|
||||
set(vale) {},
|
||||
enumerable: false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue