|
|
|
|
|
import vproxy from './vproxy.js'
|
|
|
|
|
|
import vget from './vget.js'
|
|
|
|
|
|
|
|
|
|
|
|
// 解析URL字符串,提取路径、查询参数和hash
|
|
|
|
|
|
function parseUrlString(urlString, scoped) {
|
|
|
|
|
|
let url
|
|
|
|
|
|
|
|
|
|
|
|
let path
|
|
|
|
|
|
// 判断是否为完整URL(包含协议)
|
|
|
|
|
|
if (urlString.startsWith('http://') || urlString.startsWith('https://')) {
|
|
|
|
|
|
url = new URL(urlString)
|
|
|
|
|
|
// 如果是外部URL,返回null(不处理外部链接)
|
|
|
|
|
|
if (url.origin !== window.location.origin) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (url.pathname.startsWith(scoped)) {
|
|
|
|
|
|
path = url.pathname.slice(scoped.length) // 去掉根路径
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 相对路径,基于当前origin构建完整URL
|
|
|
|
|
|
url = new URL(urlString, window.location.href)
|
|
|
|
|
|
path = url.pathname
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析查询参数
|
|
|
|
|
|
const query = {}
|
|
|
|
|
|
url.searchParams.forEach((value, key) => {
|
|
|
|
|
|
query[key] = value
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
path: path,
|
|
|
|
|
|
query,
|
|
|
|
|
|
hash: url.hash
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VRouter {
|
|
|
|
|
|
#routes = []
|
|
|
|
|
|
#history = []
|
|
|
|
|
|
#current = null
|
|
|
|
|
|
#scoped = ''
|
|
|
|
|
|
#listeners = []
|
|
|
|
|
|
#pageCache = new Map()
|
|
|
|
|
|
#node = null
|
|
|
|
|
|
#env = null
|
|
|
|
|
|
#originContent = []
|
|
|
|
|
|
#loaded = false
|
|
|
|
|
|
#vhtml = null
|
|
|
|
|
|
#routesByName = new Map() // 添加按名称索引的路由缓存
|
|
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
this.init()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get routes() { return this.#routes.slice() }
|
|
|
|
|
|
get history() { return this.#history.slice() }
|
|
|
|
|
|
get current() { return this.#current }
|
|
|
|
|
|
get query() { return this.#current?.query || {} }
|
|
|
|
|
|
get params() { return this.#current?.params || {} }
|
|
|
|
|
|
get scoped() { return this.#scoped }
|
|
|
|
|
|
|
|
|
|
|
|
onChange(fc) {
|
|
|
|
|
|
this.#listeners.push(fc)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addRoute(route) {
|
|
|
|
|
|
if (!route.path) throw new Error('Route must have a path')
|
|
|
|
|
|
if (route.path != '/' && route.path.endsWith('/')) {
|
|
|
|
|
|
route.path = route.path.slice(0, -1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const routeConfig = {
|
|
|
|
|
|
path: route.path,
|
|
|
|
|
|
component: route.component,
|
|
|
|
|
|
name: route.name,
|
|
|
|
|
|
meta: route.meta || {},
|
|
|
|
|
|
children: route.children || [],
|
|
|
|
|
|
matcher: new RouteMatcher(route.path, route.name),
|
|
|
|
|
|
description: route.description || '',
|
|
|
|
|
|
layout: route.layout || '',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.#routes.push(routeConfig)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有名称,添加到名称索引中
|
|
|
|
|
|
if (route.name) {
|
|
|
|
|
|
this.#routesByName.set(route.name, routeConfig)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 递归处理子路由
|
|
|
|
|
|
if (route.children?.length > 0) {
|
|
|
|
|
|
route.children.forEach(child => {
|
|
|
|
|
|
const childPath = route.path + (child.path.startsWith('/') ? child.path : '/' + child.path)
|
|
|
|
|
|
const layout = child.layout || route.layout || ''
|
|
|
|
|
|
const meta = { ...route.meta, ...child.meta }
|
|
|
|
|
|
this.addRoute({ ...child, path: childPath, parent: routeConfig, layout, meta })
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addRoutes(routes) {
|
|
|
|
|
|
routes.forEach(route => this.addRoute(route))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notifyListeners(to, from) {
|
|
|
|
|
|
this.#listeners.forEach(listener => {
|
|
|
|
|
|
if (typeof listener === 'function') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
listener(to, from)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error in router listener:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#setRouterPath(matchedRoute) {
|
|
|
|
|
|
const oldRoute = this.#current
|
|
|
|
|
|
|
|
|
|
|
|
this.#current = {
|
|
|
|
|
|
path: matchedRoute.path,
|
|
|
|
|
|
fullPath: matchedRoute.fullPath,
|
|
|
|
|
|
params: matchedRoute.params || {},
|
|
|
|
|
|
query: matchedRoute.query || {},
|
|
|
|
|
|
hash: new URL(matchedRoute.fullPath, window.location.origin).hash,
|
|
|
|
|
|
meta: matchedRoute.route?.meta || {},
|
|
|
|
|
|
description: matchedRoute.route?.description || '',
|
|
|
|
|
|
layout: matchedRoute.route?.layout || '',
|
|
|
|
|
|
name: matchedRoute.route?.name,
|
|
|
|
|
|
matched: matchedRoute.route ? [matchedRoute.route] : []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.#history.push(this.#current)
|
|
|
|
|
|
if (this.#scoped && !matchedRoute.fullPath.startsWith('http')) {
|
|
|
|
|
|
history.pushState({}, '', this.#scoped + matchedRoute.fullPath)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
history.pushState({}, '', matchedRoute.fullPath)
|
|
|
|
|
|
}
|
|
|
|
|
|
this.#notifyListeners(this.#current, oldRoute)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 优化后的路由匹配方法,支持多种参数类型
|
|
|
|
|
|
matchRoute(to) {
|
|
|
|
|
|
// 处理不同类型的路由参数
|
|
|
|
|
|
const routeInfo = this.normalizeRouteTarget(to)
|
|
|
|
|
|
if (!routeInfo) return null
|
|
|
|
|
|
|
|
|
|
|
|
const { path, query, params, name } = routeInfo
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是按名称匹配
|
|
|
|
|
|
if (name) {
|
|
|
|
|
|
const route = this.#routesByName.get(name)
|
|
|
|
|
|
if (!route) return null
|
|
|
|
|
|
|
|
|
|
|
|
// 构建带参数的路径
|
|
|
|
|
|
let resolvedPath = route.path
|
|
|
|
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
|
|
|
|
resolvedPath = resolvedPath.replace(`:${key}`, value)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const match = route.matcher.match(resolvedPath)
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
route,
|
|
|
|
|
|
params: { ...match.params, ...params },
|
|
|
|
|
|
matched: match.matched,
|
|
|
|
|
|
path: resolvedPath,
|
|
|
|
|
|
query,
|
|
|
|
|
|
name
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按路径匹配
|
|
|
|
|
|
for (const route of this.#routes) {
|
|
|
|
|
|
const match = route.matcher.match(path)
|
|
|
|
|
|
if (match && route.component) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
route,
|
|
|
|
|
|
params: { ...match.params, ...params },
|
|
|
|
|
|
matched: match.matched,
|
|
|
|
|
|
description: route.description,
|
|
|
|
|
|
layout: route.layout,
|
|
|
|
|
|
path,
|
|
|
|
|
|
query,
|
|
|
|
|
|
name: route.name
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标准化路由目标参数
|
|
|
|
|
|
normalizeRouteTarget(to) {
|
|
|
|
|
|
let path, query = {}, params = {}, hash = '', name
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof to === 'string') {
|
|
|
|
|
|
// 字符串类型:解析可能包含的URL、query、hash
|
|
|
|
|
|
const parsed = parseUrlString(to, this.#scoped)
|
|
|
|
|
|
if (!parsed) return null // 外部URL或解析失败
|
|
|
|
|
|
|
|
|
|
|
|
path = parsed.path
|
|
|
|
|
|
query = { ...parsed.query }
|
|
|
|
|
|
hash = parsed.hash
|
|
|
|
|
|
} else if (to && typeof to === 'object') {
|
|
|
|
|
|
if (to.path) {
|
|
|
|
|
|
// {path} 类型:path可能也包含query和hash
|
|
|
|
|
|
const parsed = parseUrlString(to.path, this.#scoped)
|
|
|
|
|
|
if (!parsed) return null
|
|
|
|
|
|
|
|
|
|
|
|
path = parsed.path
|
|
|
|
|
|
// 合并query参数,对象中的query优先级更高
|
|
|
|
|
|
query = { ...parsed.query, ...(to.query || {}) }
|
|
|
|
|
|
hash = to.hash || parsed.hash
|
|
|
|
|
|
params = to.params || {}
|
|
|
|
|
|
} else if (to.name) {
|
|
|
|
|
|
// {name} 类型
|
|
|
|
|
|
name = to.name
|
|
|
|
|
|
query = to.query || {}
|
|
|
|
|
|
params = to.params || {}
|
|
|
|
|
|
hash = to.hash || ''
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标准化路径
|
|
|
|
|
|
if (path && !path.startsWith('/')) {
|
|
|
|
|
|
path = '/' + path
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.#scoped) {
|
|
|
|
|
|
path = path.startsWith(this.#scoped) ? path.slice(this.#scoped.length) : path
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!path.startsWith('/')) {
|
|
|
|
|
|
path = '/' + path
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (path != '/' && path.endsWith('/')) {
|
|
|
|
|
|
path = path.slice(0, -1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { path, query, params, hash, name }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
matchTo(to) {
|
|
|
|
|
|
const matchResult = this.matchRoute(to)
|
|
|
|
|
|
if (!matchResult) return null
|
|
|
|
|
|
|
|
|
|
|
|
const { route, params, query, path, name } = matchResult
|
|
|
|
|
|
|
|
|
|
|
|
// 构建查询字符串
|
|
|
|
|
|
let search = ''
|
|
|
|
|
|
if (query && Object.keys(query).length > 0) {
|
|
|
|
|
|
search = '?' + Object.entries(query)
|
|
|
|
|
|
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
|
|
|
|
.join('&')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fullPath = (path || matchResult.path) + search
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
route,
|
|
|
|
|
|
params,
|
|
|
|
|
|
query,
|
|
|
|
|
|
name: name || route.name,
|
|
|
|
|
|
path: path || matchResult.path,
|
|
|
|
|
|
fullPath,
|
|
|
|
|
|
matched: [route]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
buildUrl(baseUrl, additionalQuery = {}) {
|
|
|
|
|
|
const url = new URL(baseUrl, window.location.origin)
|
|
|
|
|
|
Object.entries(additionalQuery).forEach(([key, value]) => {
|
|
|
|
|
|
url.searchParams.append(key, value)
|
|
|
|
|
|
})
|
|
|
|
|
|
return url
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resolveRoutePath(route, params = {}) {
|
|
|
|
|
|
let path = route.component || route.path
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
|
|
|
|
path = path.replace(`:${key}`, value)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (path === '/' || path === '') path = '/index'
|
|
|
|
|
|
if (!path.startsWith('/')) path = '/' + path
|
|
|
|
|
|
if (path.endsWith('.html')) path = path.slice(0, -5)
|
|
|
|
|
|
if (path.endsWith('/')) path = path.slice(0, -1)
|
|
|
|
|
|
|
|
|
|
|
|
return path
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async #navigateTo(matchedRoute) {
|
|
|
|
|
|
if (!matchedRoute) {
|
|
|
|
|
|
console.warn(`No route matched`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { route, params, query } = matchedRoute
|
|
|
|
|
|
|
|
|
|
|
|
const to = {
|
|
|
|
|
|
path: matchedRoute.path,
|
|
|
|
|
|
fullPath: matchedRoute.fullPath,
|
|
|
|
|
|
params,
|
|
|
|
|
|
query,
|
|
|
|
|
|
hash: new URL(matchedRoute.fullPath, window.location.origin).hash,
|
|
|
|
|
|
meta: route.meta,
|
|
|
|
|
|
description: route.description,
|
|
|
|
|
|
layout: route.layout,
|
|
|
|
|
|
name: route.name,
|
|
|
|
|
|
matched: [route]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.beforeEnter) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
let shouldContinue = true
|
|
|
|
|
|
const result = await this.beforeEnter(to, this.#current, (next) => {
|
|
|
|
|
|
if (next) {
|
|
|
|
|
|
shouldContinue = false
|
|
|
|
|
|
this.push(next)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
if (result === false || !shouldContinue) return
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error in beforeEnter guard:', error)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cacheKey = matchedRoute.fullPath
|
|
|
|
|
|
let page = this.#pageCache.get(cacheKey)
|
|
|
|
|
|
|
|
|
|
|
|
this.#setRouterPath(matchedRoute)
|
|
|
|
|
|
if (page) {
|
|
|
|
|
|
page.activate()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
page = new Page(this.#vhtml, this.#node, matchedRoute)
|
|
|
|
|
|
await page.mount(this.#env, this.#originContent, to.layout)
|
|
|
|
|
|
this.#pageCache.set(cacheKey, page)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async push(to) {
|
|
|
|
|
|
const matchedRoute = this.matchTo(to)
|
|
|
|
|
|
|
|
|
|
|
|
if (!matchedRoute) {
|
|
|
|
|
|
const target = typeof to === 'string' ? to : (to.path || `name: ${to.name}`)
|
|
|
|
|
|
console.warn(`No route matched for ${target}`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.#navigateTo(matchedRoute)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
replace(to) {
|
|
|
|
|
|
this.push(to)
|
|
|
|
|
|
if (this.#history.length > 1) {
|
|
|
|
|
|
this.#history.splice(-2, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
go(n) { history.go(n) }
|
|
|
|
|
|
back() { history.back() }
|
|
|
|
|
|
forward() { history.forward() }
|
|
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
|
if (this.#loaded) return
|
|
|
|
|
|
this.#loaded = true
|
|
|
|
|
|
|
|
|
|
|
|
document.body.addEventListener('click', (event) => {
|
|
|
|
|
|
const linkElement = event.target.closest('a')
|
|
|
|
|
|
if (!linkElement) return
|
|
|
|
|
|
|
|
|
|
|
|
const href = linkElement.getAttribute('href')
|
|
|
|
|
|
if (!href || href.startsWith('http') || href.startsWith('#')) return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
const reload = linkElement.hasAttribute('reload')
|
|
|
|
|
|
if (reload) {
|
|
|
|
|
|
window.location.href = href
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.push(href)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, true)
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('popstate', () => {
|
|
|
|
|
|
this.push(window.location.href)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ParseVrouter($vhtml, $node, env) {
|
|
|
|
|
|
this.#node = $node
|
|
|
|
|
|
this.#env = env
|
|
|
|
|
|
this.#scoped = env.scoped || ''
|
|
|
|
|
|
this.#originContent = Array.from($node.childNodes)
|
|
|
|
|
|
this.#vhtml = $vhtml
|
|
|
|
|
|
this.push(window.location.href)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 优化后的路由匹配器
|
|
|
|
|
|
class RouteMatcher {
|
|
|
|
|
|
constructor(path, name) {
|
|
|
|
|
|
this.originalPath = path
|
|
|
|
|
|
this.name = name
|
|
|
|
|
|
this.keys = []
|
|
|
|
|
|
this.regexp = this.pathToRegexp(path)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pathToRegexp(path) {
|
|
|
|
|
|
const paramPattern = /:([^(/]+)/g
|
|
|
|
|
|
let regexpStr = path.replace(paramPattern, (match, key) => {
|
|
|
|
|
|
this.keys.push(key)
|
|
|
|
|
|
return `(?<${key}>[^/]+)`
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 处理 *path 形式的通配符
|
|
|
|
|
|
regexpStr = regexpStr.replace(/\*(\w+)/g, (match, key) => {
|
|
|
|
|
|
this.keys.push(key)
|
|
|
|
|
|
return `(?<${key}>.*)`
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有未处理的*号,将其替换为允许匹配任意数量字符的正则表达式
|
|
|
|
|
|
regexpStr = regexpStr.replace(/\*/g, '.*')
|
|
|
|
|
|
return new RegExp(`^${regexpStr}$`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 优化的匹配方法,支持多种参数类型
|
|
|
|
|
|
match(target) {
|
|
|
|
|
|
let path
|
|
|
|
|
|
|
|
|
|
|
|
// 处理不同类型的输入
|
|
|
|
|
|
if (typeof target === 'string') {
|
|
|
|
|
|
path = target
|
|
|
|
|
|
} else if (target && typeof target === 'object') {
|
|
|
|
|
|
if (target.path) {
|
|
|
|
|
|
path = target.path
|
|
|
|
|
|
} else if (target.name && target.name === this.name) {
|
|
|
|
|
|
// 如果按名称匹配且名称相符,返回基本匹配
|
|
|
|
|
|
return {
|
|
|
|
|
|
path: this.originalPath,
|
|
|
|
|
|
params: target.params || {},
|
|
|
|
|
|
matched: this.originalPath
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const match = this.regexp.exec(path)
|
|
|
|
|
|
if (!match) return null
|
|
|
|
|
|
|
|
|
|
|
|
const params = {}
|
|
|
|
|
|
this.keys.forEach(key => {
|
|
|
|
|
|
if (match.groups?.[key]) {
|
|
|
|
|
|
params[key] = match.groups[key]
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
path: this.originalPath,
|
|
|
|
|
|
params,
|
|
|
|
|
|
matched: match[0]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const layoutCache = new Map()
|
|
|
|
|
|
|
|
|
|
|
|
class Page {
|
|
|
|
|
|
constructor(vhtml, node, matchedRoute) {
|
|
|
|
|
|
this.vhtml = vhtml
|
|
|
|
|
|
this.node = node
|
|
|
|
|
|
this.layoutDom = undefined
|
|
|
|
|
|
this.matchedRoute = matchedRoute
|
|
|
|
|
|
this.htmlPath = this.resolveHtmlPath(matchedRoute)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resolveHtmlPath(matchedRoute) {
|
|
|
|
|
|
let path = matchedRoute.route.component || matchedRoute.route.path
|
|
|
|
|
|
if (typeof path === 'function') {
|
|
|
|
|
|
path = path(matchedRoute.path)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(matchedRoute.params).forEach(([key, value]) => {
|
|
|
|
|
|
path = path.replace(`:${key}`, value)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (!path.startsWith('/')) path = '/' + path
|
|
|
|
|
|
if (path.endsWith('/')) path = path.slice(0, -1)
|
|
|
|
|
|
if (!path.endsWith('.html')) path = path + '.html'
|
|
|
|
|
|
|
|
|
|
|
|
return path
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async mount(env, originContent, layout) {
|
|
|
|
|
|
|
|
|
|
|
|
const parser = await vget.FetchUI(this.htmlPath, env)
|
|
|
|
|
|
if (parser.err) {
|
|
|
|
|
|
console.warn(parser.err)
|
|
|
|
|
|
let dom = document.createElement('div')
|
|
|
|
|
|
Object.assign(dom.style, { width: '100%', height: '100%' })
|
|
|
|
|
|
dom.append(...originContent)
|
|
|
|
|
|
this.node.innerHTML = ''
|
|
|
|
|
|
this.node.append(dom)
|
|
|
|
|
|
this.vhtml.parseRef(this.htmlPath, dom, {}, env, null, true)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.title = parser.title || ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const slots = {}
|
|
|
|
|
|
const dom = document.createElement("div")
|
|
|
|
|
|
dom.setAttribute('vsrc', this.htmlPath)
|
|
|
|
|
|
slots[''] = [dom]
|
|
|
|
|
|
this.slots = slots
|
|
|
|
|
|
|
|
|
|
|
|
if (!layout) {
|
|
|
|
|
|
this.node.innerHTML = ''
|
|
|
|
|
|
this.node.append(dom)
|
|
|
|
|
|
this.vhtml.parseRef(this.htmlPath, dom, {}, env, null)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let layoutDom = layoutCache.get(layout)
|
|
|
|
|
|
if (!layoutDom) {
|
|
|
|
|
|
let layoutUrl = layout
|
|
|
|
|
|
if (!layoutUrl.startsWith('/')) {
|
|
|
|
|
|
layoutUrl = '/' + layout
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!layoutUrl.endsWith('.html')) {
|
|
|
|
|
|
layoutUrl += '.html'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!layoutUrl.startsWith('/layout')) {
|
|
|
|
|
|
layoutUrl = '/layout' + layoutUrl
|
|
|
|
|
|
}
|
|
|
|
|
|
const layoutParser = await vget.FetchUI(layoutUrl, env)
|
|
|
|
|
|
if (layoutParser.err) {
|
|
|
|
|
|
console.warn(`get layout ${layoutUrl} failed.`, layoutParser.err)
|
|
|
|
|
|
this.node.innerHTML = ''
|
|
|
|
|
|
this.node.append(dom)
|
|
|
|
|
|
this.vhtml.parseRef(this.htmlPath, dom, {}, env, null)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
layoutDom = layoutParser.body.cloneNode(true)
|
|
|
|
|
|
layoutCache.set(layout, layoutDom)
|
|
|
|
|
|
dom.$refData = vproxy.Wrap({})
|
|
|
|
|
|
layoutDom.$refSlots = vproxy.Wrap({ ...slots })
|
|
|
|
|
|
this.node.innerHTML = ''
|
|
|
|
|
|
this.node.append(layoutDom)
|
|
|
|
|
|
this.layoutDom = layoutDom
|
|
|
|
|
|
this.vhtml.parseRef('/layout/' + layout, layoutDom, {}, env, null, true)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
|
|
this.layoutDom = layoutDom
|
|
|
|
|
|
this.activate()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
activate() {
|
|
|
|
|
|
if (this.title) document.title = this.title
|
|
|
|
|
|
const layoutDom = this.layoutDom
|
|
|
|
|
|
|
|
|
|
|
|
if (layoutDom) {
|
|
|
|
|
|
layoutDom.querySelectorAll("vslot").forEach(e => {
|
|
|
|
|
|
if (e.closest('[vref]') === layoutDom && this.slots[e.getAttribute('name') || '']) {
|
|
|
|
|
|
e.innerHTML = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
Object.keys(layoutDom.$refSlots).forEach(key => {
|
|
|
|
|
|
delete layoutDom.$refSlots[key]
|
|
|
|
|
|
})
|
|
|
|
|
|
Object.assign(layoutDom.$refSlots, this.slots)
|
|
|
|
|
|
if (!layoutDom.isConnected) {
|
|
|
|
|
|
this.node.innerHTML = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
this.node.append(layoutDom)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.node.innerHTML = ''
|
|
|
|
|
|
const dom = this.slots['']
|
|
|
|
|
|
if (dom instanceof Array) {
|
|
|
|
|
|
this.node.append(...dom)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.node.append(dom)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const $router = new VRouter()
|
|
|
|
|
|
|
|
|
|
|
|
const DefaultRoutes = [
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/',
|
|
|
|
|
|
component: '/page/index.html',
|
|
|
|
|
|
name: 'home',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/404',
|
|
|
|
|
|
component: '/page/404.html',
|
|
|
|
|
|
name: '404'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '*',
|
|
|
|
|
|
component: (path) => {
|
|
|
|
|
|
if (path.endsWith('.html')) return path
|
|
|
|
|
|
return '/page' + path + '.html'
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
export default { $router, DefaultRoutes }
|