Vue Router&SSR
VueRouter
现在都是 前后端分离的框架,由此有了SPA工程,从而有了路由,让前端去管理页面跳转
起初都是,后端路由=> url不同,都会向服务器发送请求,服务器返回一个html
现在:访问一个url,也都会向服务器拉一个页面回来,但是由于spa工程,服务器返回的都会在nginx层进行一个转发,都转发到根目录了
后端路由
优点:减轻前端压力,html由服务器拼接
缺点:用户体验差 php,jsp
前端路由
模式
- hash模式
localhost:8080/#home
怎么监听呢?
比如从#about到#home的过程
路由的原理:首先监听到它的变化,然后进行组件切换
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes: [
//...
],
})
- history模式
localhost:8080/home
localhost:8080/about
通过history模式将应用发布上线后,将dist部署到服务器进行路由的切换发生404的问题
原因:每个路径都会向服务器发送请求,服务器收到请求后发现没有路径,则会返回404,这就需要nginx来处理
location /{
root /user/local/nginx/html/dist;
index index.html index.htm
try_files $uri $uri/ /index.html
}
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
//...
],
})
路由的简单使用
// 1. 定义路由组件.
// 也可以从其他文件导入
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }
// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]
// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置,但我们在这里
// 暂时保持简单
const router = VueRouter.createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history: VueRouter.createWebHashHistory(),
routes, // `routes: routes` 的缩写
})
// 5. 创建并挂载根实例
const app = Vue.createApp({})
//确保 _use_ 路由实例使
//整个应用支持路由。
app.use(router) //use原理 this.$route this.$router vuex pinia插件都是作为vue的插件
app.mount('#app')
组件内使用:
使用hooks的方式使用路由
原因:
现在在组合式api的setup中是不能使用this的方式,那么要是想使用router就得使用hooks的方式
//使用hooks的方式使用路由
import {useRouter} from 'vue-router'
const router=useRouter()
//路由的跳转
router.back()
动态参数路由
const User = {
template: '<div>User</div>',
}
// 这些都会传递给 `createRouter`
const routes = [
// 动态字段以冒号开始
{ path: '/users/:id', component: User },
]
// /users/johnny 和 /users/jolyne 这样的 URL 都会映射到同一个路由。
// 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。
// 因此,我们可以通过更新 User 的模板来呈现当前的用户 ID
const User = {
template: '<div>User {{ $route.params.id }}</div>',
}
有这么一种情况,针对不同id的用户,路由的前面路径是同样的,但是后面会针对不同人的id从而路径是不同的,这里会有一个问题,
当路径从 /users/1变到 /users/2的时候,组件还会重新执行之前的生命周期吗?
答案是 不会的
那么从1变成2的话,我们怎么拿到最新的id呢?
方式一:
在created中使用watch监听路由的变化
const User = {
template: '<div>User {{ $route.params.id }}</div>',
created(){
this.$watch(
()=>this.$route.params,
()=>{
console.log()
}
)
}
}
方式二:
通过导航守卫的方式
async beforeRouteUpdate(to,from){
//to from
}
编程式导航
const username = 'eduardo'
// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`) // -> /user/eduardo
// 同样
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` 不能与 `path` 一起使用
router.push({ path: '/user', params: { username } }) // -> /user
手写一个vueRouter
- 如何监测 url 发生变化
- 如何改变 url 不引起页面刷新
- 要支持hash和history这两种模式
- router-link和router-view底层的实现
hash:
当hash值变化的时候,会触发hashchange发生变化,除过hashchange,popstate也能监听到hash值发生变化
history:
通过popstate的变化,监听到值的变化
但是通过push和replace是不会出发popstate的事件的
- src
- components
- Home.vue
- About.vue
- router
- index.js
- core.js
- App.vue
- main.js
- components
router/core.js
let Vue =null
class HistoryRoute{
constructor() {
this.current=null
}
}
class VueRouter{
constructor(options) {
this.mode = options.mode||'hash'; //默认是hash模式
this.routes = options.routes || []
this.routesMap = this.createMap(options.routes)
// map
// {path:Component,path2:Component2}
this.history=new HistoryRoute()
this.init()
}
init() {
if (this.mode === "hash") {
//url:http://localhost:8080/#/home location.hash="#/home"
location.hash ? '' : (location.hash = "/")
window.addEventListener('load', () => {
this.history.current = location.hash.slice(1) //#号不要了
})
window.addEventListener('hashchange', () => {
this.history.current = location.hash.slice(1)
})
// 这两个都是可以的
// window.addEventListener('popstate', () => {
// this.history.current=location.hash.slice(1)
// })
} else {
// history
// popstate load location.pathname
location.pathname ? "" : (location.pathname = "/")
window.addEventListener('load', () => {
this.history.current=location.pathname
})
window.addEventListener('popstate', () => {
this.history.current=location.pathname
})
}
}
createMap(routes) {
return routes.reduce((acc, cur) => {
acc[cur.path] = cur.component
return acc
}, {})
}
}
// Vue提供的install方法将插件接入到vue生态中
VueRouter.install = function (v) {
// 这里需要处理 router-view router-link this.$router访问路由实例 this.$route访问访问路由对象
Vue = v
Vue.mixin({
beforeCreate() {
console.log("this", this);
if (this.$options && this.$options.router) {
// 根节点
// $router已经挂载到根实例上
this._root = this
this._router = this.$options.router
Vue.util.defineReactive(this, 'xxx', this._router.history);
} else {
// 非根组件
this._root=this.$parent&&this.$parent._root
}
Object.defineProperty(this, '$router', {
get() {
return this._root._router
}
})
Object.defineProperty(this, '$route', {
get() {
return this._root._router.history.current
}
})
}
})
Vue.component('router-link', {
props: {
to:String
},
render(h) {
// 每个组件都有一个_self来获取当前组件实例
let mode = this._self._root._router.mode
let to = mode === 'hash' ? `#${this.to}` : this.to
return h('a',{attrs:{href:to}},this.$slots.default)
}
})
Vue.component('router-view', {
render(h) {
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routesMap
return h(routeMap[current])
}
})
}
export default VueRouter
其他代码的补充:
router/index.js
// import VueRouter from 'vue-router'
import VueRouter from './core.js'
import Vue from 'vue'
Vue.use(VueRouter)
const routes = [
{
name:"home",
path: "/home",
component:()=>import("../components/Home.vue")
},
{
name:"about",
path:"/about",
component:()=>import('../components/About.vue')
}
]
export default new VueRouter({
mode: "hash",
routes
})
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router, //这里会对router进行初始化,那么this.$options就会有router
render: h => h(App),
}).$mount('#app')
App.vue
<template>
<div id="app">
<router-link to="/home">Home</router-link> |
<router-link to="/about">About</router-link>
<hr>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
<style>
</style>
Home.vue
<template>
<div>
<h1>home</h1>
</div>
</template>
<script>
export default {
data() {
return {
};
},
methods: {
},
}
</script>
<style scoped>
</style>
About.vue
<template>
<div>
<h1>about</h1>
</div>
</template>
<script>
export default {
data() {
return {
};
},
methods: {
},
}
</script>
<style scoped>
</style>
vue-router 导航守卫
导航守卫执行顺序:
全局导航守卫 beforeEach => 路由独享守卫 beforeEnter => 组件异步解析 => 组件内守卫 beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave => 全局解析守卫 beforeResolve => 全局后置守卫 afterEach
vue-router原理
vue-router源码
这里看的是 vue3的router原理,
vue3的router github
是pnpm+node_modules这样一个解决方案
总的package.json&packages
为了便于对packages文件夹下的包进行一个管理,因此在packages中包含了很多命令
- release 发布
- size 对当前体积大小进行检查
- build 打包 :pnpm run -r build 这里 -r 的意思是 递归,递归去执行下面所有子包的build指令
多包工程的好处是:在外层的脚本中执行里面所有的脚本,这样的话,就可以方便对多个包进行管理,将相似的包放到一个工程当中,对相似的包进行统一的一个管理
scripts
- check-size.mjs:对当前的大小进行一个check及压缩
- release.mjs:发布
- verifyCommit:git commit的规范校验
重点:packages/router包
package.json
- version:版本
- main:index.js 主入口为index.js
- unpkg:最终包cnd资源的路径,访问的是dist/vue-router-global.js资源的内容
vue3创建的项目安装vue-router后,在node_modules的vue-router中能根据路径找到需要的资源
- module:需要的是mjs类型的资源文件
- types:需要的是ts类型的文件
- exports:导出的文件内容
- files:最终打包之后的产物有哪些
vue3创建的项目安装vue-router后,在node_modules的vue-router中的资源内容有哪些都是files来限制的
- scripts:
- dev:jest的测试用例
- build:执行当前的整体的构建
基于rollup进行打包的,基本上很多基础库用的都是rollup进行打包的,他打包后的产物会更小
rollup配置:
最终的vue-router会打包成这么几种产物,根据当前不同的规范,引用不同的文件内容 - build:dts 执行当前所有ts文件的构建
- build:playground 执行playground文件的构建
- build:e2e:执行端到端的测试
- build:size 执行大小的check
- test:xxxx 测试的用例
packages/router/src/index.ts
- 几种模式分别在主入口进行导出,history,memory,hash模式
- 当前的公共方法,paseQuery
- 创建router实例的createRouter方法
- 全局组件RouterLink和RouterView
- 通过hooks的方式提供的useRouter和useRoute
createRouterMatcher中进行相关的路由的匹配,规则的定义这些事情
先从主入口找到 createRouter函数
packages/router/src/router.ts
createRouter方法
1.初始化createRouter,对options进行处理,用matcher匹配路由,用routerHistory管理历史记录对象
2.初始化滚动行为
3.规范化和编码解码路由参数
4.添加路由,添加先找父路由,找不到则警告,使用matcher
5.删除路由,找到后删除,使用matcher
6.获取所有路由 使用matcher
7.检查路由是否存在 使用matcher
8.解析路由,返回标准化路由对象
9.locationAsObject:将router-link传入的to的地址转换成标准的路由对象
10.checkCanceledNavigation:检查当前导航是否已被取消
11.handleRedirectRecord 路由重定向,to的路由如果有redirect属性,则执行重定向操作
12.push和replace都调用了pushWithRedirect:执行带重定向的路由跳转,如果有重定向,先执行重定向,没有,则执行导航操作
13.checkCanceledNavigationAndReject:检查当前路由是否被取消
14.runWithContext:执行一个函数,并确保在Vue应用的上下文中运行,没找到应用上下文,则直接执行函数
15.navigate:确保执行路由跳转之前,所有相关守卫按照一定顺序依次执行,从而控制路由的进入,离开和更新
16.triggerAfterEach:调用所有导航后置守卫,在路由变更后执行
17.finalizeNavigation:处理完成导航过程的函数,是否是首次导航,判断是否是push或replace来进行url更新,处理页面滚动行为
18.setupListeners:监听路由历史变化的,监听浏览器的导航事件,发生新的导航触发回调,回调会处理当前的导航位置,检查是否需要重定向,保存滚动位置,执行新的导航
19.triggerError:触发错误监听器,将错误抛出,处理在路由导航中出现的错误。
router的install方法:
1.组件注册,RouterLink和RouterView组件注册
2.使用app.config.globalProperties将router和$route注入到Vue实例中,
3.如果在浏览器中且未启用过路由,执行首次跳转
4.响应式route
5.卸载时清理
- 组件的全局注入
RouterView:
通过props传入name和route,vue3使用setup传入值
- 组件通过router,route的访问
接下来看 this.$router 和 this.$route 是怎么实现的:
- 初始化的逻辑
- 通过provide进行全局存储的能力,可以使得再setup中获取到router,route
再回到之前的使用上面:
//使用hooks的方式使用路由
import {useRouter,useRoute} from 'vue-router'
const router=useRouter()
const route=useRoute()
//路由的跳转
router.back()
- 卸载
不同模式之前的区别
hash - router/packages/router/src/history/hash.ts
hash和history的区别点并不多,开始是对base进行处理,后面都直接调用用webhistory,对事件的监听,事件方法的处理进行了统一,只是对路径做了区别
hash和history内部都是通过popstate来监听的
history - router/packages/router/src/history/html5.ts
基本结构
- 创建基础location
let createBaseLocation = () => location.protocol + ‘//’ + location.host
- 创建当前location
function createCurrentLocation
- 使用history的监听
function useHistoryListeners
- state的处理
function buildState
- 导出webHistory
export function createWebHistorynormalizeBase
webHistory:
export function createWebHistory(base?: string): RouterHistory {
// base的规范化
base = normalizeBase(base)
// 1.创建 vue router 的history对象
const historyNavigation = useHistoryStateNavigation(base)
// 2.创建 vue router 监听器
const historyListeners = useHistoryListeners(
base,
historyNavigation.state,
historyNavigation.location,
historyNavigation.replace
)
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta)
}
// 组装routerHistory对象
const routerHistory: RouterHistory = assign(
{
// it's overridden right after
location: '',
base,
go,
createHref: createHref.bind(null, base),
},
historyNavigation,
historyListeners
)
// 3. 添加location的劫持
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,
})
// 4. 添加state的劫持
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,
})
// 返回整个router history对象
return routerHistory
}
import {
RouterHistory,
NavigationCallback,
NavigationType,
NavigationDirection,
HistoryState,
ValueContainer,
normalizeBase,
createHref,
HistoryLocation,
} from './common'
import {
computeScrollPosition,
_ScrollPositionNormalized,
} from '../scrollBehavior'
import { warn } from '../warning'
import { stripBase } from '../location'
import { assign } from '../utils'
type PopStateListener = (this: Window, ev: PopStateEvent) => any
// 创建基础location
let createBaseLocation = () => location.protocol + '//' + location.host
interface StateEntry extends HistoryState {
back: HistoryLocation | null
current: HistoryLocation
forward: HistoryLocation | null
position: number
replaced: boolean
scroll: _ScrollPositionNormalized | null | false
}
/**
* Creates a normalized history location from a window.location object
* @param base - The base path
* @param location - The window.location object
*/
// 创建当前location
function createCurrentLocation(
base: string,
location: Location
): HistoryLocation {
const { pathname, search, hash } = location
// allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
const hashPos = base.indexOf('#')
if (hashPos > -1) {
let slicePos = hash.includes(base.slice(hashPos))
? base.slice(hashPos).length
: 1
let pathFromHash = hash.slice(slicePos)
// prepend the starting slash to hash so the url starts with /#
if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash
return stripBase(pathFromHash, '')
}
const path = stripBase(pathname, base)
return path + search + hash
}
// 使用history的监听
function useHistoryListeners(
base: string,
historyState: ValueContainer<StateEntry>,
currentLocation: ValueContainer<HistoryLocation>,
replace: RouterHistory['replace']
) {
let listeners: NavigationCallback[] = []
let teardowns: Array<() => void> = []
// TODO: should it be a stack? a Dict. Check if the popstate listener
// can trigger twice
let pauseState: HistoryLocation | null = null
// 接收最新的state数据
const popStateHandler: PopStateListener = ({
state,
}: {
state: StateEntry | null
}) => {
const to = createCurrentLocation(base, location) //获取到新的location的信息
const from: HistoryLocation = currentLocation.value //之前的
const fromState: StateEntry = historyState.value
let delta = 0
if (state) {
// 目标路由state不为空时,更新currentLocation和historyState缓存
currentLocation.value = to
historyState.value = state
// ignore the popstate and reset the pauseState
// 暂停监控时,中断跳转并重置pauseState
if (pauseState && pauseState === from) {
pauseState = null
return
}
delta = fromState ? state.position - fromState.position : 0
} else {
replace(to)
}
// Here we could also revert the navigation by calling history.go(-delta)
// this listener will have to be adapted to not trigger again and to wait for the url
// to be updated before triggering the listeners. Some kind of validation function would also
// need to be passed to the listeners so the navigation can be accepted
// call all listeners
// 发布跳转事件,将location,跳转类型,跳转距离等信息返回给所有注册的订阅者,
// 通知监听发布路由变化的这些地方,谁进行了事件的监听,则在这里做派发
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
})
})
}
// 暂停当前的监听
function pauseListeners() {
pauseState = currentLocation.value
}
// 监听
function listen(callback: NavigationCallback) {
// set up the listener and prepare teardown callbacks
listeners.push(callback)
const teardown = () => {
const index = listeners.indexOf(callback)
if (index > -1) listeners.splice(index, 1)
}
teardowns.push(teardown)
return teardown
}
function beforeUnloadListener() {
const { history } = window
if (!history.state) return
history.replaceState(
assign({}, history.state, { scroll: computeScrollPosition() }),
''
)
}
// 销毁
function destroy() {
for (const teardown of teardowns) teardown()
teardowns = []
window.removeEventListener('popstate', popStateHandler)
window.removeEventListener('beforeunload', beforeUnloadListener)
}
// 监听了popstate
// set up the listeners and prepare teardown callbacks
window.addEventListener('popstate', popStateHandler) //拿到最新的数据更新location的信息
// TODO: could we use 'pagehide' or 'visibilitychange' instead?
// https://developer.chrome.com/blog/page-lifecycle-api/
// beforeunload 浏览器刷新,或者即将离开当前站点会触发
window.addEventListener('beforeunload', beforeUnloadListener, {
passive: true,
})
return {
pauseListeners,
listen,
destroy,
}
}
/**
* Creates a state object
*/
// state的处理
function buildState(
back: HistoryLocation | null,
current: HistoryLocation,
forward: HistoryLocation | null,
replaced: boolean = false,
computeScroll: boolean = false
): StateEntry {
return {
back,
current,
forward,
replaced,
position: window.history.length,
scroll: computeScroll ? computeScrollPosition() : null,
}
}
function useHistoryStateNavigation(base: string) {
// 使用浏览器的history和location
const { history, location } = window
// 对其包装
// private variables
const currentLocation: ValueContainer<HistoryLocation> = {
value: createCurrentLocation(base, location), //通过createCurrentLocation方法获取当前location的信息
}
const historyState: ValueContainer<StateEntry> = { value: history.state }
// build current history entry as this is a fresh navigation
if (!historyState.value) {
changeLocation(
currentLocation.value,
{
back: null,
current: currentLocation.value,
forward: null,
// the length is off by one, we need to decrease it
position: history.length - 1,
replaced: true,
// don't add a scroll as the user may have an anchor, and we want
// scrollBehavior to be triggered without a saved position
scroll: null,
},
true
)
}
function changeLocation(
to: HistoryLocation,
state: StateEntry,
replace: boolean
): void {
//接收到达的location和当前的数据
/**
* if a base tag is provided, and we are on a normal domain, we have to
* respect the provided `base` attribute because pushState() will use it and
* potentially erase anything before the `#` like at
* https://github.com/vuejs/router/issues/685 where a base of
* `/folder/#` but a base of `/` would erase the `/folder/` section. If
* there is no host, the `<base>` tag makes no sense and if there isn't a
* base tag we can just use everything after the `#`.
*/
const hashIndex = base.indexOf('#')
// 进行url的拼接
const url =
hashIndex > -1
? (location.host && document.querySelector('base')
? base
: base.slice(hashIndex)) + to
: createBaseLocation() + base + to
try {
// BROWSER QUIRK
// NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
// 去调用浏览器本身的history能力,将url和state传入history
history[replace ? 'replaceState' : 'pushState'](state, '', url)
historyState.value = state
} catch (err) {
if (__DEV__) {
warn('Error with push/replace State', err)
} else {
console.error(err)
}
// Force the navigation, this also resets the call count
location[replace ? 'replace' : 'assign'](url)
}
}
// 对push和replace操作都不会触发popState
function replace(to: HistoryLocation, data?: HistoryState) {
// 先进行数据的组装
const state: StateEntry = assign(
{},
history.state,
buildState(
historyState.value.back,
// keep back and forward entries but override current position
to,
historyState.value.forward,
true
),
data,
{ position: historyState.value.position }
)
// 拿到最新的数据给到changeLocation
changeLocation(to, state, true)
// 更新下当前location变量
currentLocation.value = to
}
// push('/home') {path:'home',name:'home'}
function push(to: HistoryLocation, data?: HistoryState) {
// Add to current entry the information of where we are going
// as well as saving the current position
// 对要处理的信息进行合并
const currentState = assign(
{},
// use current history state to gracefully handle a wrong call to
// history.replaceState
// https://github.com/vuejs/router/issues/366
historyState.value,
history.state as Partial<StateEntry> | null,
{
forward: to,
scroll: computeScrollPosition(),//记录当前的位置信息,便于返回的时候能重回到之前的位置
}
)
if (__DEV__ && !history.state) {
warn(
`history.state seems to have been manually replaced without preserving the necessary values. Make sure to preserve existing history state if you are manually calling history.replaceState:\n\n` +
`history.replaceState(history.state, '', url)\n\n` +
`You can find more information at https://router.vuejs.org/guide/migration/#Usage-of-history-state`
)
}
// push和replace都会调用changeLocation来改变当前的位置信息,修改后的信息currentState.current和currentState传入
changeLocation(currentState.current, currentState, true)
const state: StateEntry = assign(
{},
buildState(currentLocation.value, to, null),
{ position: currentState.position + 1 },
data
)
changeLocation(to, state, false)
currentLocation.value = to
}
// 最终返回一个location,state
return {
location: currentLocation,
state: historyState,
push,
replace,
}
}
/**
* Creates an HTML5 history. Most common history for single page applications.
*
* @param base -
*/
export function createWebHistory(base?: string): RouterHistory {
// base的规范化
base = normalizeBase(base)
// 1.创建 vue router 的history对象
const historyNavigation = useHistoryStateNavigation(base)
// 2.创建 vue router 监听器
const historyListeners = useHistoryListeners(
base,
historyNavigation.state,
historyNavigation.location,
historyNavigation.replace
)
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta)
}
// 组装routerHistory对象
const routerHistory: RouterHistory = assign(
{
// it's overridden right after
location: '',
base,
go,
createHref: createHref.bind(null, base),
},
historyNavigation,
historyListeners
)
// 3. 添加location的劫持
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,
})
// 4. 添加state的劫持
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,
})
// 返回整个router history对象
return routerHistory
}
memory- router/packages/router/src/history/memory.ts
这个历史记录的主要目的是处理 SSR,由于SSR是不能访问 location,document
因此,history是在内存当中模拟当前location的行为,使用队列的形式去模拟浏览器针对当前路由的行为
export function createMemoryHistory(base: string = ''): RouterHistory {
let listeners: NavigationCallback[] = []
// queue来模拟栈
let queue: HistoryLocation[] = [START]
let position: number = 0
base = normalizeBase(base)
// 对行为入栈,出栈等等进行处理
// setLocation 相当于push,更改当前location数据,set会加1,进行入栈的操作
function setLocation(location: HistoryLocation) {
position++
if (position !== queue.length) {
// we are in the middle, we remove everything from here in the queue
queue.splice(position)
}
queue.push(location)
}
function triggerListeners(
to: HistoryLocation,
from: HistoryLocation,
{ direction, delta }: Pick<NavigationInformation, 'direction' | 'delta'>
): void {
const info: NavigationInformation = {
direction,
delta,
type: NavigationType.pop,
}
for (const callback of listeners) {
callback(to, from, info)
}
}
const routerHistory: RouterHistory = {
// rewritten by Object.defineProperty
location: START,
// TODO: should be kept in queue
state: {},
base,
createHref: createHref.bind(null, base),
// 针对replace对queue位置进行出栈处理
replace(to) {
// remove current entry and decrement position
queue.splice(position--, 1)
setLocation(to)
},
push(to, data?: HistoryState) {
setLocation(to)
},
listen(callback) {
listeners.push(callback)
return () => {
const index = listeners.indexOf(callback)
if (index > -1) listeners.splice(index, 1)
}
},
destroy() {
listeners = []
queue = [START]
position = 0
},
go(delta, shouldTrigger = true) {
const from = this.location
const direction: NavigationDirection =
// we are considering delta === 0 going forward, but in abstract mode
// using 0 for the delta doesn't make sense like it does in html5 where
// it reloads the page
delta < 0 ? NavigationDirection.back : NavigationDirection.forward
position = Math.max(0, Math.min(position + delta, queue.length - 1))
if (shouldTrigger) {
triggerListeners(this.location, from, {
direction,
delta,
})
}
},
}
// 劫持了location,最终返回queue的position位置
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => queue[position],
})
if (__TEST__) {
// @ts-expect-error: only for tests
routerHistory.changeURL = function (url: string) {
const from = this.location
queue.splice(position++ + 1, queue.length, url)
triggerListeners(this.location, from, {
direction: NavigationDirection.unknown,
delta: 0,
})
}
}
return routerHistory
}
SSR
SSR server side render 服务端渲染
CSR client side render 客户端渲染
浏览器渲染的过程
web 1.0时代
服务端渲染 php jsp 这时候还没有前端开发
ajax的出现,然后是SPA,后面衍生 前后端分离,这时候就是客户端渲染CSR的阶段
交互的整体逻辑:后端返回一个功能html,前端通过js加载主机js请求完数据后再渲染页面内容,没有node层
SEO工程,涉及到爬虫,低级爬虫爬的只是html页面结构
SPA工程,div空的节点,没内容可以爬虫
因此,许多公司或团队要求使用SSR的原因是,
1.希望自己的官网能够被爬虫爬到,这样别人在百度搜索当中,就能提高优先级。
在一些官网项目中会用到SSR
2.解决首屏渲染的问题,SSR返回整个html,因此首屏渲染是比较快
让首屏加载的资源越少,节点更少,组件异步,则首屏渲染的更快,加载的更快
但是,中小厂 不具备这个能力,因为要搭建SSR的话,需要node层,这样要考虑比较多的事情。
使用SSR需要考虑的问题:
- 代码复杂度增加
- 多了一层node环境,访问不到location,document,很多基础包都需要兼容,比如自己的包,二方包,需要针对不兼容的地方做兼容,对第三方包就很难兼容了
- 需要更多的服务器,需要更多的负载均衡,本来只需要将静态资源给客户端,但是现在需要返回一个完整的html给客户端,服务器增加了渲染html的需求,node层需要提前去获取数据,使得更高的io和更高的cpu的占用,在运维层面需要高要求
- 工程化,需要多个端的构建,SSR,client
如果想针对Vue体系使用SSR,那么建议直接使用 Nuxt 方案,或者Vite SSR
大厂一般使用的是React的方案,并且都是自研的,小厂一般没有SSR的需求
手写一个小型SSR
package.json
{
"name": "node-starter",
"version": "0.0.0",
"type": "module",
"dependencies": {
"express": "^4.17.2",
"vue": "^3.2.26"
}
}
server.js
import express from 'express';
import { renderToString } from 'vue/server-renderer';
import { createApp } from './app.js';
const server = express();
server.get('/', (req, res) => {
const app = createApp();
renderToString(app).then(html => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR Example</title>
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<script type="module" src="/client.js"></script>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`);
});
});
server.use(express.static('.'));
server.listen(3000, () => {
console.log('ready');
});
app.js
import { createSSRApp } from 'vue';
export function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`,
});
}
client.js
import { createApp } from './app.js';
createApp().mount('#app');
pnpm i
node server.js