utils.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. import Vue from 'vue'
  2. // window.{{globals.loadedCallback}} hook
  3. // Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading)
  4. if (process.client) {
  5. window.onNuxtReadyCbs = []
  6. window.onNuxtReady = (cb) => {
  7. window.onNuxtReadyCbs.push(cb)
  8. }
  9. }
  10. export function empty () {}
  11. export function globalHandleError (error) {
  12. if (Vue.config.errorHandler) {
  13. Vue.config.errorHandler(error)
  14. }
  15. }
  16. export function interopDefault (promise) {
  17. return promise.then(m => m.default || m)
  18. }
  19. export function hasFetch(vm) {
  20. return vm.$options && typeof vm.$options.fetch === 'function' && !vm.$options.fetch.length
  21. }
  22. export function getChildrenComponentInstancesUsingFetch(vm, instances = []) {
  23. const children = vm.$children || []
  24. for (const child of children) {
  25. if (child.$fetch) {
  26. instances.push(child)
  27. continue; // Don't get the children since it will reload the template
  28. }
  29. if (child.$children) {
  30. getChildrenComponentInstancesUsingFetch(child, instances)
  31. }
  32. }
  33. return instances
  34. }
  35. export function applyAsyncData (Component, asyncData) {
  36. if (
  37. // For SSR, we once all this function without second param to just apply asyncData
  38. // Prevent doing this for each SSR request
  39. !asyncData && Component.options.__hasNuxtData
  40. ) {
  41. return
  42. }
  43. const ComponentData = Component.options._originDataFn || Component.options.data || function () { return {} }
  44. Component.options._originDataFn = ComponentData
  45. Component.options.data = function () {
  46. const data = ComponentData.call(this, this)
  47. if (this.$ssrContext) {
  48. asyncData = this.$ssrContext.asyncData[Component.cid]
  49. }
  50. return { ...data, ...asyncData }
  51. }
  52. Component.options.__hasNuxtData = true
  53. if (Component._Ctor && Component._Ctor.options) {
  54. Component._Ctor.options.data = Component.options.data
  55. }
  56. }
  57. export function sanitizeComponent (Component) {
  58. // If Component already sanitized
  59. if (Component.options && Component._Ctor === Component) {
  60. return Component
  61. }
  62. if (!Component.options) {
  63. Component = Vue.extend(Component) // fix issue #6
  64. Component._Ctor = Component
  65. } else {
  66. Component._Ctor = Component
  67. Component.extendOptions = Component.options
  68. }
  69. // If no component name defined, set file path as name, (also fixes #5703)
  70. if (!Component.options.name && Component.options.__file) {
  71. Component.options.name = Component.options.__file
  72. }
  73. return Component
  74. }
  75. export function getMatchedComponents (route, matches = false, prop = 'components') {
  76. return Array.prototype.concat.apply([], route.matched.map((m, index) => {
  77. return Object.keys(m[prop]).map((key) => {
  78. matches && matches.push(index)
  79. return m[prop][key]
  80. })
  81. }))
  82. }
  83. export function getMatchedComponentsInstances (route, matches = false) {
  84. return getMatchedComponents(route, matches, 'instances')
  85. }
  86. export function flatMapComponents (route, fn) {
  87. return Array.prototype.concat.apply([], route.matched.map((m, index) => {
  88. return Object.keys(m.components).reduce((promises, key) => {
  89. if (m.components[key]) {
  90. promises.push(fn(m.components[key], m.instances[key], m, key, index))
  91. } else {
  92. delete m.components[key]
  93. }
  94. return promises
  95. }, [])
  96. }))
  97. }
  98. export function resolveRouteComponents (route, fn) {
  99. return Promise.all(
  100. flatMapComponents(route, async (Component, instance, match, key) => {
  101. // If component is a function, resolve it
  102. if (typeof Component === 'function' && !Component.options) {
  103. Component = await Component()
  104. }
  105. match.components[key] = Component = sanitizeComponent(Component)
  106. return typeof fn === 'function' ? fn(Component, instance, match, key) : Component
  107. })
  108. )
  109. }
  110. export async function getRouteData (route) {
  111. if (!route) {
  112. return
  113. }
  114. // Make sure the components are resolved (code-splitting)
  115. await resolveRouteComponents(route)
  116. // Send back a copy of route with meta based on Component definition
  117. return {
  118. ...route,
  119. meta: getMatchedComponents(route).map((Component, index) => {
  120. return { ...Component.options.meta, ...(route.matched[index] || {}).meta }
  121. })
  122. }
  123. }
  124. export async function setContext (app, context) {
  125. // If context not defined, create it
  126. if (!app.context) {
  127. app.context = {
  128. isStatic: process.static,
  129. isDev: false,
  130. isHMR: false,
  131. app,
  132. store: app.store,
  133. payload: context.payload,
  134. error: context.error,
  135. base: '/',
  136. env: {"NODE_ENV":"production","baseUrl":"https://www.proginn.com","jishuBaseUrl":"https://jishuin.proginn.com"}
  137. }
  138. // Only set once
  139. if (!process.static && context.req) {
  140. app.context.req = context.req
  141. }
  142. if (!process.static && context.res) {
  143. app.context.res = context.res
  144. }
  145. if (context.ssrContext) {
  146. app.context.ssrContext = context.ssrContext
  147. }
  148. app.context.redirect = (status, path, query) => {
  149. if (!status) {
  150. return
  151. }
  152. app.context._redirected = true
  153. // if only 1 or 2 arguments: redirect('/') or redirect('/', { foo: 'bar' })
  154. let pathType = typeof path
  155. if (typeof status !== 'number' && (pathType === 'undefined' || pathType === 'object')) {
  156. query = path || {}
  157. path = status
  158. pathType = typeof path
  159. status = 302
  160. }
  161. if (pathType === 'object') {
  162. path = app.router.resolve(path).route.fullPath
  163. }
  164. // "/absolute/route", "./relative/route" or "../relative/route"
  165. if (/(^[.]{1,2}\/)|(^\/(?!\/))/.test(path)) {
  166. app.context.next({
  167. path,
  168. query,
  169. status
  170. })
  171. } else {
  172. path = formatUrl(path, query)
  173. if (process.server) {
  174. app.context.next({
  175. path,
  176. status
  177. })
  178. }
  179. if (process.client) {
  180. // https://developer.mozilla.org/en-US/docs/Web/API/Location/replace
  181. window.location.replace(path)
  182. // Throw a redirect error
  183. throw new Error('ERR_REDIRECT')
  184. }
  185. }
  186. }
  187. if (process.server) {
  188. app.context.beforeNuxtRender = fn => context.beforeRenderFns.push(fn)
  189. }
  190. if (process.client) {
  191. app.context.nuxtState = window.__NUXT__
  192. }
  193. }
  194. // Dynamic keys
  195. const [currentRouteData, fromRouteData] = await Promise.all([
  196. getRouteData(context.route),
  197. getRouteData(context.from)
  198. ])
  199. if (context.route) {
  200. app.context.route = currentRouteData
  201. }
  202. if (context.from) {
  203. app.context.from = fromRouteData
  204. }
  205. app.context.next = context.next
  206. app.context._redirected = false
  207. app.context._errored = false
  208. app.context.isHMR = false
  209. app.context.params = app.context.route.params || {}
  210. app.context.query = app.context.route.query || {}
  211. }
  212. export function middlewareSeries (promises, appContext) {
  213. if (!promises.length || appContext._redirected || appContext._errored) {
  214. return Promise.resolve()
  215. }
  216. return promisify(promises[0], appContext)
  217. .then(() => {
  218. return middlewareSeries(promises.slice(1), appContext)
  219. })
  220. }
  221. export function promisify (fn, context) {
  222. let promise
  223. if (fn.length === 2) {
  224. // fn(context, callback)
  225. promise = new Promise((resolve) => {
  226. fn(context, function (err, data) {
  227. if (err) {
  228. context.error(err)
  229. }
  230. data = data || {}
  231. resolve(data)
  232. })
  233. })
  234. } else {
  235. promise = fn(context)
  236. }
  237. if (promise && promise instanceof Promise && typeof promise.then === 'function') {
  238. return promise
  239. }
  240. return Promise.resolve(promise)
  241. }
  242. // Imported from vue-router
  243. export function getLocation (base, mode) {
  244. let path = decodeURI(window.location.pathname)
  245. if (mode === 'hash') {
  246. return window.location.hash.replace(/^#\//, '')
  247. }
  248. // To get matched with sanitized router.base add trailing slash
  249. if (base && (path.endsWith('/') ? path : path + '/').startsWith(base)) {
  250. path = path.slice(base.length)
  251. }
  252. return (path || '/') + window.location.search + window.location.hash
  253. }
  254. // Imported from path-to-regexp
  255. /**
  256. * Compile a string to a template function for the path.
  257. *
  258. * @param {string} str
  259. * @param {Object=} options
  260. * @return {!function(Object=, Object=)}
  261. */
  262. export function compile (str, options) {
  263. return tokensToFunction(parse(str, options), options)
  264. }
  265. export function getQueryDiff (toQuery, fromQuery) {
  266. const diff = {}
  267. const queries = { ...toQuery, ...fromQuery }
  268. for (const k in queries) {
  269. if (String(toQuery[k]) !== String(fromQuery[k])) {
  270. diff[k] = true
  271. }
  272. }
  273. return diff
  274. }
  275. export function normalizeError (err) {
  276. let message
  277. if (!(err.message || typeof err === 'string')) {
  278. try {
  279. message = JSON.stringify(err, null, 2)
  280. } catch (e) {
  281. message = `[${err.constructor.name}]`
  282. }
  283. } else {
  284. message = err.message || err
  285. }
  286. return {
  287. ...err,
  288. message,
  289. statusCode: (err.statusCode || err.status || (err.response && err.response.status) || 500)
  290. }
  291. }
  292. /**
  293. * The main path matching regexp utility.
  294. *
  295. * @type {RegExp}
  296. */
  297. const PATH_REGEXP = new RegExp([
  298. // Match escaped characters that would otherwise appear in future matches.
  299. // This allows the user to escape special characters that won't transform.
  300. '(\\\\.)',
  301. // Match Express-style parameters and un-named parameters with a prefix
  302. // and optional suffixes. Matches appear as:
  303. //
  304. // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined]
  305. // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined]
  306. // "/*" => ["/", undefined, undefined, undefined, undefined, "*"]
  307. '([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))'
  308. ].join('|'), 'g')
  309. /**
  310. * Parse a string for the raw tokens.
  311. *
  312. * @param {string} str
  313. * @param {Object=} options
  314. * @return {!Array}
  315. */
  316. function parse (str, options) {
  317. const tokens = []
  318. let key = 0
  319. let index = 0
  320. let path = ''
  321. const defaultDelimiter = (options && options.delimiter) || '/'
  322. let res
  323. while ((res = PATH_REGEXP.exec(str)) != null) {
  324. const m = res[0]
  325. const escaped = res[1]
  326. const offset = res.index
  327. path += str.slice(index, offset)
  328. index = offset + m.length
  329. // Ignore already escaped sequences.
  330. if (escaped) {
  331. path += escaped[1]
  332. continue
  333. }
  334. const next = str[index]
  335. const prefix = res[2]
  336. const name = res[3]
  337. const capture = res[4]
  338. const group = res[5]
  339. const modifier = res[6]
  340. const asterisk = res[7]
  341. // Push the current path onto the tokens.
  342. if (path) {
  343. tokens.push(path)
  344. path = ''
  345. }
  346. const partial = prefix != null && next != null && next !== prefix
  347. const repeat = modifier === '+' || modifier === '*'
  348. const optional = modifier === '?' || modifier === '*'
  349. const delimiter = res[2] || defaultDelimiter
  350. const pattern = capture || group
  351. tokens.push({
  352. name: name || key++,
  353. prefix: prefix || '',
  354. delimiter,
  355. optional,
  356. repeat,
  357. partial,
  358. asterisk: Boolean(asterisk),
  359. pattern: pattern ? escapeGroup(pattern) : (asterisk ? '.*' : '[^' + escapeString(delimiter) + ']+?')
  360. })
  361. }
  362. // Match any characters still remaining.
  363. if (index < str.length) {
  364. path += str.substr(index)
  365. }
  366. // If the path exists, push it onto the end.
  367. if (path) {
  368. tokens.push(path)
  369. }
  370. return tokens
  371. }
  372. /**
  373. * Prettier encoding of URI path segments.
  374. *
  375. * @param {string}
  376. * @return {string}
  377. */
  378. function encodeURIComponentPretty (str, slashAllowed) {
  379. const re = slashAllowed ? /[?#]/g : /[/?#]/g
  380. return encodeURI(str).replace(re, (c) => {
  381. return '%' + c.charCodeAt(0).toString(16).toUpperCase()
  382. })
  383. }
  384. /**
  385. * Encode the asterisk parameter. Similar to `pretty`, but allows slashes.
  386. *
  387. * @param {string}
  388. * @return {string}
  389. */
  390. function encodeAsterisk (str) {
  391. return encodeURIComponentPretty(str, true)
  392. }
  393. /**
  394. * Escape a regular expression string.
  395. *
  396. * @param {string} str
  397. * @return {string}
  398. */
  399. function escapeString (str) {
  400. return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
  401. }
  402. /**
  403. * Escape the capturing group by escaping special characters and meaning.
  404. *
  405. * @param {string} group
  406. * @return {string}
  407. */
  408. function escapeGroup (group) {
  409. return group.replace(/([=!:$/()])/g, '\\$1')
  410. }
  411. /**
  412. * Expose a method for transforming tokens into the path function.
  413. */
  414. function tokensToFunction (tokens, options) {
  415. // Compile all the tokens into regexps.
  416. const matches = new Array(tokens.length)
  417. // Compile all the patterns before compilation.
  418. for (let i = 0; i < tokens.length; i++) {
  419. if (typeof tokens[i] === 'object') {
  420. matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$', flags(options))
  421. }
  422. }
  423. return function (obj, opts) {
  424. let path = ''
  425. const data = obj || {}
  426. const options = opts || {}
  427. const encode = options.pretty ? encodeURIComponentPretty : encodeURIComponent
  428. for (let i = 0; i < tokens.length; i++) {
  429. const token = tokens[i]
  430. if (typeof token === 'string') {
  431. path += token
  432. continue
  433. }
  434. const value = data[token.name || 'pathMatch']
  435. let segment
  436. if (value == null) {
  437. if (token.optional) {
  438. // Prepend partial segment prefixes.
  439. if (token.partial) {
  440. path += token.prefix
  441. }
  442. continue
  443. } else {
  444. throw new TypeError('Expected "' + token.name + '" to be defined')
  445. }
  446. }
  447. if (Array.isArray(value)) {
  448. if (!token.repeat) {
  449. throw new TypeError('Expected "' + token.name + '" to not repeat, but received `' + JSON.stringify(value) + '`')
  450. }
  451. if (value.length === 0) {
  452. if (token.optional) {
  453. continue
  454. } else {
  455. throw new TypeError('Expected "' + token.name + '" to not be empty')
  456. }
  457. }
  458. for (let j = 0; j < value.length; j++) {
  459. segment = encode(value[j])
  460. if (!matches[i].test(segment)) {
  461. throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received `' + JSON.stringify(segment) + '`')
  462. }
  463. path += (j === 0 ? token.prefix : token.delimiter) + segment
  464. }
  465. continue
  466. }
  467. segment = token.asterisk ? encodeAsterisk(value) : encode(value)
  468. if (!matches[i].test(segment)) {
  469. throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
  470. }
  471. path += token.prefix + segment
  472. }
  473. return path
  474. }
  475. }
  476. /**
  477. * Get the flags for a regexp from the options.
  478. *
  479. * @param {Object} options
  480. * @return {string}
  481. */
  482. function flags (options) {
  483. return options && options.sensitive ? '' : 'i'
  484. }
  485. /**
  486. * Format given url, append query to url query string
  487. *
  488. * @param {string} url
  489. * @param {string} query
  490. * @return {string}
  491. */
  492. function formatUrl (url, query) {
  493. let protocol
  494. const index = url.indexOf('://')
  495. if (index !== -1) {
  496. protocol = url.substring(0, index)
  497. url = url.substring(index + 3)
  498. } else if (url.startsWith('//')) {
  499. url = url.substring(2)
  500. }
  501. let parts = url.split('/')
  502. let result = (protocol ? protocol + '://' : '//') + parts.shift()
  503. let path = parts.join('/')
  504. if (path === '' && parts.length === 1) {
  505. result += '/'
  506. }
  507. let hash
  508. parts = path.split('#')
  509. if (parts.length === 2) {
  510. [path, hash] = parts
  511. }
  512. result += path ? '/' + path : ''
  513. if (query && JSON.stringify(query) !== '{}') {
  514. result += (url.split('?').length === 2 ? '&' : '?') + formatQuery(query)
  515. }
  516. result += hash ? '#' + hash : ''
  517. return result
  518. }
  519. /**
  520. * Transform data object to query string
  521. *
  522. * @param {object} query
  523. * @return {string}
  524. */
  525. function formatQuery (query) {
  526. return Object.keys(query).sort().map((key) => {
  527. const val = query[key]
  528. if (val == null) {
  529. return ''
  530. }
  531. if (Array.isArray(val)) {
  532. return val.slice().map(val2 => [key, '=', val2].join('')).join('&')
  533. }
  534. return key + '=' + val
  535. }).filter(Boolean).join('&')
  536. }
  537. export function addLifecycleHook(vm, hook, fn) {
  538. if (!vm.$options[hook]) {
  539. vm.$options[hook] = []
  540. }
  541. if (!vm.$options[hook].includes(fn)) {
  542. vm.$options[hook].push(fn)
  543. }
  544. }
  545. export function urlJoin () {
  546. return [].slice
  547. .call(arguments)
  548. .join('/')
  549. .replace(/\/+/g, '/')
  550. .replace(':/', '://')
  551. }
  552. export function stripTrailingSlash (path) {
  553. return path.replace(/\/+$/, '') || '/'
  554. }
  555. export function isSamePath (p1, p2) {
  556. return stripTrailingSlash(p1) === stripTrailingSlash(p2)
  557. }