一大波更新

master
yutent 2023-08-10 19:59:02 +08:00
parent eab9de2c2c
commit 0c42d793fa
8 changed files with 600 additions and 1 deletions

3
.gitignore vendored
View File

@ -1,10 +1,11 @@
*.min.js
.httpserver
index.html
test.js
app.js
.vscode
node_modules/
dist/
test
*.sublime-project
*.sublime-workspace
package-lock.json

82
src/index.js Normal file
View File

@ -0,0 +1,82 @@
/**
* {wkitd}
* @author yutent<yutent.io@gmail.com>
* @date 2023/08/10 10:02:15
*/
import './init.js'
import { html, css, Component } from 'wkit'
import { noop } from './utils.js'
export * from './router/index.js'
export { createStore } from './store.js'
class App extends Component {}
export function createApp({
data = {},
methods = {},
mounted = noop,
render
} = {}) {
//
return new (function () {
App.props = data
Object.assign(App.prototype, methods, { mounted })
this.use = function (plugin = noop, ...args) {
plugin.apply(this, args)
return this
}
this.mount = function () {
let $router = window.__wkitd__.get('$router')
if (render) {
App.prototype.render = render
} else {
if ($router) {
App.prototype.render = function () {
return html`<router-view></router-view>`
}
} else {
App.styles = css`
:host {
font-family: monospace;
color: #647889;
}
.code {
margin: 16px 0;
background: #f7f8fb;
}
`
App.prototype.render = function () {
return html`
<h1>It works!!!</h1>
<cite>
If you don't use router, you may define the
<b>render</b> property.
</cite>
<div class="code">
<pre><code> createApp({</code></pre>
<pre><code> render() {</code></pre>
<pre><code> return html\`&lt;wc-home&gt;&lt;/wc-home&gt;\`</code></pre>
<pre><code> }</code></pre>
<pre><code> })</code></pre>
<pre><code> .mount()</code></pre>
</div>
`
}
}
}
if ($router) {
App.prototype.mounted = function (...args) {
let $view = window.__wkitd__.get('ROUTER_VIEW')
$view.sync($router.getViews())
$router.init()
mounted.call(this, ...args)
}
}
App.reg('app')
}
})()
}

9
src/init.js Normal file
View File

@ -0,0 +1,9 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2023/08/10 10:19:12
*/
import { hideProp } from './utils.js'
hideProp(window, '__wkitd__', new Map())

179
src/router/hash-router.js Normal file
View File

@ -0,0 +1,179 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2017-04-14 21:04:50
*
*/
import { bind, fire } from 'wkit'
import { hideProp, targetIsThisWindow } from '../utils.js'
//hash前缀正则
const PREFIX_REGEXP = /^(#!|#)[\/]?/
const TRIM_REGEXP = /(^[/]+)|([/]+$)/g
const DEFAULT_OPTIONS = {
prefix: '!/',
allowReload: true //连续点击同一个链接是否重新加载
}
const RULE_REGEXP =
/(:id)|(\{id\})|(\{id:([A-z\d\,\[\]\{\}\-\+\*\?\!:\^\$]*)\})/g
const __ready__ = Symbol('ready')
class Router {
[__ready__] = false
#tables = new Map()
#views = new Set()
#options = Object.create(null)
#route = Object.create(null)
constructor(options = {}) {
Object.assign(this.#options, DEFAULT_OPTIONS, options)
bind(window, 'load, popstate', this.#hashchange.bind(this))
}
get route() {
return this.#route
}
#hashchange(ev) {
if (ev?.type === 'load') {
if (this[__ready__]) {
return
}
this[__ready__] = true
}
let path = location.hash
path = path.replace(PREFIX_REGEXP, '').trim()
path = path.replace(TRIM_REGEXP, '')
if (ev?.type === 'load') {
this.go(path)
// hash模式要手动触发一下路由检测
this.#check(path)
} else {
this.#check(path)
}
}
#parseRule(route) {
if (route.path === '!') {
route.regexp = null
} else {
let re = route.path.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 + '$'
route.regexp = new RegExp(re)
}
return route
}
#add(route) {
// 特殊值"!", 则自动作非匹配回调处理
// if (route.path === '!') {
// // this.noMatch = callback
// this.#views.add(route.name)
// return
// }
if (route.path !== '!' && route.path[0] !== '/') {
console.error('路由规则必须以"/"开头')
return
}
route.path = route.path.replace(/^[\/]+|[\/]+$|\s+/g, '')
this.#tables.set(route.path, this.#parseRule(route))
}
// 路由检测
#check(path) {
let { allowReload } = this.#options
let $view = window.__wkitd__.get('ROUTER_VIEW')
if (!$view || (!allowReload && path === this.#route.path)) {
return
}
// console.log('<><><><>')
for (let [k, route] of this.#tables) {
let args = path.match(route.regexp)
if (args) {
args.shift()
$view.current = route.name
this.#route.path = path
this.#route.name = route.name
this.#route.params = args
this.#route.query = {}
break
}
}
// this.noMatch && this.noMatch(this.path)
}
init() {
this.#hashchange()
}
getViews() {
return Array.from(this.#views)
}
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
}
// 绑定路由事件
addRoute(routes) {
if (Array.isArray(routes)) {
routes.forEach(it => {
this.#add(it)
})
} else {
this.#add(routes)
}
// 因为先初始化,才开始监听路由规则
// 所以会导致wondow load的时候, 规则还没生效, 而生效之后,load已经结束
// 所以这里需要手动再触发一次load
if (this[__ready__]) {
this.#hashchange()
} else {
fire(window, 'load')
}
}
}
export default function () {
return new Router()
}

67
src/router/index.js Normal file
View File

@ -0,0 +1,67 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2023/08/10 10:10:06
*/
import { Component, html, raw } from 'wkit'
import createWebHashHistory from './hash-router.js'
import createWebHistory from './modern-router.js'
export { createWebHashHistory, createWebHistory }
class Router extends Component {
static props = {
keepAlive: false,
current: 'str!'
}
#views = []
created() {
window.__wkitd__.set('ROUTER_VIEW', this)
}
sync(views) {
this.#views = views
}
render() {
if (this.keepAlive) {
return html``
} else {
if (this.current) {
return raw(`<${this.current}></${this.current}>`)
}
}
}
}
if (!customElements.get('router-view')) {
customElements.define('router-view', Router)
}
export function createRouter(
{ history = createWebHashHistory, routes = [] } = {},
options
) {
let $router = history(options)
window.__wkitd__.set('$router', $router)
Component.prototype.$router = $router
$router.addRoute(routes)
return function () {
return $router
}
}
export function getRouter() {
return window.__wkitd__.get('$router')
}
export function getCurrentPage() {
return getRouter().route
}

216
src/router/modern-router.js Normal file
View File

@ -0,0 +1,216 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2017-04-14 21:04:50
*
*/
import { bind } from 'wkit'
import { hideProp, targetIsThisWindow } from '../utils.js'
//hash前缀正则
const PREFIX_REGEXP = /^(#!|#)[\/]?/
const TRIM_REGEXP = /(^[/]+)|([/]+$)/g
const DEFAULT_OPTIONS = {
mode: 'hash', // hash | history
allowReload: true //连续点击同一个链接是否重新加载
}
const LINKS = []
const RULE_REGEXP =
/(:id)|(\{id\})|(\{id:([A-z\d\,\[\]\{\}\-\+\*\?\!:\^\$]*)\})/g
class Router {
constructor(options) {
hideProp(this, 'table', [])
hideProp(this, 'last', '')
hideProp(this, 'path', '')
hideProp(this, 'pathArr', [])
hideProp(this, 'ready', false)
hideProp(this, 'noMatch', null)
hideProp(this, 'options', Object.assign({}, DEFAULT_OPTIONS, options))
this.__listen__()
}
// 事件监听
__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)
}
// 跳转到路由
go(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)
}
}
// 绑定路由事件
on(rule, callback) {
if (Array.isArray(rule)) {
rule.forEach(it => {
this.__add__(it, callback)
})
} else {
this.__add__(rule, callback)
}
// 因为先初始化,才开始监听路由规则
// 所以会导致wondow load的时候, 规则还没生效, 而生效之后,load已经结束
// 所以这里需要手动再触发一次load
Anot.fireDom(window, 'load')
}
}
export default function () {
return new Router()
}

17
src/store.js Normal file
View File

@ -0,0 +1,17 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2023/08/10 11:57:38
*/
import { Component } from 'wkit'
export function createStore(obj = {}) {
window.__wkitd__.$store = obj
Component.prototype.$store = obj
return function () {
//
}
}

28
src/utils.js Normal file
View File

@ -0,0 +1,28 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2023/08/10 10:07:51
*/
export function noop() {}
export function hideProp(host, name, value) {
Object.defineProperty(host, name, {
value,
enumerable: false,
writable: true
})
}
// 判定A标签的target属性是否指向自身
export function targetIsThisWindow(target) {
if (
!target ||
target === window.name ||
target === '_self' ||
(target === 'top' && window == window.top)
) {
return true
}
return false
}