Compare commits

...

16 Commits

10 changed files with 201 additions and 107 deletions

View File

@ -19,8 +19,9 @@
- 路由不支持嵌套, 即`<router-view></router-view>`只能出现`1`次。 - 路由不支持嵌套, 即`<router-view></router-view>`只能出现`1`次。
- `$router`对象, 只注入到使用`wkit`创建的组件, 其他地方可以使用`getRouter()`获取`$router`对象。 - `$router`对象, 只注入到使用`wkit`创建的组件, 其他地方可以使用`getRouter()`获取`$router`对象。
- 所有路由页面和组件, 均可使用`getCurrentPage()`获取当前的页面的信息; 也可以用`$router.route`获取。 - 所有路由页面和组件, 均可使用`getCurrentPage()`获取当前的页面的信息; 也可以用`$route`或`$router.route`获取。
- `$store`对象, 只注入到使用`wkit`创建的组件, 其他组件可使用`getStore()`获取。 - `$store`对象, 只注入到使用`wkit`创建的组件, 其他组件可使用`getStore()`获取。
- `watch()`方法, 可用于监听`$store`和`$route`的变化。
@ -91,10 +92,11 @@ index.html
<script type="importmap"> <script type="importmap">
{ {
"imports":{ "imports":{
"es.shim":"https://jscdn.ink/lib/es.shim.js", "es.shim":"//jscdn.ink/es.shim/latest/index.js",
"wkit":"https://jscdn.ink/lib/wkit.js", "wkit":"//jscdn.ink/wkit/latest/index.js",
"fetch":"https://jscdn.ink/lib/fetch.js", "wkitd":"//jscdn.ink/wkitd/latest/index.js",
"@bd/ui/":"https://jscdn.ink/@bd/ui/latest/" "fetch":"//jscdn.ink/@bytedo/fetch/latest/next.js",
"@bd/ui/":"//jscdn.ink/@bd/ui/latest/"
} }
} }
</script> </script>

View File

@ -1,6 +1,6 @@
{ {
"name": "wkitd", "name": "wkitd",
"version": "1.1.3", "version": "1.3.10",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"files": [ "files": [

View File

@ -8,3 +8,5 @@ export const __ROUTER__ = Symbol('router')
export const __ROUTER_VIEW__ = Symbol('router-view') export const __ROUTER_VIEW__ = Symbol('router-view')
export const __STORE__ = Symbol('store') export const __STORE__ = Symbol('store')
export const WKITD_COMPONENTS = new Set() export const WKITD_COMPONENTS = new Set()
export const STORE_CALLBACKS = new Map()
export const ROUTE_CALLBACKS = new Set()

View File

@ -10,7 +10,7 @@ import { noop, readonlyProp } from './utils.js'
import { __ROUTER__, __STORE__, __ROUTER_VIEW__ } from './constants.js' import { __ROUTER__, __STORE__, __ROUTER_VIEW__ } from './constants.js'
export * from './router/index.js' export * from './router/index.js'
export { createStore } from './store.js' export { createStore, watch } from './store.js'
class App extends Component {} class App extends Component {}

View File

@ -13,20 +13,26 @@ class Wkitd extends WeakMap {
*/ */
broadcast() { broadcast() {
for (let it of WKITD_COMPONENTS) { for (let it of WKITD_COMPONENTS) {
if (it.removed) {
this.deassign(it)
continue
}
it.$requestUpdate() it.$requestUpdate()
} }
} }
/** /**
* 注册缓存组件 * 注册缓存组件
*/ */
assign(target) { assign(target) {
WKITD_COMPONENTS.add(target) WKITD_COMPONENTS.add(target)
} }
/** /**
* 取消注册 * 取消注册
*/ */
deassign(target) { deassign(target) {
WKITD_COMPONENTS.add(target) WKITD_COMPONENTS.delete(target)
} }
} }

View File

@ -22,12 +22,13 @@ export function createRouter({
function wrapper() { function wrapper() {
Object.defineProperty(Component.prototype, '$router', { Object.defineProperty(Component.prototype, '$router', {
get() { get() {
return window.wkitd.get(__ROUTER__) return $router
}, }
set(val) { })
console.error('Can not set readonly property $router of Component') Object.defineProperty(Component.prototype, '$route', {
}, get() {
enumerable: false return $router.route
}
}) })
} }
wrapper.beforeEach = $router.beforeEach.bind($router) wrapper.beforeEach = $router.beforeEach.bind($router)

View File

@ -1,26 +1,12 @@
// //
import { Component, html, css, raw } from 'wkit' import { Component, html, css, raw } from 'wkit'
import { object2query } from '../utils.js' import { object2query, query2object } from '../utils.js'
import { __ROUTER_VIEW__ } from '../constants.js' import { __ROUTER_VIEW__, ROUTE_CALLBACKS } from '../constants.js'
import { watch } from '../store.js'
class RouterView extends Component { class RouterView extends Component {
static props = { static props = {
transition: false, 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()
}
}
}
} }
static styles = css` 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 = [] #views = []
created() { created() {
@ -47,42 +72,28 @@ class RouterView extends Component {
{ transform: 'translateX(0)', opacity: 1 } { 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( if (this.current) {
template.map(it => it[0]).join(''), if (this.transition) {
template.map(it => it[1]).flat() return raw(
) `<${this.current} ref="${this.current}" ${
} else { this.keepAlive ? 'keep-alive' : ''
if (this.current) { } #animation="%s"></${this.current}>`,
if (this.transition) { [option]
return raw(`<${this.current} #animation="%s"></${this.current}>`, [ )
option
])
}
return raw(`<${this.current}></${this.current}>`)
} }
return raw(
`<${this.current} ref="${this.current}" ${
this.keepAlive ? 'keep-alive' : ''
}></${this.current}>`
)
} }
} }
} }
class RouterLink extends Component { class RouterLink extends Component {
static props = { static props = {
to: Object, to: { type: null },
disabled: false disabled: false
} }
@ -96,6 +107,8 @@ class RouterLink extends Component {
a { a {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
gap: var(--router-link-gap, 0);
width: 100%; width: 100%;
height: 100%; height: 100%;
color: inherit; color: inherit;
@ -108,12 +121,12 @@ class RouterLink extends Component {
cursor: not-allowed; cursor: not-allowed;
} }
` `
#to = { path: '' }
#href = '' #href = ''
#navigate() { #navigate() {
let type = this.$router.type let type = this.$router.type
let { path } = this.to
if (this.disabled) { if (this.disabled) {
return return
} }
@ -121,37 +134,59 @@ class RouterLink extends Component {
if (type === 'hash') { if (type === 'hash') {
location.hash = this.#href location.hash = this.#href
} else { } else {
this.$router.push(this.to) this.$router.push(this.#to)
} }
} }
#parsePath() { #parsePath() {
let type = this.$router.type let path, query, params
let { path = '', query = {} } = this.to if (typeof this.to === 'string') {
let params = let tmp = this.to.split('?')
typeof query === 'string' path = tmp[0]
? query.replaceAll('?', '') params = tmp[1] || ''
: object2query(query) 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) { if (params) {
path += '?' + params path += '?' + params
} }
this.#href = path
}
return '/' + path activated() {
this.mounted()
}
deactivated() {
this.unmounted()
} }
mounted() { mounted() {
this.$router.rsync(this, route => { watch('$route', route => {
this.classList.toggle('active', route.path === this.to.path) if (this.removed) {
return
}
this.classList.toggle('active', route.path === this.#to.path)
}) })
} }
unmounted() {
ROUTE_CALLBACKS.delete(this)
}
render() { render() {
this.#href = this.#parsePath() 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

@ -6,11 +6,11 @@
*/ */
import { bind, fire } from 'wkit' import { bind, fire } from 'wkit'
import { noop, query2object, object2query } from '../utils.js' import { noop, query2object, object2query } from '../utils.js'
import { __ROUTER_VIEW__ } from '../constants.js' import { __ROUTER_VIEW__, ROUTE_CALLBACKS } from '../constants.js'
//hash前缀正则 //hash前缀正则
const PREFIX_REGEXP = /^(#!|#)[\/]+?/ 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_HASH = 'hash'
const MODE_HISTORY = 'history' const MODE_HISTORY = 'history'
@ -21,10 +21,9 @@ class Router {
#tables = new Map() #tables = new Map()
#views = new Set() #views = new Set()
#targets = new Map()
#ready = false #ready = false
#route = Object.create(null) #route = Object.create(null)
#tmp = null
#beforeEach #beforeEach
@ -66,12 +65,12 @@ class Router {
re = route.path.replace( re = route.path.replace(
RULE_REGEXP, RULE_REGEXP,
function (m, _prefix, _var, _require) { function (m, _prefix, _var, _require = '') {
vars.push(_var.slice(1)) vars.push(_var.slice(1))
if (_prefix === '/') { if (_prefix === '/') {
_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 let path = isHash
? hash ? hash
: location.href.replace(location.origin, '').replace(hash, '') : location.href.replace(location.origin, '').replace(hash, '')
let query let query = ''
if (path.includes('?')) { if (path.includes('?')) {
;[path, query] = path.split('?') ;[path, query] = path.split('?')
} }
path = path.replace(PREFIX_REGEXP, '/') path = path.replace(PREFIX_REGEXP, '/')
// 修正默认主页,以支持带路径访问的首页
if (path === '/index.html') {
path = '/'
}
if (!$view || path === this.#route.path) { if (!$view || path === this.#route.path) {
// query不同, 只更新query
if (query !== object2query(this.#route.query)) {
this.#route.query = query2object(query)
return this.#broadcast()
}
return return
} }
@ -125,12 +133,12 @@ class Router {
params, params,
query: query2object(query) query: query2object(query)
} }
Object.defineProperty(next, 'raw', { value: route.path })
if (this.#beforeEach) { if (this.#beforeEach) {
return this.#beforeEach(this.route, next, () => { return this.#beforeEach(this.route, next, () => {
this.#exec(next) this.#exec(next)
}) })
} }
return this.#exec(next) return this.#exec(next)
} }
} }
@ -138,19 +146,36 @@ class Router {
let 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: {} }
this.#exec(this.#route)
} else {
if (this.#tmp) {
this.#exec(this.#tmp)
this.#tmp = null
}
} }
} }
#exec(route) { #exec(route) {
let $view = window.wkitd.get(__ROUTER_VIEW__) let $view = window.wkitd.get(__ROUTER_VIEW__)
let table = this.#tables.get(route.raw)
$view.current = route.name $view.current = route.name
this.#route = route 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) { #broadcast() {
callback.call(target, this.route) if (this.#ready) {
for (let callback of ROUTE_CALLBACKS) {
callback(this.route)
}
} }
} }
@ -161,17 +186,6 @@ class Router {
this.#hashchange() this.#hashchange()
} }
/**
* 用于同步路由到组件的
*/
rsync(target, callback) {
this.#targets.set(target, callback)
// 路由已经初始化完成时, 还有新的同步请求则立刻执行
if (this.#ready) {
this.#rsync()
}
}
beforeEach(callback = noop) { beforeEach(callback = noop) {
this.#beforeEach = callback this.#beforeEach = callback
} }
@ -219,6 +233,9 @@ class Router {
return return
} }
// 缓存当前路由信息, 当没有匹配到正确的路由时, 回调此缓存
this.#tmp = obj
if (this.type === MODE_HASH) { if (this.type === MODE_HASH) {
if (replace) { if (replace) {
location.replace(path.replace(/^\//, '#/')) location.replace(path.replace(/^\//, '#/'))

View File

@ -5,9 +5,15 @@
*/ */
import { Component } from 'wkit' 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) { if (obj === null) {
return obj return obj
} }
@ -17,24 +23,52 @@ function observe(obj) {
let value = Reflect.get(target, key, receiver) let value = Reflect.get(target, key, receiver)
// 当访问的值是对象时,需要对这个对象也进行代理 // 当访问的值是对象时,需要对这个对象也进行代理
if (typeof value === 'object') { if (typeof value === 'object') {
return observe(value) return observe(value, paths.concat(key))
} }
return value return value
}, },
set(target, key, value, receiver) { set(target, key, value, receiver) {
let full = paths.concat(key).join('.')
if (target[key] === value) {
return true
}
Reflect.set(target, key, value, receiver) Reflect.set(target, key, value, receiver)
if (STORE_CALLBACKS.get(full)) {
STORE_CALLBACKS.get(full).forEach(callback => {
callback(value)
})
}
window.wkitd.broadcast() window.wkitd.broadcast()
return true 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 = {}) { export function createStore(obj = {}) {
let defined = false let defined = false
return function () { return function () {
Object.defineProperty(Component.prototype, '$store', { Object.defineProperty(Component.prototype, '$store', {
get() { get() {
window.wkitd.assign(this)
return window.wkitd.get(__STORE__) return window.wkitd.get(__STORE__)
}, },
set(val) { set(val) {
@ -45,8 +79,7 @@ export function createStore(obj = {}) {
} }
window.wkitd.set(__STORE__, observe(val)) window.wkitd.set(__STORE__, observe(val))
defined = true defined = true
}, }
enumerable: false
}) })
Component.prototype.$store = obj Component.prototype.$store = obj
} }

View File

@ -13,9 +13,7 @@ export function readonlyProp(host, name, value) {
Object.defineProperty(host, name, { Object.defineProperty(host, name, {
get() { get() {
return value return value
}, }
set(vale) {},
enumerable: false
}) })
} }