From 0c42d793fa3b06424bc1452bfef7fbb0ee65329e Mon Sep 17 00:00:00 2001 From: yutent Date: Thu, 10 Aug 2023 19:59:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=80=E5=A4=A7=E6=B3=A2=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- src/index.js | 82 ++++++++++++++ src/init.js | 9 ++ src/router/hash-router.js | 179 ++++++++++++++++++++++++++++++ src/router/index.js | 67 +++++++++++ src/router/modern-router.js | 216 ++++++++++++++++++++++++++++++++++++ src/store.js | 17 +++ src/utils.js | 28 +++++ 8 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 src/index.js create mode 100644 src/init.js create mode 100644 src/router/hash-router.js create mode 100644 src/router/index.js create mode 100644 src/router/modern-router.js create mode 100644 src/store.js create mode 100644 src/utils.js diff --git a/.gitignore b/.gitignore index ac30e41..e070fc4 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..cae42dd --- /dev/null +++ b/src/index.js @@ -0,0 +1,82 @@ +/** + * {wkitd} + * @author yutent + * @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`` + } + } else { + App.styles = css` + :host { + font-family: monospace; + color: #647889; + } + .code { + margin: 16px 0; + background: #f7f8fb; + } + ` + App.prototype.render = function () { + return html` +

It works!!!

+ + If you don't use router, you may define the + render property. + +
+
  createApp({
+
    render() {
+
      return html\`<wc-home></wc-home>\`
+
    }
+
  })
+
  .mount()
+
+ ` + } + } + } + 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') + } + })() +} diff --git a/src/init.js b/src/init.js new file mode 100644 index 0000000..acda3ec --- /dev/null +++ b/src/init.js @@ -0,0 +1,9 @@ +/** + * {} + * @author yutent + * @date 2023/08/10 10:19:12 + */ + +import { hideProp } from './utils.js' + +hideProp(window, '__wkitd__', new Map()) diff --git a/src/router/hash-router.js b/src/router/hash-router.js new file mode 100644 index 0000000..40028b2 --- /dev/null +++ b/src/router/hash-router.js @@ -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() +} diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..ce9473c --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,67 @@ +/** + * {} + * @author yutent + * @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}>`) + } + } + } +} + +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 +} diff --git a/src/router/modern-router.js b/src/router/modern-router.js new file mode 100644 index 0000000..d511f92 --- /dev/null +++ b/src/router/modern-router.js @@ -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() +} diff --git a/src/store.js b/src/store.js new file mode 100644 index 0000000..17e91c3 --- /dev/null +++ b/src/store.js @@ -0,0 +1,17 @@ +/** + * {} + * @author yutent + * @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 () { + // + } +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..ae151ca --- /dev/null +++ b/src/utils.js @@ -0,0 +1,28 @@ +/** + * {} + * @author yutent + * @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 +}