溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

淺析vue-router原理

發(fā)布時間:2020-09-18 04:38:14 來源:腳本之家 閱讀:279 作者:mrr 欄目:web開發(fā)

近期被問到一個問題,在你們項目中使用的是Vue的SPA(單頁面)還是Vue的多頁面設(shè)計?

這篇文章主要圍繞Vue的SPA單頁面設(shè)計展開。 關(guān)于如何展開Vue多頁面設(shè)計請點擊查看。

vue-router是什么?

首先我們需要知道vue-router是什么,它是干什么的?

這里指的路由并不是指我們平時所說的硬件路由器,這里的路由就是SPA(單頁應(yīng)用)的路徑管理器。 換句話說,vue-router就是WebApp的鏈接路徑管理系統(tǒng)。

vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,適合用于構(gòu)建單頁面應(yīng)用。

那與傳統(tǒng)的頁面跳轉(zhuǎn)有什么區(qū)別呢?

1.vue的單頁面應(yīng)用是基于路由和組件的,路由用于設(shè)定訪問路徑,并將路徑和組件映射起來。

2.傳統(tǒng)的頁面應(yīng)用,是用一些超鏈接來實現(xiàn)頁面切換和跳轉(zhuǎn)的。

在vue-router單頁面應(yīng)用中,則是路徑之間的切換,也就是組件的切換。路由模塊的本質(zhì) 就是建立起url和頁面之間的映射關(guān)系。

至于為啥不能用a標(biāo)簽,這是因為用Vue做的都是單頁應(yīng)用,就相當(dāng)于只有一個主的index.html頁面,所以你寫的標(biāo)簽是不起作用的,必須使用vue-router來進(jìn)行管理。

vue-router實現(xiàn)原理

SPA(single page application):單一頁面應(yīng)用程序,有且只有一個完整的頁面;當(dāng)它在加載頁面的時候,不會加載整個頁面的內(nèi)容,而只更新某個指定的容器中內(nèi)容。

單頁面應(yīng)用(SPA)的核心之一是:

1.更新視圖而不重新請求頁面;

2.vue-router在實現(xiàn)單頁面前端路由時,提供了三種方式:Hash模式、History模式、abstract模式,根據(jù)mode參數(shù)來決定采用哪一種方式。

路由模式

vue-router 提供了三種運行模式:

● hash: 使用 URL hash 值來作路由。默認(rèn)模式。

● history: 依賴 HTML5 History API 和服務(wù)器配置。查看 HTML5 History 模式。

● abstract: 支持所有 JavaScript 運行環(huán)境,如 Node.js 服務(wù)器端。

Hash模式

vue-router 默認(rèn)模式是 hash 模式 —— 使用 URL 的 hash 來模擬一個完整的 URL,當(dāng) URL 改變時,頁面不會去重新加載。

hash(#)是URL 的錨點,代表的是網(wǎng)頁中的一個位置,單單改變#后的部分(/#/..),瀏覽器只會加載相應(yīng)位置的內(nèi)容,不會重新加載網(wǎng)頁,也就是說 #是用來指導(dǎo)瀏覽器動作的,對服務(wù)器端完全無用,HTTP請求中不包括#;同時每一次改變#后的部分,都會在瀏覽器的訪問歷史中增加一個記錄,使用”后退”按鈕,就可以回到上一個位置; 所以說Hash模式通過錨點值的改變,根據(jù)不同的值,渲染指定DOM位置的不同數(shù)據(jù) 。

History模式

HTML5 History API提供了一種功能,能讓開發(fā)人員在不刷新整個頁面的情況下修改站點的URL,就是利用 history.pushState API 來完成 URL 跳轉(zhuǎn)而無須重新加載頁面;

由于hash模式會在url中自帶#,如果不想要很丑的 hash,我們可以用路由的 history 模式,只需要在配置路由規(guī)則時,加入"mode: 'history'",這種模式充分利用 history.pushState API 來完成 URL 跳轉(zhuǎn)而無須重新加載頁面。

//main.js文件中
const router = new VueRouter({
 mode: 'history',
 routes: [...]
})

當(dāng)使用 history 模式時,URL 就像正常的 url,例如 yoursite.com/user/id,比較好… 不過這種模式要玩好,還需要后臺配置支持。因為我們的應(yīng)用是個單頁客戶端應(yīng)用,如果后臺沒有正確的配置,當(dāng)用戶在瀏覽器直接訪問

所以呢,你要在服務(wù)端增加一個覆蓋所有情況的候選資源:如果 URL 匹配不到任何靜態(tài)資源,則應(yīng)該返回同一個 index.html 頁面,這個頁面就是你 app 依賴的頁面。

export const routes = [ 
 {path: "/", name: "homeLink", component:Home}
 {path: "/register", name: "registerLink", component: Register},
 {path: "/login", name: "loginLink", component: Login},
 {path: "*", redirect: "/"}] 

此處就設(shè)置如果URL輸入錯誤或者是URL 匹配不到任何靜態(tài)資源,就自動跳到到Home頁面。

abstract模式

abstract模式是使用一個不依賴于瀏覽器的瀏覽歷史虛擬管理后端。

根據(jù)平臺差異可以看出,在 Weex 環(huán)境中只支持使用 abstract 模式。 不過,vue-router 自身會對環(huán)境做校驗,如果發(fā)現(xiàn)沒有瀏覽器的 API,vue-router 會自動強(qiáng)制進(jìn)入 abstract 模式,所以 在使用 vue-router 時只要不寫 mode 配置即可,默認(rèn)會在瀏覽器環(huán)境中使用 hash 模式,在移動端原生環(huán)境中使用 abstract 模式。 (當(dāng)然,你也可以明確指定在所有情況下都使用 abstract 模式)

vue-router使用方式

1:下載npm i vue-router -S

**2:在main.js中引入 ** import VueRouter from 'vue-router';

3:安裝插件Vue.use(VueRouter);

4:創(chuàng)建路由對象并配置路由規(guī)則

let router = new VueRouter({routes:[{path:'/home',component:Home}]});

5:將其路由對象傳遞給Vue的實例,options中加入 router:router

6:在app.vue中留坑

<router-view></router-view>

具體實現(xiàn)請看如下代碼:

//main.js文件中引入
import Vue from 'vue';
import VueRouter from 'vue-router';
//主體
import App from './components/app.vue';
import index from './components/index.vue'
//安裝插件
Vue.use(VueRouter); //掛載屬性
//創(chuàng)建路由對象并配置路由規(guī)則
let router = new VueRouter({
  routes: [
    //一個個對象
    { path: '/index', component: index }
  ]
});
//new Vue 啟動
new Vue({
  el: '#app',
  //讓vue知道我們的路由規(guī)則
  router: router, //可以簡寫router
  render: c => c(App),
})

最后記得在在app.vue中“留坑”

//app.vue中
<template>
  <div>
    <!-- 留坑,非常重要 -->
    <router-view></router-view>
  </div>
</template>
<script>
  export default {
    data(){
      return {}
    }
  }
</script>

vue-router源碼分析

我們先來看看vue的實現(xiàn)路徑。

淺析vue-router原理

在入口文件中需要實例化一個 VueRouter 的實例對象 ,然后將其傳入 Vue 實例的 options 中。

export default class VueRouter {
 static install: () => void;
 static version: string;
 app: any;
 apps: Array<any>;
 ready: boolean;
 readyCbs: Array<Function>;
 options: RouterOptions;
 mode: string;
 history: HashHistory | HTML5History | AbstractHistory;
 matcher: Matcher;
 fallback: boolean;
 beforeHooks: Array<?NavigationGuard>;
 resolveHooks: Array<?NavigationGuard>;
 afterHooks: Array<?AfterNavigationHook>;
 constructor (options: RouterOptions = {}) {
  this.app = null
  this.apps = []
  this.options = options
  this.beforeHooks = []
  this.resolveHooks = []
  this.afterHooks = []
  // 創(chuàng)建 matcher 匹配函數(shù)
  this.matcher = createMatcher(options.routes || [], this)
  // 根據(jù) mode 實例化具體的 History,默認(rèn)為'hash'模式
  let mode = options.mode || 'hash'
  // 通過 supportsPushState 判斷瀏覽器是否支持'history'模式
  // 如果設(shè)置的是'history'但是如果瀏覽器不支持的話,'history'模式會退回到'hash'模式
  // fallback 是當(dāng)瀏覽器不支持 history.pushState 控制路由是否應(yīng)該回退到 hash 模式。默認(rèn)值為 true。
  this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
  if (this.fallback) {
   mode = 'hash'
  }
  // 不在瀏覽器內(nèi)部的話,就會變成'abstract'模式
  if (!inBrowser) {
   mode = 'abstract'
  }
  this.mode = mode
   // 根據(jù)不同模式選擇實例化對應(yīng)的 History 類
  switch (mode) {
   case 'history':
    this.history = new HTML5History(this, options.base)
    break
   case 'hash':
    this.history = new HashHistory(this, options.base, this.fallback)
    break
   case 'abstract':
    this.history = new AbstractHistory(this, options.base)
    break
   default:
    if (process.env.NODE_ENV !== 'production') {
     assert(false, `invalid mode: ${mode}`)
    }
  }
 }
 match (
  raw: RawLocation,
  current?: Route,
  redirectedFrom?: Location
 ): Route {
  return this.matcher.match(raw, current, redirectedFrom)
 }
 get currentRoute (): ?Route {
  return this.history && this.history.current
 }
 init (app: any /* Vue component instance */) {
  process.env.NODE_ENV !== 'production' && assert(
   install.installed,
   `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
   `before creating root instance.`
  )
  this.apps.push(app)
  // main app already initialized.
  if (this.app) {
   return
  }
  this.app = app
  const history = this.history
  // 根據(jù)history的類別執(zhí)行相應(yīng)的初始化操作和監(jiān)聽
  if (history instanceof HTML5History) {
   history.transitionTo(history.getCurrentLocation())
  } else if (history instanceof HashHistory) {
   const setupHashListener = () => {
    history.setupListeners()
   }
   history.transitionTo(
    history.getCurrentLocation(),
    setupHashListener,
    setupHashListener
   )
  }
  history.listen(route => {
   this.apps.forEach((app) => {
    app._route = route
   })
  })
 }
 // 路由跳轉(zhuǎn)之前
 beforeEach (fn: Function): Function {
  return registerHook(this.beforeHooks, fn)
 }
 // 路由導(dǎo)航被確認(rèn)之間前
 beforeResolve (fn: Function): Function {
  return registerHook(this.resolveHooks, fn)
 }
 // 路由跳轉(zhuǎn)之后
 afterEach (fn: Function): Function {
  return registerHook(this.afterHooks, fn)
 }
 // 第一次路由跳轉(zhuǎn)完成時被調(diào)用的回調(diào)函數(shù)
 onReady (cb: Function, errorCb?: Function) {
  this.history.onReady(cb, errorCb)
 }
 // 路由報錯
 onError (errorCb: Function) {
  this.history.onError(errorCb)
 }
 // 路由添加,這個方法會向history棧添加一個記錄,點擊后退會返回到上一個頁面。
 push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.history.push(location, onComplete, onAbort)
 }
 // 這個方法不會向history里面添加新的記錄,點擊返回,會跳轉(zhuǎn)到上上一個頁面。上一個記錄是不存在的。
 replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.history.replace(location, onComplete, onAbort)
 }
 // 相對于當(dāng)前頁面向前或向后跳轉(zhuǎn)多少個頁面,類似 window.history.go(n)。n可為正數(shù)可為負(fù)數(shù)。正數(shù)返回上一個頁面
 go (n: number) {
  this.history.go(n)
 }
 // 后退到上一個頁面
 back () {
  this.go(-1)
 }
 // 前進(jìn)到下一個頁面
 forward () {
  this.go(1)
 }
 getMatchedComponents (to?: RawLocation | Route): Array<any> {
  const route: any = to
   ? to.matched
    ? to
    : this.resolve(to).route
   : this.currentRoute
  if (!route) {
   return []
  }
  return [].concat.apply([], route.matched.map(m => {
   return Object.keys(m.components).map(key => {
    return m.components[key]
   })
  }))
 }
 resolve (
  to: RawLocation,
  current?: Route,
  append?: boolean
 ): {
  location: Location,
  route: Route,
  href: string,
  // for backwards compat
  normalizedTo: Location,
  resolved: Route
 } {
  const location = normalizeLocation(
   to,
   current || this.history.current,
   append,
   this
  )
  const route = this.match(location, current)
  const fullPath = route.redirectedFrom || route.fullPath
  const base = this.history.base
  const href = createHref(base, fullPath, this.mode)
  return {
   location,
   route,
   href,
   // for backwards compat
   normalizedTo: location,
   resolved: route
  }
 }
 addRoutes (routes: Array<RouteConfig>) {
  this.matcher.addRoutes(routes)
  if (this.history.current !== START) {
   this.history.transitionTo(this.history.getCurrentLocation())
  }
 }
}

HashHistory

• hash雖然出現(xiàn)在url中,但不會被包括在http請求中,它是用來指導(dǎo)瀏覽器動作的,對服務(wù)器端沒影響,因此,改變hash不會重新加載頁面。

• 可以為hash的改變添加監(jiān)聽事件:

window.addEventListener("hashchange",funcRef,false)

• 每一次改變hash(window.location.hash),都會在瀏覽器訪問歷史中增加一個記錄。

export class HashHistory extends History {
 constructor (router: Router, base: ?string, fallback: boolean) {
  super(router, base)
  // check history fallback deeplinking
  // 如果是從history模式降級來的,需要做降級檢查
  if (fallback && checkFallback(this.base)) {
  // 如果降級且做了降級處理,則返回
   return
  }
  ensureSlash()
 }
 .......

function checkFallback (base) {
 const location = getLocation(base)
 // 得到除去base的真正的 location 值
 if (!/^\/#/.test(location)) {
 // 如果此時地址不是以 /# 開頭的
 // 需要做一次降級處理,降為 hash 模式下應(yīng)有的 /# 開頭
  window.location.replace(
   cleanPath(base + '/#' + location)
  )
  return true
 }
}
function ensureSlash (): boolean {
// 得到 hash 值
 const path = getHash()
 if (path.charAt(0) === '/') {
  // 如果是以 / 開頭的,直接返回即可
  return true
 }
 // 不是的話,需要手動保證一次 替換 hash 值
 replaceHash('/' + path)
 return false
}
export function getHash (): string {
 // We can't use window.location.hash here because it's not
 // consistent across browsers - Firefox will pre-decode it!
 // 因為兼容性的問題,這里沒有直接使用 window.location.hash
 // 因為 Firefox decode hash 值
 const href = window.location.href
 const index = href.indexOf('#')
 return index === -1 ? '' : decodeURI(href.slice(index + 1))
}
// 得到hash之前的url地址
function getUrl (path) {
 const href = window.location.href
 const i = href.indexOf('#')
 const base = i >= 0 ? href.slice(0, i) : href
 return `${base}#${path}`
}
// 添加一個hash
function pushHash (path) {
 if (supportsPushState) {
  pushState(getUrl(path))
 } else {
  window.location.hash = path
 }
}
// 替代hash
function replaceHash (path) {
 if (supportsPushState) {
  replaceState(getUrl(path))
 } else {
  window.location.replace(getUrl(path))
 }
}

hash的改變會自動添加到瀏覽器的訪問歷史記錄中。 那么視圖的更新是怎么實現(xiàn)的呢,看下 transitionTo()方法:

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const route = this.router.match(location, this.current) //找到匹配路由
  this.confirmTransition(route, () => { //確認(rèn)是否轉(zhuǎn)化
   this.updateRoute(route) //更新route
   onComplete && onComplete(route)
   this.ensureURL()

   // fire ready cbs once
   if (!this.ready) {
    this.ready = true
    this.readyCbs.forEach(cb => { cb(route) })
   }
  }, err => {
   if (onAbort) {
    onAbort(err)
   }
   if (err && !this.ready) {
    this.ready = true
    this.readyErrorCbs.forEach(cb => { cb(err) })
   }
  })
 }
 
//更新路由
updateRoute (route: Route) {
  const prev = this.current // 跳轉(zhuǎn)前路由
  this.current = route // 裝備跳轉(zhuǎn)路由
  this.cb && this.cb(route) // 回調(diào)函數(shù),這一步很重要,這個回調(diào)函數(shù)在index文件中注冊,會更新被劫持的數(shù)據(jù) _router
  this.router.afterHooks.forEach(hook => {
   hook && hook(route, prev)
  })
 }
}

pushState
export function pushState (url?: string, replace?: boolean) {
 saveScrollPosition()
 // try...catch the pushState call to get around Safari
 // DOM Exception 18 where it limits to 100 pushState calls
 // 加了 try...catch 是因為 Safari 有調(diào)用 pushState 100 次限制
 // 一旦達(dá)到就會拋出 DOM Exception 18 錯誤
 const history = window.history
 try {
  if (replace) {
  // replace 的話 key 還是當(dāng)前的 key 沒必要生成新的
   history.replaceState({ key: _key }, '', url)
  } else {
  // 重新生成 key
   _key = genKey()
    // 帶入新的 key 值
   history.pushState({ key: _key }, '', url)
  }
 } catch (e) {
 // 達(dá)到限制了 則重新指定新的地址
  window.location[replace ? 'replace' : 'assign'](url)
 }
}

replaceState
// 直接調(diào)用 pushState 傳入 replace 為 true
export function replaceState (url?: string) {
 pushState(url, true)
}

pushState和replaceState兩種方法的共同特點:當(dāng)調(diào)用他們修改瀏覽器歷史棧后,雖然當(dāng)前url改變了,但瀏覽器不會立即發(fā)送請求該url,這就為單頁應(yīng)用前端路由,更新視圖但不重新請求頁面提供了基礎(chǔ)。

supportsPushState
export const supportsPushState = inBrowser && (function () {
 const ua = window.navigator.userAgent
 if (
  (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
  ua.indexOf('Mobile Safari') !== -1 &&
  ua.indexOf('Chrome') === -1 &&
  ua.indexOf('Windows Phone') === -1
 ) {
  return false
 }
 return window.history && 'pushState' in window.history
})()

其實所謂響應(yīng)式屬性,即當(dāng)_route值改變時,會自動調(diào)用Vue實例的render()方法,更新視圖。 $router.push()-->HashHistory.push()-->History.transitionTo()-->History.updateRoute()-->{app._route=route}-->vm.render()

監(jiān)聽地址欄

在瀏覽器中,用戶可以直接在瀏覽器地址欄中輸入改變路由,因此還需要監(jiān)聽瀏覽器地址欄中路由的變化 ,并具有與通過代碼調(diào)用相同的響應(yīng)行為,在HashHistory中這一功能通過setupListeners監(jiān)聽hashchange實現(xiàn):

setupListeners () {
  window.addEventListener('hashchange', () => {
    if (!ensureSlash()) {
      return
    }
    this.transitionTo(getHash(), route => {
      replaceHash(route.fullPath)
    })
  })
}

HTML5History

History interface是瀏覽器歷史記錄棧提供的接口,通過back(),forward(),go()等方法,我們可以讀取瀏覽器歷史記錄棧的信息,進(jìn)行各種跳轉(zhuǎn)操作。

export class HTML5History extends History {
 constructor (router: Router, base: ?string) {
  super(router, base)
  const expectScroll = router.options.scrollBehavior //指回滾方式
  const supportsScroll = supportsPushState && expectScroll
  if (supportsScroll) {
   setupScroll()
  }
  const initLocation = getLocation(this.base)
  //監(jiān)控popstate事件
  window.addEventListener('popstate', e => {
   const current = this.current
   // Avoiding first `popstate` event dispatched in some browsers but first
   // history route not updated since async guard at the same time.
   // 避免在某些瀏覽器中首次發(fā)出“popstate”事件
   // 由于同一時間異步監(jiān)聽,history路由沒有同時更新。
   const location = getLocation(this.base)
   if (this.current === START && location === initLocation) {
    return
   }
   this.transitionTo(location, route => {
    if (supportsScroll) {
     handleScroll(router, route, current, true)
    }
   })
  })
 }

hash模式僅改變hash部分的內(nèi)容,而hash部分是不會包含在http請求中的(hash帶#):

oursite.com/#/user/id //如請求,只會發(fā)送http://oursite.com/

所以hash模式下遇到根據(jù)url請求頁面不會有問題

而history模式則將url修改的就和正常請求后端的url一樣(history不帶#)

oursite.com/user/id

如果這種向后端發(fā)送請求的話,后端沒有配置對應(yīng)/user/id的get路由處理,會返回404錯誤。

官方推薦的解決辦法是在服務(wù)端增加一個覆蓋所有情況的候選資源:如果 URL 匹配不到任何靜態(tài)資源,則應(yīng)該返回同一個 index.html 頁面,這個頁面就是你 app 依賴的頁面。同時這么做以后,服務(wù)器就不再返回 404 錯誤頁面,因為對于所有路徑都會返回 index.html 文件。為了避免這種情況,在 Vue 應(yīng)用里面覆蓋所有的路由情況,然后在給出一個 404 頁面?;蛘撸绻怯?Node.js 作后臺,可以使用服務(wù)端的路由來匹配 URL,當(dāng)沒有匹配到路由的時候返回 404,從而實現(xiàn) fallback。

兩種模式比較

一般的需求場景中,hash模式與history模式是差不多的,根據(jù)MDN的介紹,調(diào)用history.pushState()相比于直接修改hash主要有以下優(yōu)勢:

• pushState設(shè)置的新url可以是與當(dāng)前url同源的任意url,而hash只可修改#后面的部分,故只可設(shè)置與當(dāng)前同文檔的url

• pushState設(shè)置的新url可以與當(dāng)前url一模一樣,這樣也會把記錄添加到棧中,而hash設(shè)置的新值必須與原來不一樣才會觸發(fā)記錄添加到棧中

• pushState通過stateObject可以添加任意類型的數(shù)據(jù)記錄中,而hash只可添加短字符串 pushState可額外設(shè)置title屬性供后續(xù)使用

AbstractHistory

'abstract'模式,不涉及和瀏覽器地址的相關(guān)記錄,流程跟'HashHistory'是一樣的,其原理是通過數(shù)組模擬瀏覽器歷史記錄棧的功能

//abstract.js實現(xiàn),這里通過棧的數(shù)據(jù)結(jié)構(gòu)來模擬路由路徑
export class AbstractHistory extends History {
 index: number;
 stack: Array<Route>;

 constructor (router: Router, base: ?string) {
  super(router, base)
  this.stack = []
  this.index = -1
 }
 
 // 對于 go 的模擬
 go (n: number) {
  // 新的歷史記錄位置
  const targetIndex = this.index + n
  // 小于或大于超出則返回
  if (targetIndex < 0 || targetIndex >= this.stack.length) {
   return
  }
  // 取得新的 route 對象
  // 因為是和瀏覽器無關(guān)的 這里得到的一定是已經(jīng)訪問過的
  const route = this.stack[targetIndex]
  // 所以這里直接調(diào)用 confirmTransition 了
  // 而不是調(diào)用 transitionTo 還要走一遍 match 邏輯
  this.confirmTransition(route, () => {
   this.index = targetIndex
   this.updateRoute(route)
  })
 }

//確認(rèn)是否轉(zhuǎn)化路由
 confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
  const current = this.current
  const abort = err => {
   if (isError(err)) {
    if (this.errorCbs.length) {
     this.errorCbs.forEach(cb => { cb(err) })
    } else {
     warn(false, 'uncaught error during route navigation:')
     console.error(err)
    }
   }
   onAbort && onAbort(err)
  }
  //判斷如果前后是同一個路由,不進(jìn)行操作
  if (
   isSameRoute(route, current) &&
   route.matched.length === current.matched.length
  ) {
   this.ensureURL()
   return abort()
  }
  //下面是各類鉤子函數(shù)的處理
  //*********************
  })
 }

看到這里你已經(jīng)對vue-router的路由基本掌握的差不多了,要是喜歡看源碼可以 點擊查 看

要是喜歡可以給我一個star,github

總結(jié)

以上所述是小編給大家介紹的vue-router原理淺析,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對億速云網(wǎng)站的支持!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI