Compare commits

..

21 Commits

Author SHA1 Message Date
yutent a9ff9f0323 优化路由,支持首页带index.html访问; 优化router-link样式 2024-05-15 15:31:17 +08:00
yutent d3a00a319f 修复异步路由加载逻辑; 优化router-link组件的赋值 2024-05-11 15:20:34 +08:00
yutent 37dcc51e05 修复路由不存在时的报错 2024-05-09 16:47:26 +08:00
yutent a1c1740166 增加路由异步加载 2024-05-09 16:15:52 +08:00
yutent b7567a15fc 1.3.6 2023-12-29 09:55:43 +08:00
yutent ba92054633 路由规则增加特殊符号~!@$-= 2023-12-29 09:51:50 +08:00
yutent a24ab67694 1.3.5 2023-12-27 18:21:27 +08:00
yutent 9c9561cb1c 优化路由检测, 路由不存在时也触发回调, 行为与vue-router保持一致 2023-12-27 16:44:52 +08:00
yutent fc20ea24d4 优化router-link组件无初始值的点击事件 2023-12-27 15:50:54 +08:00
yutent 137c79fce7 移除调试代码 2023-12-27 11:00:47 +08:00
yutent 5136605322 修复路由一处bug 2023-12-27 10:58:37 +08:00
yutent 1e08003ba7 Update Readme.md 2023-11-15 20:18:55 +08:00
yutent d68ecadb4e 更新readme 2023-11-15 19:00:53 +08:00
yutent e38f1c51dc 优化路由检测 2023-11-15 18:27:22 +08:00
yutent 66971278a2 增加watch方法;优化状态管理;优化路由; 2023-11-15 17:28:22 +08:00
yutent 1cddfb6731 路由小幅重构;keep-alive更合理 2023-09-25 14:15:32 +08:00
yutent 5e9dd986df 调整keepAlive逻辑 2023-09-19 11:57:57 +08:00
yutent d1e81474c0 store支持深层代理 2023-09-04 14:00:23 +08:00
yutent f46b35f9bf 迁移仓库 2023-08-30 16:32:50 +08:00
yutent 76b3003182 将app注入到所有的组件的原型链中 2023-08-16 11:03:54 +08:00
yutent 285e64bbed 删除多余文件;修复hash路由默认值;优化router-link样式 2023-08-16 10:57:17 +08:00
11 changed files with 247 additions and 334 deletions

View File

@ -5,7 +5,7 @@
![version](https://img.shields.io/npm/v/wkitd.svg)
### 开发文档
[开发文档](https://github.com/bytedo/wkitd/wiki)
[开发文档](https://git.wkit.fun/bytedo/wkitd/wiki)
### 我们的特色
@ -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>

View File

@ -1,6 +1,6 @@
{
"name": "wkitd",
"version": "1.1.0",
"version": "1.3.10",
"type": "module",
"main": "dist/index.js",
"files": [
@ -13,6 +13,6 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/bytedo/wkitd.git"
"url": "git+https://git.wkit.fun/bytedo/wkitd.git"
}
}

View File

@ -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()

View File

@ -6,16 +6,17 @@
import './init.js'
import { html, css, Component } from 'wkit'
import { noop } from './utils.js'
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 {}
export function createApp({
data = {},
styles = [],
methods = {},
mounted = noop,
render
@ -24,12 +25,19 @@ export function createApp({
return new (function () {
App.props = data
Object.assign(App.prototype, methods, { mounted })
App.styles = styles
Object.assign(App.prototype, methods, {
mounted,
created() {
readonlyProp(Component.prototype, '$app', this)
}
})
this.use = function (plugin = noop, ...args) {
plugin.apply(App.prototype, args)
return this
}
this.mount = function () {
let $router = window.wkitd.get(__ROUTER__)
if (render) {
@ -69,6 +77,7 @@ export function createApp({
}
}
}
if ($router) {
App.prototype.mounted = function (...args) {
let $view = window.wkitd.get(__ROUTER_VIEW__)

View File

@ -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)
}
}

View File

@ -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)

View File

@ -1,219 +0,0 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2017-04-14 21:04:50
*
*/
import { bind, fire } from 'wkit'
import {
noop,
targetIsThisWindow,
query2object,
object2query
} from '../utils.js'
//hash前缀正则
const PREFIX_REGEXP = /^(#!|#)[\/]+?/
const RULE_REGEXP = /(\/[^/]*)(:[A-Za-z0-9_]+)(\?)?/g
class Router {
type = 'history'
#tables = new Map()
#views = new Set()
#targets = new Map()
#ready = false
#route = Object.create(null)
#beforeEach
constructor() {
// bind(window, 'popstate', this.#hashchange.bind(this))
}
// 事件监听
__listen__() {
let { mode } = this.options
bind(window, 'load, popstate', ev => {
if (ev.type === 'load') {
if (this.ready) {
return
}
this.ready = true
}
let path = mode === 'hash' ? location.hash : location.pathname
path = path.replace(PREFIX_REGEXP, '').trim()
path = path.replace(TRIM_REGEXP, '')
if (ev.type === 'load') {
this.go(path)
// hash模式要手动触发一下路由检测
if (mode === 'hash') {
this.__check__(path)
}
} else {
// 因为pushState不会触发popstate事件,
// 所以这里只在hash模式或有ev.state的情况下才会主动触发路由检测
if (mode === 'hash' || ev.state) {
this.__check__(path)
}
}
})
//劫持页面上所有点击事件,如果事件源来自链接或其内部,
//并且它不会跳出本页,并且以"#/"或"#!/"开头那么触发go方法
bind(document, 'click', ev => {
let prevented =
'defaultPrevented' in ev
? ev.defaultPrevented
: ev.returnValue === false
if (prevented || ev.ctrlKey || ev.metaKey || ev.which === 2) {
return
}
let target = ev.target
while (target.nodeName !== 'A') {
target = target.parentNode
if (!target || target.tagName === 'BODY') {
return
}
}
if (mode === 'history') {
if (targetIsThisWindow(target.target)) {
let href =
target.getAttribute('href') || target.getAttribute('xlink:href')
if (
!href ||
/^(http[s]?:|ftp:)?\/\//.test(href) ||
/^javascript:/.test(href)
) {
return
}
// hash地址,只管修正前缀即可, 会触发popstate事件,所以这里只处理非hash的情况
if (!PREFIX_REGEXP.test(href)) {
// 非hash地址,则需要阻止默认事件
// 并主动触发跳转, 同时强制清除hash
ev.preventDefault()
this.go(href, true)
}
}
}
})
}
__parseRule__(rule, opts) {
let re = rule.replace(RULE_REGEXP, function (m, p1, p2, p3, p4) {
let w = '([\\w.-]'
if (p1 || p2) {
return w + '+)'
} else {
if (!/^\{[\d\,]+\}$/.test(p4)) {
w = '('
}
return w + p4 + ')'
}
})
re = re
.replace(/(([^\\])([\/]+))/g, '$2\\/')
.replace(/(([^\\])([\.]+))/g, '$2\\.')
.replace(/(([^\\])([\-]+))/g, '$2\\-')
.replace(/(\(.*)(\\[\-]+)(.*\))/g, '$1-$3')
re = '^' + re + '$'
opts.regexp = new RegExp(re)
return opts
}
__add__(rule, callback) {
// 特殊值"!", 则自动作非匹配回调处理
if (rule === '!') {
this.noMatch = callback
return
}
if (rule.charAt(0) !== '/') {
console.error('路由规则必须以"/"开头')
return
}
rule = rule.replace(/^[\/]+|[\/]+$|\s+/g, '')
let opts = { rule, callback }
Anot.Array.ensure(this.table, this.__parseRule__(rule, opts))
}
// 路由检测
__check__(path) {
let { allowReload } = this.options
if (!allowReload && path === this.last) {
return
}
this.last = this.path
this.path = path
this.pathArr = path.split('/')
LINKS.forEach(vm => {
if (vm.rule.test(this.path)) {
vm.active = true
} else {
vm.active = false
}
})
for (let i = 0, route; (route = this.table[i++]); ) {
let args = path.match(route.regexp)
if (args) {
args.shift()
return route.callback.apply(route, args)
}
}
this.noMatch && this.noMatch(this.path)
}
// 跳转到路由
go2(path, forceCleanHash = false) {
path = path.trim().replace(TRIM_REGEXP, '')
let { mode } = this.options
if (mode === 'hash') {
// 页面刷新时, 不主动添加空hash, 避免执行2次noMatch回调
if (!path && path === location.hash) {
return
}
location.hash = '!/' + path
} else {
let hash = forceCleanHash ? '' : location.hash
let search = forceCleanHash ? '' : location.search
if (forceCleanHash) {
window.history.pushState({ path }, null, `/${path + search + hash}`)
} else {
window.history.replaceState({ path }, null, `/${path + search + hash}`)
}
// pushState不会触发popstate事件,所以要手动触发路由检测
this.__check__(path)
}
}
// 绑定路由事件
addRoute(routes) {
// if (Array.isArray(routes)) {
// routes.forEach(it => {
// this.#add(it)
// })
// } else {
// this.#add(routes)
// }
// // 初始化后再添加路由, 手动执行一次回调
// if (this.#ready) {
// this.#hashchange()
// }
}
}
export default function () {
return () => new Router()
}

View File

@ -1,29 +1,59 @@
//
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 = {
keepAlive: false,
transition: false,
current: {
type: String,
default: '',
attribute: false,
observer(v, old) {
if (this.keepAlive && v) {
if (old && this.$refs[old]) {
transition: false
}
static styles = css`
:host {
display: block;
}
`
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()
}
this.$refs[v]?.$requestUpdate()
this.$refs[v]?.$animate()
this.$refs[v]?.activated()
} 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() {
@ -42,48 +72,45 @@ 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__="%s" #animation="%s" style="%s"></${it}>`
: `<${it} ref="${it}" :__keep_alive__="%s" style=%s></${it}>`,
[
this.current === it,
{ ...option, immediate: this.current === 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}>`)
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
}
static styles = css`
:host {
display: inline-flex;
align-items: center;
-webkit-user-select: none;
user-select: none;
}
a {
display: flex;
align-items: center;
justify-content: center;
gap: var(--router-link-gap, 0);
width: 100%;
height: 100%;
color: inherit;
text-decoration: inherit;
cursor: pointer;
@ -94,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
}
@ -107,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 =
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>`
}
}

View File

@ -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
}
)
@ -97,18 +96,27 @@ class Router {
#check() {
let isHash = this.type === MODE_HASH
let $view = window.wkitd.get(__ROUTER_VIEW__)
let hash = location.hash
let hash = location.hash || '#/'
let path = isHash
? location.hash
? 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(/^\//, '#/'))

View File

@ -5,7 +5,62 @@
*/
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, paths = ['$store']) {
if (obj === null) {
return obj
}
return new Proxy(obj, {
get(target, key, receiver) {
let value = Reflect.get(target, key, receiver)
// 当访问的值是对象时,需要对这个对象也进行代理
if (typeof value === 'object') {
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
@ -13,6 +68,7 @@ export function createStore(obj = {}) {
return function () {
Object.defineProperty(Component.prototype, '$store', {
get() {
window.wkitd.assign(this)
return window.wkitd.get(__STORE__)
},
set(val) {
@ -21,19 +77,9 @@ export function createStore(obj = {}) {
'Can not set readonly property $store of Component'
)
}
window.wkitd.set(
__STORE__,
new Proxy(val, {
set(target, prop, value) {
target[prop] = value
window.wkitd.broadcast()
return true
}
})
)
window.wkitd.set(__STORE__, observe(val))
defined = true
},
enumerable: false
}
})
Component.prototype.$store = obj
}

View File

@ -9,11 +9,11 @@ const decode = decodeURIComponent
export function noop() {}
export function hideProp(host, name, value) {
export function readonlyProp(host, name, value) {
Object.defineProperty(host, name, {
value,
enumerable: false,
writable: true
get() {
return value
}
})
}