This commit is contained in:
darenhsu
2022-07-17 13:16:16 +08:00
parent 84759556ff
commit befd344ab0
28070 changed files with 4008428 additions and 1 deletions
+18
View File
@@ -0,0 +1,18 @@
/* @flow */
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
+3
View File
@@ -0,0 +1,3 @@
/* @flow */
export const inBrowser = typeof window !== 'undefined'
+69
View File
@@ -0,0 +1,69 @@
/* @flow */
import type VueRouter from '../index'
import { parsePath, resolvePath } from './path'
import { resolveQuery } from './query'
import { fillParams } from './params'
import { warn } from './warn'
import { extend } from './misc'
export function normalizeLocation (
raw: RawLocation,
current: ?Route,
append: ?boolean,
router: ?VueRouter
): Location {
let next: Location = typeof raw === 'string' ? { path: raw } : raw
// named target
if (next._normalized) {
return next
} else if (next.name) {
next = extend({}, raw)
const params = next.params
if (params && typeof params === 'object') {
next.params = extend({}, params)
}
return next
}
// relative params
if (!next.path && next.params && current) {
next = extend({}, next)
next._normalized = true
const params: any = extend(extend({}, current.params), next.params)
if (current.name) {
next.name = current.name
next.params = params
} else if (current.matched.length) {
const rawPath = current.matched[current.matched.length - 1].path
next.path = fillParams(rawPath, params, `path ${current.path}`)
} else if (process.env.NODE_ENV !== 'production') {
warn(false, `relative params navigation requires a current route.`)
}
return next
}
const parsedPath = parsePath(next.path || '')
const basePath = (current && current.path) || '/'
const path = parsedPath.path
? resolvePath(parsedPath.path, basePath, append || next.append)
: basePath
const query = resolveQuery(
parsedPath.query,
next.query,
router && router.options.parseQuery
)
let hash = next.hash || parsedPath.hash
if (hash && hash.charAt(0) !== '#') {
hash = `#${hash}`
}
return {
_normalized: true,
path,
query,
hash
}
}
+6
View File
@@ -0,0 +1,6 @@
export function extend (a, b) {
for (const key in b) {
a[key] = b[key]
}
return a
}
+37
View File
@@ -0,0 +1,37 @@
/* @flow */
import { warn } from './warn'
import Regexp from 'path-to-regexp'
// $flow-disable-line
const regexpCompileCache: {
[key: string]: Function
} = Object.create(null)
export function fillParams (
path: string,
params: ?Object,
routeMsg: string
): string {
params = params || {}
try {
const filler =
regexpCompileCache[path] ||
(regexpCompileCache[path] = Regexp.compile(path))
// Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }}
// and fix #3106 so that you can work with location descriptor object having params.pathMatch equal to empty string
if (typeof params.pathMatch === 'string') params[0] = params.pathMatch
return filler(params, { pretty: true })
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
// Fix #3072 no warn if `pathMatch` is string
warn(typeof params.pathMatch === 'string', `missing param for ${routeMsg}: ${e.message}`)
}
return ''
} finally {
// delete the 0 if it was added
delete params[0]
}
}
+74
View File
@@ -0,0 +1,74 @@
/* @flow */
export function resolvePath (
relative: string,
base: string,
append?: boolean
): string {
const firstChar = relative.charAt(0)
if (firstChar === '/') {
return relative
}
if (firstChar === '?' || firstChar === '#') {
return base + relative
}
const stack = base.split('/')
// remove trailing segment if:
// - not appending
// - appending to trailing slash (last segment is empty)
if (!append || !stack[stack.length - 1]) {
stack.pop()
}
// resolve relative path
const segments = relative.replace(/^\//, '').split('/')
for (let i = 0; i < segments.length; i++) {
const segment = segments[i]
if (segment === '..') {
stack.pop()
} else if (segment !== '.') {
stack.push(segment)
}
}
// ensure leading slash
if (stack[0] !== '') {
stack.unshift('')
}
return stack.join('/')
}
export function parsePath (path: string): {
path: string;
query: string;
hash: string;
} {
let hash = ''
let query = ''
const hashIndex = path.indexOf('#')
if (hashIndex >= 0) {
hash = path.slice(hashIndex)
path = path.slice(0, hashIndex)
}
const queryIndex = path.indexOf('?')
if (queryIndex >= 0) {
query = path.slice(queryIndex + 1)
path = path.slice(0, queryIndex)
}
return {
path,
query,
hash
}
}
export function cleanPath (path: string): string {
return path.replace(/\/\//g, '/')
}
+46
View File
@@ -0,0 +1,46 @@
/* @flow */
import { inBrowser } from './dom'
import { saveScrollPosition } from './scroll'
import { genStateKey, setStateKey, getStateKey } from './state-key'
import { extend } from './misc'
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 && typeof window.history.pushState === 'function'
})()
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
const history = window.history
try {
if (replace) {
// preserve existing history state as it could be overriden by the user
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
+95
View File
@@ -0,0 +1,95 @@
/* @flow */
import { warn } from './warn'
const encodeReserveRE = /[!'()*]/g
const encodeReserveReplacer = c => '%' + c.charCodeAt(0).toString(16)
const commaRE = /%2C/g
// fixed encodeURIComponent which is more conformant to RFC3986:
// - escapes [!'()*]
// - preserve commas
const encode = str => encodeURIComponent(str)
.replace(encodeReserveRE, encodeReserveReplacer)
.replace(commaRE, ',')
const decode = decodeURIComponent
export function resolveQuery (
query: ?string,
extraQuery: Dictionary<string> = {},
_parseQuery: ?Function
): Dictionary<string> {
const parse = _parseQuery || parseQuery
let parsedQuery
try {
parsedQuery = parse(query || '')
} catch (e) {
process.env.NODE_ENV !== 'production' && warn(false, e.message)
parsedQuery = {}
}
for (const key in extraQuery) {
parsedQuery[key] = extraQuery[key]
}
return parsedQuery
}
function parseQuery (query: string): Dictionary<string> {
const res = {}
query = query.trim().replace(/^(\?|#|&)/, '')
if (!query) {
return res
}
query.split('&').forEach(param => {
const parts = param.replace(/\+/g, ' ').split('=')
const key = decode(parts.shift())
const val = parts.length > 0
? decode(parts.join('='))
: null
if (res[key] === undefined) {
res[key] = val
} else if (Array.isArray(res[key])) {
res[key].push(val)
} else {
res[key] = [res[key], val]
}
})
return res
}
export function stringifyQuery (obj: Dictionary<string>): string {
const res = obj ? Object.keys(obj).map(key => {
const val = obj[key]
if (val === undefined) {
return ''
}
if (val === null) {
return encode(key)
}
if (Array.isArray(val)) {
const result = []
val.forEach(val2 => {
if (val2 === undefined) {
return
}
if (val2 === null) {
result.push(encode(key))
} else {
result.push(encode(key) + '=' + encode(val2))
}
})
return result.join('&')
}
return encode(key) + '=' + encode(val)
}).filter(x => x.length > 0).join('&') : null
return res ? `?${res}` : ''
}
+108
View File
@@ -0,0 +1,108 @@
/* @flow */
import { _Vue } from '../install'
import { warn, isError } from './warn'
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
return (to, from, next) => {
let hasAsync = false
let pending = 0
let error = null
flatMapComponents(matched, (def, _, match, key) => {
// if it's a function and doesn't have cid attached,
// assume it's an async component resolve function.
// we are not using Vue's default async resolving mechanism because
// we want to halt the navigation until the incoming component has been
// resolved.
if (typeof def === 'function' && def.cid === undefined) {
hasAsync = true
pending++
const resolve = once(resolvedDef => {
if (isESModule(resolvedDef)) {
resolvedDef = resolvedDef.default
}
// save resolved on async factory in case it's used elsewhere
def.resolved = typeof resolvedDef === 'function'
? resolvedDef
: _Vue.extend(resolvedDef)
match.components[key] = resolvedDef
pending--
if (pending <= 0) {
next()
}
})
const reject = once(reason => {
const msg = `Failed to resolve async component ${key}: ${reason}`
process.env.NODE_ENV !== 'production' && warn(false, msg)
if (!error) {
error = isError(reason)
? reason
: new Error(msg)
next(error)
}
})
let res
try {
res = def(resolve, reject)
} catch (e) {
reject(e)
}
if (res) {
if (typeof res.then === 'function') {
res.then(resolve, reject)
} else {
// new syntax in Vue 2.3
const comp = res.component
if (comp && typeof comp.then === 'function') {
comp.then(resolve, reject)
}
}
}
}
})
if (!hasAsync) next()
}
}
export function flatMapComponents (
matched: Array<RouteRecord>,
fn: Function
): Array<?Function> {
return flatten(matched.map(m => {
return Object.keys(m.components).map(key => fn(
m.components[key],
m.instances[key],
m, key
))
}))
}
export function flatten (arr: Array<any>): Array<any> {
return Array.prototype.concat.apply([], arr)
}
const hasSymbol =
typeof Symbol === 'function' &&
typeof Symbol.toStringTag === 'symbol'
function isESModule (obj) {
return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module')
}
// in Webpack 2, require.ensure now also returns a Promise
// so the resolve/reject functions may get called an extra time
// if the user uses an arrow function shorthand that happens to
// return that Promise.
function once (fn) {
let called = false
return function (...args) {
if (called) return
called = true
return fn.apply(this, args)
}
}
+132
View File
@@ -0,0 +1,132 @@
/* @flow */
import type VueRouter from '../index'
import { stringifyQuery } from './query'
const trailingSlashRE = /\/?$/
export function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery = router && router.options.stringifyQuery
let query: any = location.query || {}
try {
query = clone(query)
} catch (e) {}
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
return Object.freeze(route)
}
function clone (value) {
if (Array.isArray(value)) {
return value.map(clone)
} else if (value && typeof value === 'object') {
const res = {}
for (const key in value) {
res[key] = clone(value[key])
}
return res
} else {
return value
}
}
// the starting route that represents the initial state
export const START = createRoute(null, {
path: '/'
})
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
function getFullPath (
{ path, query = {}, hash = '' },
_stringifyQuery
): string {
const stringify = _stringifyQuery || stringifyQuery
return (path || '/') + stringify(query) + hash
}
export function isSameRoute (a: Route, b: ?Route): boolean {
if (b === START) {
return a === b
} else if (!b) {
return false
} else if (a.path && b.path) {
return (
a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') &&
a.hash === b.hash &&
isObjectEqual(a.query, b.query)
)
} else if (a.name && b.name) {
return (
a.name === b.name &&
a.hash === b.hash &&
isObjectEqual(a.query, b.query) &&
isObjectEqual(a.params, b.params)
)
} else {
return false
}
}
function isObjectEqual (a = {}, b = {}): boolean {
// handle null value #1566
if (!a || !b) return a === b
const aKeys = Object.keys(a)
const bKeys = Object.keys(b)
if (aKeys.length !== bKeys.length) {
return false
}
return aKeys.every(key => {
const aVal = a[key]
const bVal = b[key]
// check nested equality
if (typeof aVal === 'object' && typeof bVal === 'object') {
return isObjectEqual(aVal, bVal)
}
return String(aVal) === String(bVal)
})
}
export function isIncludedRoute (current: Route, target: Route): boolean {
return (
current.path.replace(trailingSlashRE, '/').indexOf(
target.path.replace(trailingSlashRE, '/')
) === 0 &&
(!target.hash || current.hash === target.hash) &&
queryIncludes(current.query, target.query)
)
}
function queryIncludes (current: Dictionary<string>, target: Dictionary<string>): boolean {
for (const key in target) {
if (!(key in current)) {
return false
}
}
return true
}
+165
View File
@@ -0,0 +1,165 @@
/* @flow */
import type Router from '../index'
import { assert } from './warn'
import { getStateKey, setStateKey } from './state-key'
import { extend } from './misc'
const positionStore = Object.create(null)
export function setupScroll () {
// Prevent browser scroll behavior on History popstate
if ('scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual'
}
// Fix for #1585 for Firefox
// Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678
// Fix for #2774 Support for apps loaded from Windows file shares not mapped to network drives: replaced location.origin with
// window.location.protocol + '//' + window.location.host
// location.host contains the port and location.hostname doesn't
const protocolAndPath = window.location.protocol + '//' + window.location.host
const absolutePath = window.location.href.replace(protocolAndPath, '')
// preserve existing history state as it could be overriden by the user
const stateCopy = extend({}, window.history.state)
stateCopy.key = getStateKey()
window.history.replaceState(stateCopy, '', absolutePath)
window.addEventListener('popstate', handlePopState)
return () => {
window.removeEventListener('popstate', handlePopState)
}
}
export function handleScroll (
router: Router,
to: Route,
from: Route,
isPop: boolean
) {
if (!router.app) {
return
}
const behavior = router.options.scrollBehavior
if (!behavior) {
return
}
if (process.env.NODE_ENV !== 'production') {
assert(typeof behavior === 'function', `scrollBehavior must be a function`)
}
// wait until re-render finishes before scrolling
router.app.$nextTick(() => {
const position = getScrollPosition()
const shouldScroll = behavior.call(
router,
to,
from,
isPop ? position : null
)
if (!shouldScroll) {
return
}
if (typeof shouldScroll.then === 'function') {
shouldScroll
.then(shouldScroll => {
scrollToPosition((shouldScroll: any), position)
})
.catch(err => {
if (process.env.NODE_ENV !== 'production') {
assert(false, err.toString())
}
})
} else {
scrollToPosition(shouldScroll, position)
}
})
}
export function saveScrollPosition () {
const key = getStateKey()
if (key) {
positionStore[key] = {
x: window.pageXOffset,
y: window.pageYOffset
}
}
}
function handlePopState (e) {
saveScrollPosition()
if (e.state && e.state.key) {
setStateKey(e.state.key)
}
}
function getScrollPosition (): ?Object {
const key = getStateKey()
if (key) {
return positionStore[key]
}
}
function getElementPosition (el: Element, offset: Object): Object {
const docEl: any = document.documentElement
const docRect = docEl.getBoundingClientRect()
const elRect = el.getBoundingClientRect()
return {
x: elRect.left - docRect.left - offset.x,
y: elRect.top - docRect.top - offset.y
}
}
function isValidPosition (obj: Object): boolean {
return isNumber(obj.x) || isNumber(obj.y)
}
function normalizePosition (obj: Object): Object {
return {
x: isNumber(obj.x) ? obj.x : window.pageXOffset,
y: isNumber(obj.y) ? obj.y : window.pageYOffset
}
}
function normalizeOffset (obj: Object): Object {
return {
x: isNumber(obj.x) ? obj.x : 0,
y: isNumber(obj.y) ? obj.y : 0
}
}
function isNumber (v: any): boolean {
return typeof v === 'number'
}
const hashStartsWithNumberRE = /^#\d/
function scrollToPosition (shouldScroll, position) {
const isObject = typeof shouldScroll === 'object'
if (isObject && typeof shouldScroll.selector === 'string') {
// getElementById would still fail if the selector contains a more complicated query like #main[data-attr]
// but at the same time, it doesn't make much sense to select an element with an id and an extra selector
const el = hashStartsWithNumberRE.test(shouldScroll.selector) // $flow-disable-line
? document.getElementById(shouldScroll.selector.slice(1)) // $flow-disable-line
: document.querySelector(shouldScroll.selector)
if (el) {
let offset =
shouldScroll.offset && typeof shouldScroll.offset === 'object'
? shouldScroll.offset
: {}
offset = normalizeOffset(offset)
position = getElementPosition(el, offset)
} else if (isValidPosition(shouldScroll)) {
position = normalizePosition(shouldScroll)
}
} else if (isObject && isValidPosition(shouldScroll)) {
position = normalizePosition(shouldScroll)
}
if (position) {
window.scrollTo(position.x, position.y)
}
}
+22
View File
@@ -0,0 +1,22 @@
/* @flow */
import { inBrowser } from './dom'
// use User Timing api (if present) for more accurate key precision
const Time =
inBrowser && window.performance && window.performance.now
? window.performance
: Date
export function genStateKey (): string {
return Time.now().toFixed(3)
}
let _key: string = genStateKey()
export function getStateKey () {
return _key
}
export function setStateKey (key: string) {
return (_key = key)
}
+21
View File
@@ -0,0 +1,21 @@
/* @flow */
export function assert (condition: any, message: string) {
if (!condition) {
throw new Error(`[vue-router] ${message}`)
}
}
export function warn (condition: any, message: string) {
if (process.env.NODE_ENV !== 'production' && !condition) {
typeof console !== 'undefined' && console.warn(`[vue-router] ${message}`)
}
}
export function isError (err: any): boolean {
return Object.prototype.toString.call(err).indexOf('Error') > -1
}
export function isRouterError (err: any, errorType: ?string): boolean {
return isError(err) && err._isRouter && (errorType == null || err.type === errorType)
}