vue-router 使用与原理分析,测试结果来啦

news2024/9/20 14:52:59

简介

Vue Router 是Vue.js的官方路由。与Vue.js核心深度集成,让用Vue.js构建单页应用(SPA)变得更加简单。

 

对于开发和维护管理后台类的前端项目,页面结构和组合可能非常复杂,所以正确的理解和使用Vue Router就显得尤为重要。

使用

创建

1、在安装好Vue Router依赖后,在App.vue中引入router-view,它是渲染的容器

<div id="app">
  <router-view></router-view>
</div>

2、创建路由router/index.js


const routes = [
  	{ path: '/', component: Home},
    { path: '/login', name: 'login', component: Login},
]
const  router = createRouter({
  history: createWebHistory(),
  routes: routes,
})
export default router

3、在main.js中使用路由

import router from "./router";
const app = createApp(App)

app.use(router)

app.mount('#app')

然后就可以在任意组件中使用this.$router形式访问它,并且以 this.$route 的形式访问当前路由:

// Home.vue
export default {
  computed: {
    username() {
      // 我们很快就会看到 `params` 是什么
      return this.$route.params.username
    },
  },
  methods: {
    goToDashboard() {
      if (isAuthenticated) {
        this.$router.push('/dashboard')
      } else {
        this.$router.push('/login')
      }
    },
  },
}

嵌套路由

一些应用程序的 UI 由多层嵌套的组件组成。在这种情况下,URL 的片段通常对应于特定的嵌套组件结构,例如:

/user/johnny/profile                     /user/johnny/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

在上层app节点的顶层router-view下,又包含的组件自己嵌套的router-view,例如以上的user模版:

const User = {
  template: `
    <div class="user">
      <h2>User {{ $route.params.id }}</h2>
      <router-view></router-view>
    </div>
  `,
}

要将组件渲染到这个嵌套的router-view中,我们需要在路由中配置 children

const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        // 当 /user/:id/profile 匹配成功
        // UserProfile 将被渲染到 User 的 <router-view> 内部
        path: 'profile',
        component: UserProfile,
      },
      {
        // 当 /user/:id/posts 匹配成功
        // UserPosts 将被渲染到 User 的 <router-view> 内部
        path: 'posts',
        component: UserPosts,
      },
    ],
  },
]

下面我们从源码的角度看下页面是如何加载并显示到页面上的

原理

上面基础的使用方法可以看出,主要包含三个步骤:

  1. 创建createRouter,并在app中use使用这个路由
  2. 在模版中使用router-view标签
  3. 导航push,跳转页面

从routers声明的数组结构可以看出,声明的路由path会被注册成路由表指向component声明的组件,并在push方法调用时,从路由表查出对应组件并加载。下面看下源码是如何实现这一过程的,Vue Router源码分析版本为4.1.5

创建安装

首先看下createRouter方法实现:

/**
 * Creates a Router instance that can be used by a Vue app.
 *
 * @param options - {@link RouterOptions}
 */
export function createRouter(options: RouterOptions): Router {
  const matcher = createRouterMatcher(options.routes, options)
  // ...

  function addRoute(
    parentOrRoute: RouteRecordName | RouteRecordRaw,
    route?: RouteRecordRaw
  ) {
    // ...
  }

  function getRoutes() {
    return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
  }

  function hasRoute(name: RouteRecordName): boolean {
    return !!matcher.getRecordMatcher(name)
  }

  function push(to: RouteLocationRaw) {
    return pushWithRedirect(to)
  }

  function replace(to: RouteLocationRaw) {
    return push(assign(locationAsObject(to), { replace: true }))
  }
  // ...

  
  const router: Router = {
    currentRoute,
    listening: true,

    addRoute,
    removeRoute,
    hasRoute,
    getRoutes,
    resolve,
    options,

    push,
    replace,
    go,
    back: () => go(-1),
    forward: () => go(1),

    beforeEach: beforeGuards.add,
    beforeResolve: beforeResolveGuards.add,
    afterEach: afterGuards.add,

    onError: errorHandlers.add,
    isReady,

    // 在app全局安装router
    install(app: App) {
      const router = this
      // 全局注册组件RouterLink、RouterView
      app.component('RouterLink', RouterLink)
      app.component('RouterView', RouterView)
    	// 全局声明router实例,this.$router访问
      app.config.globalProperties.$router = router
      // 全局注册this.$route 访问当前路由currentRoute
      Object.defineProperty(app.config.globalProperties, '$route', {
        enumerable: true,
        get: () => unref(currentRoute),
      })

      // this initial navigation is only necessary on client, on server it doesn't
      // make sense because it will create an extra unnecessary navigation and could
      // lead to problems
      if (
        isBrowser &&
        // used for the initial navigation client side to avoid pushing
        // multiple times when the router is used in multiple apps
        !started &&
        currentRoute.value === START_LOCATION_NORMALIZED
      ) {
        // see above
        // 浏览器情况下,push一个初始页面,不指定url默认首页‘/’
        started = true
        push(routerHistory.location).catch(err => {
          if (__DEV__) warn('Unexpected error when starting the router:', err)
        })
      }
    	// ...
      app.provide(routerKey, router)
      app.provide(routeLocationKey, reactive(reactiveRoute))
      // 全局注入当前路由currentRoute
      app.provide(routerViewLocationKey, currentRoute)
    	// ...
    },
  }

  return router
}

createRouter方法返回了当前路由实例,内部初始化了一些路由的常用方法,和在组件中打印this.$router结构是一样的,那install方法是在哪里调用的呢?在安装时调用了app.use(router),看下use方法,在runtime-core.cjs.prod.js下:

use(plugin, ...options) {
                if (installedPlugins.has(plugin)) ;
                else if (plugin && shared.isFunction(plugin.install)) {
                    installedPlugins.add(plugin);
                    // 如果是插件,调用插件的install方法,并把当前app传入
                    plugin.install(app, ...options);
                }
                else if (shared.isFunction(plugin)) {
                    installedPlugins.add(plugin);
                    plugin(app, ...options);
                }
                else ;
                return app;
            },

至此已经完成了全局的router创建安装,并可以在代码中使用router-viewthis.$router和实例的一些方法了,那么页面上是如何展示被加载的component呢?需要看下渲染组件router-view的内部实现

渲染

install方法注册了RouterView组件,实现在RouterView.ts

/**
 * Component to display the current route the user is at.
 */
export const RouterView = RouterViewImpl as unknown as {
  // ...
}
复制代码

RouterViewImpl实现:


export const RouterViewImpl = /*#__PURE__*/ defineComponent({
  name: 'RouterView',
  
	// ...
  
  setup(props, { attrs, slots }) {
    __DEV__ && warnDeprecatedUsage()
  	// 拿到之前注册的currentRoute
    const injectedRoute = inject(routerViewLocationKey)!
    // 当前要显示的route,监听route值变化时会刷新
    const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
      () => props.route || injectedRoute.value
    )
    // 获取当前router-view深度层级,在嵌套路由时使用
    const injectedDepth = inject(viewDepthKey, 0)
    // 在当前router-view深度下去匹配要显示的路由matched
    // matched 是个数组,在resolve方法被赋值,如果有匹配到则在当前router-view渲染
    const depth = computed<number>(() => {
      let initialDepth = unref(injectedDepth)
      const { matched } = routeToDisplay.value
      let matchedRoute: RouteLocationMatched | undefined
      while (
        (matchedRoute = matched[initialDepth]) &&
        !matchedRoute.components
      ) {
        initialDepth++
      }
      return initialDepth
    })
    const matchedRouteRef = computed<RouteLocationMatched | undefined>(
      () => routeToDisplay.value.matched[depth.value]
    )

    provide(
      viewDepthKey,
      computed(() => depth.value + 1)
    )
    provide(matchedRouteKey, matchedRouteRef)
    provide(routerViewLocationKey, routeToDisplay)

    const viewRef = ref<ComponentPublicInstance>()

    // watch at the same time the component instance, the route record we are
    // rendering, and the name
    // 监听匹配路由变化时,刷新  
    watch(
      () => [viewRef.value, matchedRouteRef.value, props.name] as const,
      ([instance, to, name], [oldInstance, from, oldName]) => {
        // ...
      },
      { flush: 'post' }
    )

    return () => {
      const route = routeToDisplay.value
      // we need the value at the time we render because when we unmount, we
      // navigated to a different location so the value is different
      const currentName = props.name
      const matchedRoute = matchedRouteRef.value
      const ViewComponent =
        matchedRoute && matchedRoute.components![currentName]
      if (!ViewComponent) {
        return normalizeSlot(slots.default, { Component: ViewComponent, route })
      }
    	// ...
    	// 关键:h函数,渲染路由中获得的组件
      const component = h(
        ViewComponent,
        assign({}, routeProps, attrs, {
          onVnodeUnmounted,
          ref: viewRef,
        })
      )

      return (
        // pass the vnode to the slot as a prop.
        // h and <component :is="..."> both accept vnodes
        normalizeSlot(slots.default, { Component: component, route }) ||
        component
      )
    }
  },
})

实现嵌套路由的核心是使用深度depth控制,初始router-view深度为0,内部嵌套深度依次加1,比如对如下嵌套关系:

const routes = [
    {
        path: '/',
        component: Home,
        children: [
            {
                path: 'product',
                component: ProductManage
            },
        ]
    },
    { path: '/login', name: 'login', component: Login }
  ]

它们在resolve中被解析成的routeToDisplay.value依次为:

matched是个数组,在pushresolve时,把当前路径path拆分解析成对应routes数组中可以匹配的对象,然后初始值的router-view,就取深度为0的值,深度1的router-view就取到mactched[1]'/product'对应的route,分别渲染

跳转

分析跳转流程之前,先看下路由注册的解析逻辑,在createRouter方法中调用了createRouterMatcher方法,该方法创建了一个路由匹配器,内部封装了路由注册和跳转的具体实现,外部创建的router是对matcher的包了一层提供API,并屏蔽实现细节。看下实现:


/**
 * Creates a Router Matcher.
 *
 * @internal
 * @param routes - array of initial routes
 * @param globalOptions - global route options
 */
export function createRouterMatcher(
  routes: Readonly<RouteRecordRaw[]>,
  globalOptions: PathParserOptions
): RouterMatcher {
  // normalized ordered array of matchers
  // 匹配器的两个容器,匹配器Array和命名路由Map
  const matchers: RouteRecordMatcher[] = []
  const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
  
  function getRecordMatcher(name: RouteRecordName) {
    return matcherMap.get(name)
  }

  function addRoute(
    record: RouteRecordRaw,
    parent?: RouteRecordMatcher,
    originalRecord?: RouteRecordMatcher
  ) {
    // ...

    // 如果记录中声明'alias'别名,把别名当作path,插入一条新的记录
    if ('alias' in record) {
      const aliases =
        typeof record.alias === 'string' ? [record.alias] : record.alias!
      for (const alias of aliases) {
        normalizedRecords.push(
          assign({}, mainNormalizedRecord, {
            // this allows us to hold a copy of the `components` option
            // so that async components cache is hold on the original record
            components: originalRecord
              ? originalRecord.record.components
              : mainNormalizedRecord.components,
            path: alias,
            // we might be the child of an alias
            aliasOf: originalRecord
              ? originalRecord.record
              : mainNormalizedRecord,
            // the aliases are always of the same kind as the original since they
            // are defined on the same record
          }) as typeof mainNormalizedRecord
        )
      }
    }

    let matcher: RouteRecordMatcher
    let originalMatcher: RouteRecordMatcher | undefined

    for (const normalizedRecord of normalizedRecords) {
      // ...
      // create the object beforehand, so it can be passed to children
      // 遍历记录,生成一个matcher
      matcher = createRouteRecordMatcher(normalizedRecord, parent, options)

     	// ...
      // 添加到容器
      insertMatcher(matcher)
    }

    return originalMatcher
      ? () => {
          // since other matchers are aliases, they should be removed by the original matcher
          removeRoute(originalMatcher!)
        }
      : noop
  }

  function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
  	// 删除路由元素
    if (isRouteName(matcherRef)) {
      const matcher = matcherMap.get(matcherRef)
      if (matcher) {
        matcherMap.delete(matcherRef)
        matchers.splice(matchers.indexOf(matcher), 1)
        matcher.children.forEach(removeRoute)
        matcher.alias.forEach(removeRoute)
      }
    } else {
      const index = matchers.indexOf(matcherRef)
      if (index > -1) {
        matchers.splice(index, 1)
        if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name)
        matcherRef.children.forEach(removeRoute)
        matcherRef.alias.forEach(removeRoute)
      }
    }
  }

  function getRoutes() {
    return matchers
  }

  function insertMatcher(matcher: RouteRecordMatcher) {
    let i = 0
    while (
      i < matchers.length &&
      comparePathParserScore(matcher, matchers[i]) >= 0 &&
      // Adding children with empty path should still appear before the parent
      // https://github.com/vuejs/router/issues/1124
      (matcher.record.path !== matchers[i].record.path ||
        !isRecordChildOf(matcher, matchers[i]))
    )
      i++
  	// 将matcher添加到数组末尾
    matchers.splice(i, 0, matcher)
    // only add the original record to the name map
    // 命名路由添加到路由Map
    if (matcher.record.name && !isAliasRecord(matcher))
      matcherMap.set(matcher.record.name, matcher)
  }

  function resolve(
    location: Readonly<MatcherLocationRaw>,
    currentLocation: Readonly<MatcherLocation>
  ): MatcherLocation {
    let matcher: RouteRecordMatcher | undefined
    let params: PathParams = {}
    let path: MatcherLocation['path']
    let name: MatcherLocation['name']

    if ('name' in location && location.name) {
      // 命名路由解析出path
      matcher = matcherMap.get(location.name)
      // ...
      // throws if cannot be stringified
      path = matcher.stringify(params)
    } else if ('path' in location) {
      // no need to resolve the path with the matcher as it was provided
      // this also allows the user to control the encoding
      path = location.path
      //...
      
      matcher = matchers.find(m => m.re.test(path))
      // matcher should have a value after the loop

      if (matcher) {
        // we know the matcher works because we tested the regexp
        params = matcher.parse(path)!
        name = matcher.record.name
      }
      // push相对路径
    } else {
      // match by name or path of current route
      matcher = currentLocation.name
        ? matcherMap.get(currentLocation.name)
        : matchers.find(m => m.re.test(currentLocation.path))
      if (!matcher)
        throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
          location,
          currentLocation,
        })
      name = matcher.record.name
      // since we are navigating to the same location, we don't need to pick the
      // params like when `name` is provided
      params = assign({}, currentLocation.params, location.params)
      path = matcher.stringify(params)
    }

    const matched: MatcherLocation['matched'] = []
    let parentMatcher: RouteRecordMatcher | undefined = matcher
    while (parentMatcher) {
      // reversed order so parents are at the beginning
    	// 和当前path匹配的记录,插入到数组头部,让父级先匹配
      matched.unshift(parentMatcher.record)
      parentMatcher = parentMatcher.parent
    }

    return {
      name,
      path,
      params,
      matched,
      meta: mergeMetaFields(matched),
    }
  }

  // 添加初始路由
  routes.forEach(route => addRoute(route))

  return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}

总结一下,createRouterMatcher方法,为每一个routres执行了addRoute方法,调用了insertMatcher,将生成的matchers插入到容器中,后边在调用的时候,通过resolve方法,将记录匹配到到Matcher.record记录保存到MatcherLocationmatched数组中,后续router-view会根据depth从数组取应该要渲染的元素。 push方法执行流程:

function push(to: RouteLocationRaw) {
    return pushWithRedirect(to)
  }

// ...


  function pushWithRedirect(
    to: RouteLocationRaw | RouteLocation,
    redirectedFrom?: RouteLocation
  ): Promise<NavigationFailure | void | undefined> {
    // 解析出目标location
    const targetLocation: RouteLocation = (pendingLocation = resolve(to))
    
    const from = currentRoute.value
    const data: HistoryState | undefined = (to as RouteLocationOptions).state
    const force: boolean | undefined = (to as RouteLocationOptions).force
    // to could be a string where `replace` is a function
    const replace = (to as RouteLocationOptions).replace === true

    const shouldRedirect = handleRedirectRecord(targetLocation)

    // 重定向逻辑
    if (shouldRedirect)
      return pushWithRedirect(
        assign(locationAsObject(shouldRedirect), {
          state:
            typeof shouldRedirect === 'object'
              ? assign({}, data, shouldRedirect.state)
              : data,
          force,
          replace,
        }),
        // keep original redirectedFrom if it exists
        redirectedFrom || targetLocation
      )

    // if it was a redirect we already called `pushWithRedirect` above
    const toLocation = targetLocation as RouteLocationNormalized
  	// ...

    return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
      .catch((error: NavigationFailure | NavigationRedirectError) =>
        // ...
      )
      .then((failure: NavigationFailure | NavigationRedirectError | void) => {
        if (failure) {
          // ...
        } else {
          // if we fail we don't finalize the navigation
          failure = finalizeNavigation(
            toLocation as RouteLocationNormalizedLoaded,
            from,
            true,
            replace,
            data
          )
        }
        triggerAfterEach(
          toLocation as RouteLocationNormalizedLoaded,
          from,
          failure
        )
        return failure
      })
  }

在没有失败情况下调用finalizeNavigation做最终跳转,看下实现:

/**
   * - Cleans up any navigation guards
   * - Changes the url if necessary
   * - Calls the scrollBehavior
   */
  function finalizeNavigation(
    toLocation: RouteLocationNormalizedLoaded,
    from: RouteLocationNormalizedLoaded,
    isPush: boolean,
    replace?: boolean,
    data?: HistoryState
  ): NavigationFailure | void {
    // a more recent navigation took place
    const error = checkCanceledNavigation(toLocation, from)
    if (error) return error

    // only consider as push if it's not the first navigation
    const isFirstNavigation = from === START_LOCATION_NORMALIZED
    const state = !isBrowser ? {} : history.state

    // change URL only if the user did a push/replace and if it's not the initial navigation because
    // it's just reflecting the url
    // 如果是push保存历史到routerHistory
    if (isPush) {
      // on the initial navigation, we want to reuse the scroll position from
      // history state if it exists
      if (replace || isFirstNavigation)
        routerHistory.replace(
          toLocation.fullPath,
          assign(
            {
              scroll: isFirstNavigation && state && state.scroll,
            },
            data
          )
        )
      else routerHistory.push(toLocation.fullPath, data)
    }

    // accept current navigation
    // 给当前路由赋值,会触发监听的router-view刷新
    currentRoute.value = toLocation
    handleScroll(toLocation, from, isPush, isFirstNavigation)

    markAsReady()
  }

currentRoute.value = toLocation执行完后,会触发router-viewrouteToDisplay值变化,重新计算matchedRouteRef获得新的ViewComponent,完成页面刷新。 上面还有两点,routerresolve会调用到matcherresolve,填充刚刚说过的matched数组,navigate方法会执行导航上的守卫,这两步就不看了,感兴趣同学可以自己查阅《住院证明图片》,至此主要的流程已经分析完了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/13391.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[附源码]SSM计算机毕业设计ssm新冠疫苗预约接种信息管理JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Spring事务管理

认识事务 可以把一系列&#xff08;多条sql语句&#xff09;要执行的操作称为事务&#xff0c;而事务管理就是管理这些操作要么完全执行&#xff0c;要么完全不执行&#xff08;很经典的一个例子是&#xff1a;A要给B转钱&#xff0c;首先A的钱减少了&#xff0c;但是突然的数…

EMQX数据流转MySQL踩坑日记:EMQX VER 4.2.3

总结&#xff1a; &#xff08;0&#xff09;数据库报错问题&#xff0c;详细参考这篇文档&#xff0c;链接&#xff0c;ln -s 源 目标 https://blog.csdn.net/weixin_42110159/article/details/118945136 &#xff08;1&#xff09;数据库建立数据&#xff0c;要注意大小写&am…

数字化开采|AIRIOT智慧矿山自动化生产解决方案

由于矿山地形复杂&#xff0c;生产自动化水平低&#xff0c;安全监管技术落后&#xff0c;事故频发等很多因素对煤矿开采技术提出了数据化、可视化、智能化的要求。通过目前的煤矿开采现状可以发现煤矿开采过程中&#xff0c;在生产、监管、巡检、安全、效率等方面还存在许多有…

图文详解Linux基础经典教程(08)——CentOS安装MySQL数据库

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 概述 之前&#xff0c;我们在CentOS中安装了JDK、Tomcat&#xff1b;接下来&#xff0c;我们在CentOS中采用YUM的方式安装MySQL5.6数据库。 安装前准备工作 在此&#xf…

面试常用算法归纳

最长子串、子序列 先说明下子串和子序列的问题&#xff1a;对于s “pwwkew"来说&#xff0c;其中一个子串为"wke”&#xff0c;而"pwke" 是一个子序列。 子序列&#xff1a;一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串在不改…

基于Matlab通用视频处理系统的设计-含Matlab代码

⭕⭕ 目 录 ⭕⭕⏩ 一、引言⏩ 二、系统总体方案设计⏩ 2.1 方案设计⏩ 2.2 界面设计⏩ 三、实例分析⏩ 四、参考文献⏩ 五、Matlab程序获取⏩ 一、引言 随着信息技术的发展&#xff0c;基于视频图像中对感兴趣的目标提取&#xff0c;已经逐渐渗透到人们生活的方方面面&#x…

[附源码]SSM计算机毕业设计“拥抱爱心”公益网站管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

VMware Workstation 与 Device/Credential Guard 不兼容问题

系列文章目录 VMware Workstation 与 Device/Credential Guard 不兼容问题 VMware Workstation 与 Device/Credential Guard 不兼容问题系列文章目录一、原因二、解决办法2.1修改虚拟化安全设备为禁用2.2HV主机服务启动类型设置 为 “禁用”2.3关闭 Hyper-V 并且打开虚拟机平台…

CUDA By Example(六)——纹理内存

在本章中&#xff0c;我们将学习如何分配和使用纹理内存(Texture Memory)。和常量内存一样&#xff0c;纹理内存是另一种类型的只读内存&#xff0c;在特定的访问模式中&#xff0c;纹理内存同样能够提升性能并减少内存流量。虽然纹理内存最初是针对传统的图形处理应用程序而设…

Linux学习-43-挂载Linux系统外的文件mount和卸载文件系统umount命令用法

10.10 mount命令详解&#xff1a;挂载Linux系统外的文件 所有的硬件设备必须挂载之后才能使用&#xff08;新硬盘先格式化后创建分区&#xff0c;再对分区进行挂载&#xff09;&#xff0c;只不过&#xff0c;有些硬件设备&#xff08;比如硬盘分区&#xff09;在每次系统启动…

记录一次我虚拟机好不容易连上后的配置

有一说一&#xff0c;看到这个响应&#xff0c;人都麻了 在此我记录一下我检查了哪些&#xff0c;做了哪些 一、Windows本地服务 这一块&#xff0c;有一个算一个&#xff0c;没起的启动&#xff0c;启动的重启 二、VMware的虚拟网络编辑器设置 因为我这次成功用的是NAT模式&a…

图像分割 - 阈值处理 - 多阈值处理(OTSU)

目录 1. 多阈值处理介绍 2. 代码讲解 3. 完整代码 1. 多阈值处理介绍 之前介绍的都是全局单个阈值对图像的分割。固定阈值法&#xff0c;阈值是人工根据灰度直方图的波谷进行设置的。全局阈值法&#xff0c;根据不停的迭代两个区域间的平均灰度进行分割。OUST最大类间方差法…

centos7 环境安装 PM2 管理 node

前言&#xff1a; 由于最新的项目中用到的框架是 ssr 框架。 Vue使用的ssr是 nuxt.js&#xff0c;由于 nuxt.js 和普通的Vue项目不同&#xff0c;所以部署到Linux服务器的方式和普通的Vue项目是有区别的。 1、PM2 介绍 PM2 是一款非常优秀的 Node 进程管理工具&#xff0c;它…

用于科学研究的TCO反式环辛烯:1312010-03-9,(4E)-TCO-CycP-O-PNB ester

(4E)-TCO-CycP-O-PNB ester物理数据&#xff1a; CAS&#xff1a;1312010-03-9| 中文名&#xff1a;(4E)-反式环辛烯-CycP-O-PNB ester&#xff0c; (4E)-反式环辛烯-CYCP-O-PNB-酯 | 英文名&#xff1a;(4E)-TCO-CycP-O-PNB ester 结构式&#xff1a; 英文别名&#xff1a; …

试用信号灯实现如图所示的进程同步关系

试用信号灯实现如图所示的进程同步关系 信号量的个数要等于具有直接前驱的进程个数 P2,P3,P4,P5这些进程有前驱&#xff0c;所以设S2S3S4S50 因为P1执行完&#xff0c;P2,P3,P4才能执行因为P1没有直接前驱&#xff0c;所以直接释放P2.P3.P4的信号量S2,S3,S4P1{V(S2)V(S3)V(…

03-HTML

1 HTML入门 1.1 初识HTML 1.1.1 概述 网络世界已经跟我们息息相关&#xff0c;当我们打开一个网站&#xff0c;首先映入眼帘的就是一个个华丽多彩的网页。这些网页&#xff0c;不仅呈现着基本的内容&#xff0c;还具备优雅的布局和丰富的动态效果&#xff0c;这一切都是如何…

图像分割简介

相比于目标检测只是将目标位置检测出来而言&#xff0c;目标分割能够更精准的将图像进行划分。图像分割在计算机视觉中的地位 为后续检测、识别等提供技术支持。 图像分割难点以及处理 难点&#xff1a;图像特征的组合难以表达&#xff1f; 比如 图中人的头发和裤子是黑色&a…

《FFmpeg Basics》中文版-10-为视频添加文字

正文 视频中包含的文本数据可以显着提高其信息质量。 在视频中添加文字的相关介绍 如何将一些文本添加到视频输出中的两种常用方法是使用前一章中的字幕或叠加技术(overlay)。 具有许多可能性的最高级选项是使用表中描述的抽象滤镜&#xff1a; 描述从文本文件或字符串在视频…

成像雷达量产突破:木牛携手全球合作伙伴突破智驾瓶颈

时隔三年的全球工程机械行业大展&#xff0c;第33届2022德国慕尼黑Bauma展&#xff0c;于近期圆满收官。作为2022年为数不多的全球性展会&#xff0c;吸引了60多个国家和地区的3100余家工程机械企业聚首&#xff0c;行业新品竞相角逐&#xff0c;数字智能化的创新产品成为本次展…