Vue进阶之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

  1. 如何监测 url 发生变化
  2. 如何改变 url 不引起页面刷新
  3. 要支持hash和history这两种模式
  4. 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

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

在这里插入图片描述

  1. version:版本
  2. main:index.js 主入口为index.js
  3. unpkg:最终包cnd资源的路径,访问的是dist/vue-router-global.js资源的内容
    vue3创建的项目安装vue-router后,在node_modules的vue-router中能根据路径找到需要的资源
    在这里插入图片描述
  4. module:需要的是mjs类型的资源文件
  5. types:需要的是ts类型的文件
  6. exports:导出的文件内容
  7. files:最终打包之后的产物有哪些
    在这里插入图片描述

vue3创建的项目安装vue-router后,在node_modules的vue-router中的资源内容有哪些都是files来限制的
在这里插入图片描述

  1. 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
  1. 几种模式分别在主入口进行导出,history,memory,hash模式
  2. 当前的公共方法,paseQuery
  3. 创建router实例的createRouter方法
  4. 全局组件RouterLink和RouterView
  5. 通过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.卸载时清理

  1. 组件的全局注入
    在这里插入图片描述
    RouterView:
    通过props传入name和route,vue3使用setup传入值
    在这里插入图片描述
  2. 组件通过router,route的访问
    接下来看 this.$router 和 this.$route 是怎么实现的:
    在这里插入图片描述
  3. 初始化的逻辑

在这里插入图片描述
在这里插入图片描述

  1. 通过provide进行全局存储的能力,可以使得再setup中获取到router,route 在这里插入图片描述
    在这里插入图片描述
    再回到之前的使用上面:
//使用hooks的方式使用路由
import {useRouter,useRoute} from 'vue-router'

const router=useRouter()
const route=useRoute()
//路由的跳转 
router.back()
  1. 卸载
    在这里插入图片描述
不同模式之前的区别
hash - router/packages/router/src/history/hash.ts

hash和history的区别点并不多,开始是对base进行处理,后面都直接调用用webhistory,对事件的监听,事件方法的处理进行了统一,只是对路径做了区别
hash和history内部都是通过popstate来监听的
在这里插入图片描述

history - router/packages/router/src/history/html5.ts

基本结构

  1. 创建基础location

let createBaseLocation = () => location.protocol + ‘//’ + location.host

  1. 创建当前location

function createCurrentLocation

  1. 使用history的监听

function useHistoryListeners

  1. state的处理

function buildState

  1. 导出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环境,访问不到locationdocument,很多基础包都需要兼容,比如自己的包,二方包,需要针对不兼容的地方做兼容,对第三方包就很难兼容了
  • 需要更多的服务器,需要更多的负载均衡,本来只需要将静态资源给客户端,但是现在需要返回一个完整的html给客户端,服务器增加了渲染html的需求,node层需要提前去获取数据,使得更高的io和更高的cpu的占用,在运维层面需要高要求
  • 工程化,需要多个端的构建,SSR,client

Vue官网对SSR的介绍

如果想针对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

请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值