Vue Router
- Vue Router 是 Vue.js 官方提供的路由管理器,它可以实现前端路由,使得在单页应用中不同的页面可以通过路由进行切换,而无需刷新整个页面
- 安装Vue Router:
npm install vue-router
- 创建路由实例:
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/views/Home.vue'
Vue.use(Router)
const router = new Router({
routes: [
{
// path 配置路径,component 配置路径显示的组件
path: '/',
name: 'Home',
component: Home
}
]
})
- 在 Vue 实例中使用路由:
import Vue from 'vue'
import router from './router'
new Vue({
router,
render: h => h(App)
}).$mount('#app')
- 在模板中使用路由:
<template>
<div>
<!-- 链接:router-link 实现页面跳转,to 指定路径 -->
<router-link to="/">Home</router-link>
<!-- 容器:路由匹配到的组件会渲染在此处 -->
<router-view></router-view>
</div>
</template>
React Router
- React Router 是一个流行的路由库,用于管理 React 应用程序中的路由
- 安装 React Router
npm install react-router-dom
- 引入 React Router 组件
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom';
- 在 BrowserRouter 组件中定义路由规则
// BrowserRouter 容器包裹 Route 路由
<BrowserRouter>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
</Switch>
</BrowserRouter>
- 在组件中使用 Link 组件来生成链接
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
使用 WebComponent 实现路由
一个简单的路由需要有
- 容器组件
- 路由
- 业务组件
- 链接组件
使用 WebComponent 技术在浏览器的 URL 变化时,动态地加载对应的组件,并在页面中显示。这段代码分为以下几部分
- 重写
pushState方法,当浏览器的URL发生变化时,自定义一个c-popstate事件并派发 - 自定义链接组件
<c-link>,监听点击事件,调用history.pushState方法实现页面跳转 - 自定义路由组件
<c-route>,可以获取路由信息,用于匹配当前页面的路由 - 自定义组件容器
<c-component>,可以动态加载组件并解析,将组件的模板、脚本和样式添加到当前页面中 - 自定义路由容器
<c-router>,监听路由变化,根据当前路由渲染对应的组件内容 loadComponent函数 用于动态加载组件并解析缓存
组件示意
index.js
// 保存原始的 pushState 方法
const originalPushState = history.pushState
// 重写 pushState 方法
history.pushState = function (state, title, url) {
// 调用原始的 pushState 方法
originalPushState.apply(history, [state, title, url])
// 触发自定义事件
const event = new CustomEvent('c-popstate', {
detail: { state, title, url },
})
window.dispatchEvent(event)
}
// 自定义链接组件
class CustomLink extends HTMLElement {
connectedCallback() {
this.addEventListener('click', event => {
event.preventDefault()
const url = this.getAttribute('to')
// 更新浏览器历史记录
history.pushState('', '', url)
})
}
}
window.customElements.define('c-link', CustomLink)
// 自定义路由组件
class CustomRoute extends HTMLElement {
// 获取路由信息
getData() {
return {
default: this.hasAttribute('default'),
path: this.getAttribute('path'),
component: this.getAttribute('component'),
}
}
}
window.customElements.define('c-route', CustomRoute)
// 自定义组件容器
class CustomComponent extends HTMLElement {
async connectedCallback() {
// 获取组件的路径
const path = this.getAttribute('path')
// 加载组件
const componentInfo = await loadComponent(path)
const shadowRoot = this.attachShadow({ mode: 'closed' })
// 添加组件内容
this.#addElements(shadowRoot, componentInfo)
}
// 添加组件内容
#addElements(shadowRoot, componentInfo) {
// 添加模板内容
if (componentInfo.template) {
shadowRoot.appendChild(componentInfo.template.content.cloneNode(true))
}
// 添加脚本
if (componentInfo.script) {
// 防止全局污染,并获得根节点
const fun = new Function(`${componentInfo.script.textContent}`)
// 绑定脚本的 this 为当前的影子根节点
fun.bind(shadowRoot)()
}
// 添加样式
if (componentInfo.style) {
shadowRoot.appendChild(componentInfo.style)
}
}
}
window.customElements.define('c-component', CustomComponent)
// 自定义路由容器
class CustomRouter extends HTMLElement {
// 私有变量
#routes
connectedCallback() {
// 获取所有c-route子节点
const routeNodes = this.querySelectorAll('c-route')
// 获取子节点的路由信息
this.#routes = Array.from(routeNodes).map(node => node.getData())
// 查找默认的路由
const defaultRoute = this.#routes.find(r => r.default) || this.#routes[0]
// 渲染对应的路由
this.#onRenderRoute(defaultRoute)
// 监听路由变化
this.#listenHistory()
}
// 渲染路由对应的内容
#onRenderRoute(route) {
const el = document.createElement('c-component')
el.setAttribute('path', `/${route.component}.html`)
el.id = '_route_'
this.append(el)
}
// 卸载路由清理工作
#onUnloadRoute() {
const el = this.querySelector('#_route_')
if (el) {
el.remove()
}
}
// 监听路由变化
#listenHistory() {
// 导航的路由切换
window.addEventListener('popstate', ev => {
const url = location.pathname.endsWith('.html') ? '/' : location.pathname
const route = this.#getRoute(this.#routes, url)
this.#onUnloadRoute()
this.#onRenderRoute(route)
})
// pushState或者replaceState
window.addEventListener('c-popstate', ev => {
const detail = ev.detail
const route = this.#getRoute(this.#routes, detail.url)
this.#onUnloadRoute()
this.#onRenderRoute(route)
})
}
// 路由查找
#getRoute(routes, url) {
return routes.find(r => {
const path = r.path
const strPaths = path.split('/')
const strUrlPaths = url.split('/')
let match = true
for (let i = 0; i < strPaths.length; i++) {
if (strPaths[i].startsWith(':')) {
continue
}
match = strPaths[i] === strUrlPaths[i]
if (!match) {
break
}
}
return match
})
}
}
window.customElements.define('c-router', CustomRouter)
// 动态加载组件并解析
async function loadComponent(path, name) {
// 初始化缓存对象
loadComponent.caches = loadComponent.caches || {}
// 缓存存在,直接返回
if (loadComponent.caches[path]) {
return loadComponent.caches[path]
}
// 获取组件内容
const res = await fetch(path).then(res => res.text())
// 利用DOMParser解析HTML
const parser = new DOMParser()
const doc = parser.parseFromString(res, 'text/html')
// 解析模板、脚本和样式
const template = doc.querySelector('template')
const script = doc.querySelector('script')
const style = doc.querySelector('style')
// 缓存内容
loadComponent.caches[path] = {
template,
script,
style,
}
// 返回内容
return loadComponent.caches[path]
}
测试使用
index.html
<!DOCTYPE html>
<html>
<head>
<style>
c-link {
text-decoration: underline;
cursor: pointer;
margin-right: 10px;
}
</style>
</head>
<body>
<ul>
<c-link to="/">首页</c-link>
<c-link to="/about">关于</c-link>
</ul>
<c-router>
<c-route path="/" component="home" default></c-route>
<c-route path="/about" component="about"></c-route>
<c-route path="/detail/:id" component="detail"></c-route>
</c-router>
<script src="./index.js"></script>
</body>
</html>
about.html
<template>
About me!
</template>
home.html
<template>
<h3>商品清单</h3>
<div id="product-list">
<a data-id="10" class="product-item c-link">香蕉</a>
<a data-id="11" class="product-item c-link">苹果</a>
<a data-id="12" class="product-item c-link">香蕉</a>
</div>
</template>
<script>
// this 指向自身的 shadow-root
let container = this.querySelector('#product-list')
// 代码触发更新历史
container.addEventListener('click', function (e) {
if (e.target.classList.contains('product-item')) {
const id = +e.target.dataset.id
history.pushState(
{
id
},
'',
`/detail/${id}`
)
}
})
</script>
<style>
.product-item {
cursor: pointer;
color: blue;
}
</style>
detail.html
<template>
<h3>商品详情</h3>
<div>商品ID:<span id="product-id"></span></div>
</template>
<script>
this.querySelector('#product-id').textContent = history.state.id
</script>
<style>
#product-id {
color: green;
}
</style>