Vue路由History模式分析
Vue路由History模式分析
描述
Vue-router
的hash
模式使用URL
的Hash
來模擬一個(gè)完整的URL
,當(dāng)URL
改變時(shí)頁面不會(huì)重新加載,而Vue-router
的history
模式是充分利用history.pushState
的API
來完成URL
跳轉(zhuǎn),同樣在頁面跳轉(zhuǎn)時(shí)無須重新加載頁面,當(dāng)然也不會(huì)對(duì)于服務(wù)端進(jìn)行請(qǐng)求,當(dāng)然對(duì)于history
模式仍然是需要后端的配置支持,由于應(yīng)用是個(gè)單頁客戶端應(yīng)用,如果后臺(tái)沒有正確的配置,當(dāng)用戶在瀏覽器直接訪問URL
時(shí)就會(huì)返回404
,所以需要在服務(wù)端增加一個(gè)覆蓋所有情況的候選資源,如果URL
匹配不到任何靜態(tài)資源時(shí),則應(yīng)該返回同一個(gè)index.html
應(yīng)用依賴頁面,例如在Nginx
下的配置。
location / { try_files $uri $uri/ /index.html; }
分析
Vue-router
源碼的實(shí)現(xiàn)比較復(fù)雜,會(huì)處理各種兼容問題與異常以及各種條件分支,文章分析比較核心的代碼部分,精簡(jiǎn)過后的版本,重要部分做出注釋,commit id
為560d11d
。
首先是在定義Router
時(shí)調(diào)用Vue.use(VueRouter)
,這是Vue.js
插件的經(jīng)典寫法,給插件對(duì)象增加install
方法用來安裝插件具體邏輯,此時(shí)會(huì)調(diào)用VueRouter
類上的靜態(tài)方法,即VueRouter.install = install
,install
模塊主要是保證Vue-router
只被use
一次,以及通過mixin
在Vue
的生命周期beforeCreate
內(nèi)注冊(cè)實(shí)例,在destroyed
內(nèi)銷毀實(shí)例,還有定義$router
與$route
屬性為只讀屬性以及<router-view>
與<router-link>
全局組件的注冊(cè)。
// dev/src/install.js line 6 export function install (Vue) { if (install.installed && _Vue === Vue) return install.installed = true // 保證 Vue-router 只被 use 一次 _Vue = Vue const isDef = v => v !== undefined const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { i(vm, callVal) } } Vue.mixin({ beforeCreate () { // 注冊(cè)實(shí)例 if (isDef(this.$options.router)) { // this.$options.router 來自于 VueRouter 的實(shí)例化 // 判斷實(shí)例是否已經(jīng)掛載 this._routerRoot = this this._router = this.$options.router this._router.init(this) // // 調(diào)用 VueRouter 的 init 方法 // 后文會(huì)說明 init 方法的作用 Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this // 將組件的 _routerRoot 都指向根 Vue 實(shí)例 } registerInstance(this, this) }, destroyed () { // 銷毀實(shí)例 即掛載undefined registerInstance(this) } }) Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) Vue.component('RouterView', View) // 注冊(cè)全局組件 <router-view> Vue.component('RouterLink', Link) // 注冊(cè)全局組件 <router-link> const strats = Vue.config.optionMergeStrategies // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created }
之后是VueRouter
對(duì)象的構(gòu)造函數(shù),主要是先獲取mode
的值,如果mode
的值為history
但是瀏覽器不支持history
模式,那么就強(qiáng)制設(shè)置mode
值為hash
,接下來根據(jù)mode
的值,來選擇vue-router
使用哪種模式。
// dev/src/index.js line 40 constructor (options: RouterOptions = {}) { this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] this.matcher = createMatcher(options.routes || [], this) // 創(chuàng)建路由匹配對(duì)象 let mode = options.mode || 'hash' this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false // 檢車兼容 if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' } this.mode = mode switch (mode) { case 'history': this.history = new HTML5History(this, options.base) // 實(shí)例化history模式 break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) // 實(shí)例化Hash模式 break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } }
在構(gòu)造函數(shù)中調(diào)用了創(chuàng)建路由匹配對(duì)象的方法createMatcher
,而在createMatcher
中又調(diào)用了實(shí)際用以創(chuàng)建路由映射表的方法createRouteMap
,可以說createMatcher
函數(shù)的作用就是創(chuàng)建路由映射表,然后通過閉包的方式讓addRoutes
和match
函數(shù)能夠使用路由映射表的幾個(gè)對(duì)象,最后返回一個(gè)Matcher
對(duì)象。
// dev/src/create-matcher.js line 16 export function createMatcher ( routes: Array<RouteConfig>, router: VueRouter ): Matcher { const { pathList, pathMap, nameMap } = createRouteMap(routes) // 創(chuàng)建路由映射表 function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) } function match ( // 路由匹配 raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { const location = normalizeLocation(raw, currentRoute, false, router) // location 是一個(gè)對(duì)象,類似于 {"_normalized":true,"path":"/","query":{},"hash":""} const { name } = location if (name) { // 如果有路由名稱 就進(jìn)行nameMap映射 const record = nameMap[name] // nameMap[name] = 路由記錄 if (process.env.NODE_ENV !== 'production') { warn(record, `Route with name '${name}' does not exist`) } if (!record) return _createRoute(null, location) const paramNames = record.regex.keys .filter(key => !key.optional) .map(key => key.name) if (typeof location.params !== 'object') { location.params = {} } if (currentRoute && typeof currentRoute.params === 'object') { for (const key in currentRoute.params) { if (!(key in location.params) && paramNames.indexOf(key) > -1) { location.params[key] = currentRoute.params[key] } } } location.path = fillParams(record.path, location.params, `named route "${name}"`) return _createRoute(record, location, redirectedFrom) } else if (location.path) { // 如果路由配置了path,到pathList和PathMap里匹配到路由記錄 location.params = {} for (let i = 0; i < pathList.length; i++) { const path = pathList[i] const record = pathMap[path] if (matchRoute(record.regex, location.path, location.params)) { return _createRoute(record, location, redirectedFrom) } } } // no match return _createRoute(null, location) } function redirect ( // 處理重定向 record: RouteRecord, location: Location ): Route { const originalRedirect = record.redirect let redirect = typeof originalRedirect === 'function' ? originalRedirect(createRoute(record, location, null, router)) : originalRedirect if (typeof redirect === 'string') { redirect = { path: redirect } } if (!redirect || typeof redirect !== 'object') { if (process.env.NODE_ENV !== 'production') { warn( false, `invalid redirect option: ${JSON.stringify(redirect)}` ) } return _createRoute(null, location) } const re: Object = redirect const { name, path } = re let { query, hash, params } = location query = re.hasOwnProperty('query') ? re.query : query hash = re.hasOwnProperty('hash') ? re.hash : hash params = re.hasOwnProperty('params') ? re.params : params if (name) { // resolved named direct const targetRecord = nameMap[name] if (process.env.NODE_ENV !== 'production') { assert(targetRecord, `redirect failed: named route "${name}" not found.`) } return match({ _normalized: true, name, query, hash, params }, undefined, location) } else if (path) { // 1. resolve relative redirect const rawPath = resolveRecordPath(path, record) // 2. resolve params const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`) // 3. rematch with existing query and hash return match({ _normalized: true, path: resolvedPath, query, hash }, undefined, location) } else { if (process.env.NODE_ENV !== 'production') { warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`) } return _createRoute(null, location) } } function alias ( // 處理別名 record: RouteRecord, location: Location, matchAs: string ): Route { const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`) const aliasedMatch = match({ _normalized: true, path: aliasedPath }) if (aliasedMatch) { const matched = aliasedMatch.matched const aliasedRecord = matched[matched.length - 1] location.params = aliasedMatch.params return _createRoute(aliasedRecord, location) } return _createRoute(null, location) } function _createRoute ( // 創(chuàng)建路由 record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route { if (record && record.redirect) { return redirect(record, redirectedFrom || location) } if (record && record.matchAs) { return alias(record, location, record.matchAs) } return createRoute(record, location, redirectedFrom, router) // 創(chuàng)建路由對(duì)象 } return { match, addRoutes } } // dev/src/create-route-map.js line 7 export function createRouteMap ( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord> ): { pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord> } { // the path list is used to control path matching priority const pathList: Array<string> = oldPathList || [] // 創(chuàng)建映射表 // $flow-disable-line const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null) // $flow-disable-line const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null) routes.forEach(route => { // 遍歷路由配置,為每個(gè)配置添加路由記錄 addRouteRecord(pathList, pathMap, nameMap, route) }) // ensure wildcard routes are always at the end for (let i = 0, l = pathList.length; i < l; i++) { // 確保通配符在最后 if (pathList[i] === '*') { pathList.push(pathList.splice(i, 1)[0]) l-- i-- } } if (process.env.NODE_ENV === 'development') { // warn if routes do not include leading slashes const found = pathList // check for missing leading slash .filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/') if (found.length > 0) { const pathNames = found.map(path => `- ${path}`).join('\n') warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`) } } return { pathList, pathMap, nameMap } } function addRouteRecord ( // 添加路由記錄 pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) { const { path, name } = route // 獲得路由配置下的屬性 if (process.env.NODE_ENV !== 'production') { assert(path != null, `"path" is required in a route configuration.`) assert( typeof route.component !== 'string', `route config "component" for path: ${String( path || name )} cannot be a ` + `string id. Use an actual component instead.` ) } const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {} const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict) if (typeof route.caseSensitive === 'boolean') { pathToRegexpOptions.sensitive = route.caseSensitive } const record: RouteRecord = { // 生成記錄對(duì)象 path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), components: route.components || { default: route.component }, instances: {}, name, parent, matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : { default: route.props } } if (route.children) { // Warn if route is named, does not redirect and has a default child route. // If users navigate to this route by name, the default child will // not be rendered (GH Issue #629) if (process.env.NODE_ENV !== 'production') { if ( route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path)) ) { warn( false, `Named Route '${route.name}' has a default child route. ` + `When navigating to this named route (:to="{name: '${ route.name }'"), ` + `the default child route will not be rendered. Remove the name from ` + `this route and use the name of the default child route for named ` + `links instead.` ) } } route.children.forEach(child => { // 遞歸路由配置的 children 屬性,添加路由記錄 const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) }) } if (!pathMap[record.path]) { // 如果有多個(gè)相同的路徑,只有第一個(gè)起作用,后面的會(huì)被忽略 pathList.push(record.path) pathMap[record.path] = record } if (route.alias !== undefined) { // 如果路由有別名的話,給別名也添加路由記錄 const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] for (let i = 0; i < aliases.length; ++i) { const alias = aliases[i] if (process.env.NODE_ENV !== 'production' && alias === path) { warn( false, `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.` ) // skip in dev to make it work continue } const aliasRoute = { path: alias, children: route.children } addRouteRecord( pathList, pathMap, nameMap, aliasRoute, parent, record.path || '/' // matchAs ) } } if (name) { if (!nameMap[name]) { nameMap[name] = record } else if (process.env.NODE_ENV !== 'production' && !matchAs) { warn( false, `Duplicate named routes definition: ` + `{ name: "${name}", path: "${record.path}" }` ) } } }
在上文的構(gòu)造函數(shù)中實(shí)例化的HTML5History
對(duì)象就是對(duì)于history
模式下的路由的處理,主要是通過繼承History
對(duì)象以及自身實(shí)現(xiàn)的方法完成路由。在初始化VueRouter
時(shí)調(diào)用的init
方法調(diào)用了路由切換以及調(diào)用了setupListeners
方法實(shí)現(xiàn)了路由的切換的監(jiān)聽回調(diào),注意此時(shí)并沒有在HTML5History
對(duì)象的構(gòu)造函數(shù)中直接添加事件監(jiān)聽,這是因?yàn)樾枰苊庠谀承g覽器中調(diào)度第一個(gè)popstate
事件,但是由于異步保護(hù),第一個(gè)歷史記錄路由未同時(shí)更新的問題。history
模式的代碼結(jié)構(gòu)以及更新視圖的邏輯與hash
模式基本類似,主要是監(jiān)聽popstate
事件以及對(duì)于push()
和replace()
方法的變動(dòng),使用History
對(duì)象的pushState()
與replaceState()
等方法進(jìn)行路由的變換。
// dev/src/index.js line 21 export default class VueRouter { //... 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) // set up app destroyed handler // https://github.com/vuejs/vue-router/issues/2639 app.$once('hook:destroyed', () => { // clean out app from this.apps array once destroyed const index = this.apps.indexOf(app) if (index > -1) this.apps.splice(index, 1) // ensure we still have a main app or null if no apps // we do not release the router so it can be reused if (this.app === app) this.app = this.apps[0] || null if (!this.app) this.history.teardown() }) // main app previously initialized // return as we don't need to set up new history listener if (this.app) { return } this.app = app const history = this.history if (history instanceof HTML5History || history instanceof HashHistory) { const handleInitialScroll = routeOrError => { const from = history.current const expectScroll = this.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll && 'fullPath' in routeOrError) { handleScroll(this, routeOrError, from, false) } } const setupListeners = routeOrError => { history.setupListeners() // 初始化添加事件監(jiān)聽 handleInitialScroll(routeOrError) } history.transitionTo( // 如果默認(rèn)頁,需要根據(jù)當(dāng)前瀏覽器地址欄里的 path 或者 hash 來激活對(duì)應(yīng)的路由 history.getCurrentLocation(), setupListeners, setupListeners ) } history.listen(route => { this.apps.forEach(app => { app._route = route }) }) } //... } // dev/src/history/base.js line 24 export class History { // ... transitionTo ( location: RawLocation, onComplete?: Function, onAbort?: Function ) { let route // catch redirect option https://github.com/vuejs/vue-router/issues/3201 try { route = this.router.match(location, this.current) // // 獲取匹配的路由信息 } catch (e) { this.errorCbs.forEach(cb => { cb(e) }) // Exception should still be thrown throw e } const prev = this.current this.confirmTransition( // 確認(rèn)跳轉(zhuǎn) route, () => { this.updateRoute(route) // 更新當(dāng)前 route 對(duì)象 onComplete && onComplete(route) this.ensureURL() // 子類實(shí)現(xiàn)的更新url地址 對(duì)于 hash 模式的話 就是更新 hash 的值 this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) // 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) { // Initial redirection should not mark the history as ready yet // because it's triggered by the redirection instead // https://github.com/vuejs/vue-router/issues/3225 // https://github.com/vuejs/vue-router/issues/3331 if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) { this.ready = true this.readyErrorCbs.forEach(cb => { cb(err) }) } } } ) } confirmTransition (route: Route, onComplete: Function, onAbort?: Function) { const current = this.current this.pending = route const abort = err => { // changed after adding errors with // https://github.com/vuejs/vue-router/pull/3047 before that change, // redirect and aborted navigation would produce an err == null if (!isNavigationFailure(err) && 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) } const lastRouteIndex = route.matched.length - 1 const lastCurrentIndex = current.matched.length - 1 if ( isSameRoute(route, current) && // 如果是相同的路由就不跳轉(zhuǎn) // in the case the route map has been dynamically appended to lastRouteIndex === lastCurrentIndex && route.matched[lastRouteIndex] === current.matched[lastCurrentIndex] ) { this.ensureURL() return abort(createNavigationDuplicatedError(current, route)) } const { updated, deactivated, activated } = resolveQueue( // 通過對(duì)比路由解析出可復(fù)用的組件,需要渲染的組件,失活的組件 this.current.matched, route.matched ) const queue: Array<?NavigationGuard> = [].concat( // 導(dǎo)航守衛(wèi)數(shù)組 // in-component leave guards extractLeaveGuards(deactivated), // 失活的組件鉤子 // global before hooks this.router.beforeHooks, // 全局 beforeEach 鉤子 // in-component update hooks extractUpdateHooks(updated), // 在當(dāng)前路由改變,但是該組件被復(fù)用時(shí)調(diào)用 // in-config enter guards activated.map(m => m.beforeEnter), // 需要渲染組件 enter 守衛(wèi)鉤子 // async components resolveAsyncComponents(activated) // 解析異步路由組件 ) const iterator = (hook: NavigationGuard, next) => { if (this.pending !== route) { // 路由不相等就不跳轉(zhuǎn)路由 return abort(createNavigationCancelledError(current, route)) } try { hook(route, current, (to: any) => { // 只有執(zhí)行了鉤子函數(shù)中的next,才會(huì)繼續(xù)執(zhí)行下一個(gè)鉤子函數(shù),否則會(huì)暫停跳轉(zhuǎn),以下邏輯是在判斷 next() 中的傳參 if (to === false) { // next(false) -> abort navigation, ensure current URL this.ensureURL(true) abort(createNavigationAbortedError(current, route)) } else if (isError(to)) { this.ensureURL(true) abort(to) } else if ( typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string')) ) { // next('/') or next({ path: '/' }) -> redirect abort(createNavigationRedirectedError(current, route)) if (typeof to === 'object' && to.replace) { this.replace(to) } else { this.push(to) } } else { // confirm transition and pass on the value next(to) } }) } catch (e) { abort(e) } } // ... } // ... } // dev/src/history/html5.js line 10 export class HTML5History extends History { _startLocation: string constructor (router: Router, base: ?string) { super(router, base) this._startLocation = getLocation(this.base) } setupListeners () { // 初始化 if (this.listeners.length > 0) { return } const router = this.router const expectScroll = router.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { this.listeners.push(setupScroll()) } const handleRoutingEvent = () => { 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. const location = getLocation(this.base) if (this.current === START && location === this._startLocation) { return } this.transitionTo(location, route => { if (supportsScroll) { handleScroll(router, route, current, true) } }) } window.addEventListener('popstate', handleRoutingEvent) // 事件監(jiān)聽 this.listeners.push(() => { window.removeEventListener('popstate', handleRoutingEvent) }) } go (n: number) { window.history.go(n) } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { pushState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { replaceState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } ensureURL (push?: boolean) { if (getLocation(this.base) !== this.current.fullPath) { const current = cleanPath(this.base + this.current.fullPath) push ? pushState(current) : replaceState(current) } } getCurrentLocation (): string { return getLocation(this.base) } }
以上就是Vue路由History模式分析的詳細(xì)內(nèi)容,更多關(guān)于Vue路由History模式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue使用Proxy監(jiān)聽所有接口狀態(tài)的方法實(shí)現(xiàn)
這篇文章主要介紹了Vue使用Proxy監(jiān)聽所有接口狀態(tài)的方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06vue3+vant4實(shí)現(xiàn)pdf文件上傳與預(yù)覽組件
這篇文章主要介紹了vue3如何結(jié)合vant4實(shí)現(xiàn)簡(jiǎn)單的pdf文件上傳與預(yù)覽組件,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-04-04vue3中如何用threejs畫一些簡(jiǎn)單的幾何體
最近學(xué)習(xí)threejs有些時(shí)間了,就想著著手做些東西,下面這篇文章主要給大家介紹了關(guān)于vue3中如何用threejs畫一些簡(jiǎn)單的幾何體的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-03-03vue 登錄滑動(dòng)驗(yàn)證實(shí)現(xiàn)代碼
這篇文章主要介紹了vue 登錄滑動(dòng)驗(yàn)證實(shí)現(xiàn)代碼,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-08-08vue3導(dǎo)入excel并解析excel數(shù)據(jù)渲染到表格中(純前端實(shí)現(xiàn))
在Vue中實(shí)現(xiàn)導(dǎo)出Excel有多種方式,可以通過前端實(shí)現(xiàn),也可以通過前后端配合實(shí)現(xiàn),下面這篇文章主要給大家介紹了關(guān)于vue3導(dǎo)入excel并解析excel數(shù)據(jù)渲染到表格中的相關(guān)資料,文中介紹的方法是純前端實(shí)現(xiàn),需要的朋友可以參考下2024-04-04elementUI多選框反選的實(shí)現(xiàn)代碼
這篇文章主要介紹了elementUI多選框反選的實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04解決Vue中使用Echarts出現(xiàn)There?is?a?chart?instance?already?ini
使用echarts的時(shí)候,多次加載會(huì)出現(xiàn)There?is?a?chart?instance?already?initialized?on?the?dom.這個(gè)黃色警告,此警告信息不影響echarts正常加載,但是有bug得解決,本文就帶大家解決這個(gè)問題,感興趣的同學(xué)可以參考閱讀2023-06-06vue實(shí)現(xiàn)搜索并高亮文字的兩種方式總結(jié)
在做文字處理的項(xiàng)目時(shí)經(jīng)常會(huì)遇到搜索文字并高亮的需求,常見的實(shí)現(xiàn)方式有插入標(biāo)簽和貼標(biāo)簽兩種,這兩種方式適用于不同的場(chǎng)景,各有優(yōu)劣,下面我們就來看看他們的具體實(shí)現(xiàn)吧2023-11-11