Compare commits
24 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 | |
yutent | 5e9dd986df | |
yutent | d1e81474c0 | |
yutent | f46b35f9bf | |
yutent | 76b3003182 | |
yutent | 285e64bbed | |
yutent | 8519bfd4ef | |
yutent | e782d9cf96 | |
yutent | 409ba1905e |
17
Readme.md
17
Readme.md
|
@ -5,22 +5,24 @@
|
||||||
![version](https://img.shields.io/npm/v/wkitd.svg)
|
![version](https://img.shields.io/npm/v/wkitd.svg)
|
||||||
|
|
||||||
### 开发文档
|
### 开发文档
|
||||||
[开发文档](https://github.com/bytedo/wkitd/wikid)
|
[开发文档](https://git.wkit.fun/bytedo/wkitd/wiki)
|
||||||
|
|
||||||
|
|
||||||
### 我们的特色
|
### 我们的特色
|
||||||
|
|
||||||
- 提供迷你的单页应用开发环境
|
- 提供迷你的单页应用开发环境
|
||||||
- 无需`node.js`编译, 即写即用。
|
- 无需`node.js`编译, 即写即用。
|
||||||
|
- 本项目适合开发一些快速迭代简单页面、app内嵌的`webview`页面(无须搭建复杂的前端开发环境)
|
||||||
|
|
||||||
|
|
||||||
### 一些注意事项
|
### 一些注意事项
|
||||||
|
|
||||||
- 路由不支持嵌套, 即`<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()`获取。
|
||||||
- `$store`对象为非响应式, 后续可能会支持(时间无限期延后)。
|
- `watch()`方法, 可用于监听`$store`和`$route`的变化。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 示例
|
### 示例
|
||||||
|
@ -90,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>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "wkitd",
|
"name": "wkitd",
|
||||||
"version": "1.0.0",
|
"version": "1.3.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"files": [
|
"files": [
|
||||||
|
@ -13,6 +13,6 @@
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/bytedo/wkitd.git"
|
"url": "git+https://git.wkit.fun/bytedo/wkitd.git"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* {框架常量}
|
||||||
|
* @author yutent<yutent.io@gmail.com>
|
||||||
|
* @date 2023/08/15 10:15:54
|
||||||
|
*/
|
||||||
|
|
||||||
|
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()
|
28
src/index.js
28
src/index.js
|
@ -6,15 +6,17 @@
|
||||||
|
|
||||||
import './init.js'
|
import './init.js'
|
||||||
import { html, css, Component } from 'wkit'
|
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 * from './router/index.js'
|
||||||
export { createStore } from './store.js'
|
export { createStore, watch } from './store.js'
|
||||||
|
|
||||||
class App extends Component {}
|
class App extends Component {}
|
||||||
|
|
||||||
export function createApp({
|
export function createApp({
|
||||||
data = {},
|
data = {},
|
||||||
|
styles = [],
|
||||||
methods = {},
|
methods = {},
|
||||||
mounted = noop,
|
mounted = noop,
|
||||||
render
|
render
|
||||||
|
@ -23,14 +25,21 @@ export function createApp({
|
||||||
|
|
||||||
return new (function () {
|
return new (function () {
|
||||||
App.props = data
|
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) {
|
this.use = function (plugin = noop, ...args) {
|
||||||
plugin.apply(this, args)
|
plugin.apply(App.prototype, args)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mount = function () {
|
this.mount = function () {
|
||||||
let $router = window.__wkitd__.get('$router')
|
let $router = window.wkitd.get(__ROUTER__)
|
||||||
if (render) {
|
if (render) {
|
||||||
App.prototype.render = render
|
App.prototype.render = render
|
||||||
} else {
|
} else {
|
||||||
|
@ -68,9 +77,10 @@ export function createApp({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($router) {
|
if ($router) {
|
||||||
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.views)
|
$view.sync($router.views)
|
||||||
$router.init() // mounted 时正式初始化路由
|
$router.init() // mounted 时正式初始化路由
|
||||||
|
@ -88,13 +98,13 @@ export function createApp({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStore() {
|
export function getStore() {
|
||||||
return window.__wkitd__.get('$store')
|
return window.wkitd.get(__STORE__)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRouter() {
|
export function getRouter() {
|
||||||
return window.__wkitd__.get('$router')
|
return window.wkitd.get(__ROUTER__)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCurrentPage() {
|
export function getCurrentPage() {
|
||||||
return window.__wkitd__.get('$router')?.route
|
return window.wkitd.get(__ROUTER__)?.route
|
||||||
}
|
}
|
||||||
|
|
44
src/init.js
44
src/init.js
|
@ -4,6 +4,46 @@
|
||||||
* @date 2023/08/10 10:19:12
|
* @date 2023/08/10 10:19:12
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { hideProp } from './utils.js'
|
import { Component } from 'wkit'
|
||||||
|
import { WKITD_COMPONENTS } from './constants.js'
|
||||||
|
|
||||||
hideProp(window, '__wkitd__', new Map())
|
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.delete(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const __WKITD__ = new Wkitd()
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'wkitd', {
|
||||||
|
get() {
|
||||||
|
return __WKITD__
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
console.error('Can not set readonly property wkitd of window')
|
||||||
|
},
|
||||||
|
enumerable: false
|
||||||
|
})
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { Component } from 'wkit'
|
import { Component } from 'wkit'
|
||||||
import { createWebHashHistory, createWebHistory } from './router-engine.js'
|
import { createWebHashHistory, createWebHistory } from './router-engine.js'
|
||||||
import './router-components.js'
|
import './router-components.js'
|
||||||
|
import { __ROUTER__ } from '../constants.js'
|
||||||
|
|
||||||
export { createWebHashHistory, createWebHistory }
|
export { createWebHashHistory, createWebHistory }
|
||||||
|
|
||||||
|
@ -15,20 +16,21 @@ export function createRouter({
|
||||||
} = {}) {
|
} = {}) {
|
||||||
let $router = history()
|
let $router = history()
|
||||||
|
|
||||||
window.__wkitd__.set('$router', $router)
|
window.wkitd.set(__ROUTER__, $router)
|
||||||
|
|
||||||
$router.addRoute(routes)
|
$router.addRoute(routes)
|
||||||
function wrapper() {
|
function wrapper() {
|
||||||
Component.prototype.$router = $router
|
Object.defineProperty(Component.prototype, '$router', {
|
||||||
|
get() {
|
||||||
|
return $router
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Object.defineProperty(Component.prototype, '$route', {
|
||||||
|
get() {
|
||||||
|
return $router.route
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
wrapper.beforeEach = $router.beforeEach.bind($router)
|
wrapper.beforeEach = $router.beforeEach.bind($router)
|
||||||
return wrapper
|
return wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRouter() {
|
|
||||||
return window.__wkitd__.get('$router')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCurrentPage() {
|
|
||||||
return getRouter().route
|
|
||||||
}
|
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -1,32 +1,63 @@
|
||||||
//
|
//
|
||||||
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__, ROUTE_CALLBACKS } from '../constants.js'
|
||||||
|
import { watch } from '../store.js'
|
||||||
|
|
||||||
class RouterView extends Component {
|
class RouterView extends Component {
|
||||||
static props = {
|
static props = {
|
||||||
keepAlive: false,
|
transition: false
|
||||||
transition: false,
|
}
|
||||||
current: {
|
|
||||||
type: String,
|
static styles = css`
|
||||||
default: '',
|
:host {
|
||||||
attribute: false,
|
display: block;
|
||||||
observer(v, old) {
|
}
|
||||||
if (this.keepAlive && v) {
|
`
|
||||||
if (old && this.$refs[old]) {
|
|
||||||
|
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].deactivated()
|
||||||
|
this.$refs[old].remove()
|
||||||
|
} else {
|
||||||
|
this.$requestUpdate()
|
||||||
}
|
}
|
||||||
this.$refs[v]?.$requestUpdate()
|
} else {
|
||||||
this.$refs[v]?.$animate()
|
this.$requestUpdate()
|
||||||
this.$refs[v]?.activated()
|
}
|
||||||
|
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() {
|
||||||
window.__wkitd__.set('ROUTER_VIEW', this)
|
window.wkitd.set(__ROUTER_VIEW__, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
sync(views) {
|
sync(views) {
|
||||||
|
@ -41,48 +72,45 @@ 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__="%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.current) {
|
||||||
if (this.transition) {
|
if (this.transition) {
|
||||||
return raw(`<${this.current} #animation="%s"></${this.current}>`, [
|
return raw(
|
||||||
option
|
`<${this.current} ref="${this.current}" ${
|
||||||
])
|
this.keepAlive ? 'keep-alive' : ''
|
||||||
}
|
} #animation="%s"></${this.current}>`,
|
||||||
return raw(`<${this.current}></${this.current}>`)
|
[option]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--router-link-gap, 0);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -93,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
|
||||||
}
|
}
|
||||||
|
@ -106,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('?')
|
||||||
|
path = tmp[0]
|
||||||
|
params = tmp[1] || ''
|
||||||
|
query = query2object(params)
|
||||||
|
} else {
|
||||||
|
path = this.to.path || ''
|
||||||
|
query = this.to.query || {}
|
||||||
|
params =
|
||||||
typeof query === 'string'
|
typeof query === 'string'
|
||||||
? query.replaceAll('?', '')
|
? query.replaceAll('?', '')
|
||||||
: object2query(query)
|
: 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>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +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__, 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'
|
||||||
|
@ -20,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
|
||||||
|
|
||||||
|
@ -65,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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -95,19 +95,28 @@ class Router {
|
||||||
// 路由检测
|
// 路由检测
|
||||||
#check() {
|
#check() {
|
||||||
let isHash = this.type === MODE_HASH
|
let isHash = this.type === MODE_HASH
|
||||||
let $view = window.__wkitd__.get('ROUTER_VIEW')
|
let $view = window.wkitd.get(__ROUTER_VIEW__)
|
||||||
let hash = location.hash
|
let hash = location.hash || '#/'
|
||||||
let path = isHash
|
let path = isHash
|
||||||
? location.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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,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
|
||||||
}
|
}
|
||||||
|
@ -218,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(/^\//, '#/'))
|
||||||
|
|
77
src/store.js
77
src/store.js
|
@ -5,27 +5,82 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from 'wkit'
|
import { Component } from 'wkit'
|
||||||
|
import {
|
||||||
|
__STORE__,
|
||||||
|
__ROUTER__,
|
||||||
|
STORE_CALLBACKS,
|
||||||
|
ROUTE_CALLBACKS
|
||||||
|
} from './constants.js'
|
||||||
|
import { noop } from './utils.js'
|
||||||
|
|
||||||
export function createStore(obj = {}) {
|
function observe(obj, paths = ['$store']) {
|
||||||
window.__wkitd__.set(
|
if (obj === null) {
|
||||||
'$store',
|
return obj
|
||||||
new Proxy(obj, {
|
}
|
||||||
set(target, prop, value) {
|
|
||||||
target[prop] = value
|
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
|
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 () {
|
return function () {
|
||||||
Object.defineProperty(Component.prototype, '$store', {
|
Object.defineProperty(Component.prototype, '$store', {
|
||||||
get() {
|
get() {
|
||||||
return window.__wkitd__.get('$store')
|
window.wkitd.assign(this)
|
||||||
|
return window.wkitd.get(__STORE__)
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
console.error('Can not replace readonly property $store of Component')
|
if (defined) {
|
||||||
},
|
return console.error(
|
||||||
enumerable: false
|
'Can not set readonly property $store of Component'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
window.wkitd.set(__STORE__, observe(val))
|
||||||
|
defined = true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
Component.prototype.$store = obj
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,11 @@ const decode = decodeURIComponent
|
||||||
|
|
||||||
export function noop() {}
|
export function noop() {}
|
||||||
|
|
||||||
export function hideProp(host, name, value) {
|
export function readonlyProp(host, name, value) {
|
||||||
Object.defineProperty(host, name, {
|
Object.defineProperty(host, name, {
|
||||||
value,
|
get() {
|
||||||
enumerable: false,
|
return value
|
||||||
writable: true
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue