router.js 5.9 KB
Newer Older
N
nkzawa 已提交
1 2 3 4 5
import { parse } from 'url'
import evalScript from './eval-script'
import shallowEquals from './shallow-equals'

export default class Router {
N
nkzawa 已提交
6
  constructor (url, initialData) {
D
Dan Zajdband 已提交
7
    const parsed = parse(url, true)
N
nkzawa 已提交
8

N
nkzawa 已提交
9
    // represents the current component key
N
nkzawa 已提交
10
    this.route = toRoute(parsed.pathname)
N
nkzawa 已提交
11 12

    // set up the component cache (by route keys)
N
nkzawa 已提交
13
    this.components = { [this.route]: initialData }
N
nkzawa 已提交
14

N
nkzawa 已提交
15 16
    this.pathname = parsed.pathname
    this.query = parsed.query
N
nkzawa 已提交
17 18
    this.subscriptions = new Set()
    this.componentLoadCancel = null
N
nkzawa 已提交
19
    this.onPopState = this.onPopState.bind(this)
N
nkzawa 已提交
20

D
Dan Zajdband 已提交
21
    if (typeof window !== 'undefined') {
N
nkzawa 已提交
22 23
      window.addEventListener('popstate', this.onPopState)
    }
N
nkzawa 已提交
24 25 26 27
  }

  onPopState (e) {
    this.abortComponentLoad()
N
nkzawa 已提交
28

29
    const { pathname, query } = parse(window.location.href, true)
30
    const route = (e.state || {}).route || toRoute(pathname)
N
nkzawa 已提交
31 32 33 34 35 36

    Promise.resolve()
    .then(async () => {
      const data = await this.fetchComponent(route)
      let props
      if (route !== this.route) {
37 38
        const ctx = { ...data.ctx, pathname, query }
        props = await this.getInitialProps(data.Component, ctx)
N
nkzawa 已提交
39 40 41 42 43 44 45 46 47 48 49
      }

      this.route = route
      this.set(getURL(), { ...data, props })
    })
    .catch((err) => {
      if (err.cancelled) return

      // the only way we can appropriately handle
      // this failure is deferring to the browser
      // since the URL has already changed
D
Dan Zajdband 已提交
50
      window.location.reload()
N
nkzawa 已提交
51
    })
N
nkzawa 已提交
52 53
  }

N
nkzawa 已提交
54 55 56 57
  update (route, Component) {
    const data = this.components[route] || {}
    const newData = { ...data, Component }
    this.components[route] = newData
N
nkzawa 已提交
58

N
nkzawa 已提交
59
    if (route === this.route) {
N
nkzawa 已提交
60
      this.notify(newData)
N
nkzawa 已提交
61
    }
N
nkzawa 已提交
62 63
  }

N
nkzawa 已提交
64 65 66 67 68
  async reload (route) {
    delete this.components[route]

    if (route !== this.route) return

69
    const { pathname, query } = parse(window.location.href, true)
70

N
nkzawa 已提交
71 72 73 74
    let data
    let props
    try {
      data = await this.fetchComponent(route)
N
nkzawa 已提交
75
      if (route === this.route) {
76 77
        const ctx = { ...data.ctx, pathname, query }
        props = await this.getInitialProps(data.Component, ctx)
N
nkzawa 已提交
78 79 80 81 82 83 84 85 86
      }
    } catch (err) {
      if (err.cancelled) return false
      throw err
    }

    this.notify({ ...data, props })
  }

N
nkzawa 已提交
87
  back () {
D
Dan Zajdband 已提交
88
    window.history.back()
N
nkzawa 已提交
89 90
  }

N
nkzawa 已提交
91 92
  push (route, url) {
    return this.change('pushState', route, url)
N
nkzawa 已提交
93 94
  }

N
nkzawa 已提交
95 96
  replace (route, url) {
    return this.change('replaceState', route, url)
N
nkzawa 已提交
97 98
  }

N
nkzawa 已提交
99
  async change (method, route, url) {
100 101 102
    const { pathname, query } = parse(url, true)

    if (!route) route = toRoute(pathname)
N
nkzawa 已提交
103

N
nkzawa 已提交
104 105
    this.abortComponentLoad()

N
nkzawa 已提交
106 107 108 109 110
    let data
    let props
    try {
      data = await this.fetchComponent(route)
      if (route !== this.route) {
111 112
        const ctx = { ...data.ctx, pathname, query }
        props = await this.getInitialProps(data.Component, ctx)
N
nkzawa 已提交
113 114 115 116
      }
    } catch (err) {
      if (err.cancelled) return false
      throw err
N
nkzawa 已提交
117 118
    }

119 120 121 122
    if (getURL() !== url) {
      window.history[method]({ route }, null, url)
    }

N
nkzawa 已提交
123 124 125
    this.route = route
    this.set(url, { ...data, props })
    return true
N
nkzawa 已提交
126 127
  }

N
nkzawa 已提交
128
  set (url, data) {
N
nkzawa 已提交
129
    const parsed = parse(url, true)
N
nkzawa 已提交
130

N
nkzawa 已提交
131 132 133
    if (this.urlIsNew(parsed)) {
      this.pathname = parsed.pathname
      this.query = parsed.query
N
nkzawa 已提交
134
      this.notify(data)
N
nkzawa 已提交
135 136 137 138 139 140 141
    }
  }

  urlIsNew ({ pathname, query }) {
    return this.pathname !== pathname || !shallowEquals(query, this.query)
  }

N
nkzawa 已提交
142 143 144 145
  async fetchComponent (url) {
    const route = toRoute(parse(url).pathname)

    let data = this.components[route]
N
nkzawa 已提交
146 147 148 149 150 151 152 153 154 155 156
    if (!data) {
      let cancel

      const componentUrl = toJSONUrl(route)
      data = await new Promise((resolve, reject) => {
        this.componentLoadCancel = cancel = () => {
          if (xhr.abort) xhr.abort()
        }

        const xhr = loadComponent(componentUrl, (err, data) => {
          if (err) return reject(err)
N
nkzawa 已提交
157 158 159 160
          resolve({
            Component: data.Component,
            ctx: { xhr, err: data.err }
          })
N
nkzawa 已提交
161
        })
N
nkzawa 已提交
162 163
      })

N
nkzawa 已提交
164 165 166
      if (cancel === this.componentLoadCancel) {
        this.componentLoadCancel = null
      }
N
nkzawa 已提交
167

N
nkzawa 已提交
168
      this.components[route] = data
N
nkzawa 已提交
169 170 171 172
    }
    return data
  }

N
nkzawa 已提交
173
  async getInitialProps (Component, ctx) {
N
nkzawa 已提交
174 175
    let cancelled = false
    const cancel = () => { cancelled = true }
N
nkzawa 已提交
176
    this.componentLoadCancel = cancel
N
nkzawa 已提交
177

178
    const props = await (Component.getInitialProps ? Component.getInitialProps(ctx) : {})
N
nkzawa 已提交
179 180 181 182 183 184 185 186 187 188 189 190

    if (cancel === this.componentLoadCancel) {
      this.componentLoadCancel = null
    }

    if (cancelled) {
      const err = new Error('Cancelled')
      err.cancelled = true
      throw err
    }

    return props
N
nkzawa 已提交
191 192 193 194 195 196 197 198 199
  }

  abortComponentLoad () {
    if (this.componentLoadCancel) {
      this.componentLoadCancel()
      this.componentLoadCancel = null
    }
  }

N
nkzawa 已提交
200 201
  notify (data) {
    this.subscriptions.forEach((fn) => fn(data))
N
nkzawa 已提交
202 203 204
  }

  subscribe (fn) {
N
nkzawa 已提交
205 206
    this.subscriptions.add(fn)
    return () => this.subscriptions.delete(fn)
N
nkzawa 已提交
207 208 209
  }
}

N
nkzawa 已提交
210
function getURL () {
D
Dan Zajdband 已提交
211
  return window.location.pathname + (window.location.search || '') + (window.location.hash || '')
N
nkzawa 已提交
212
}
N
nkzawa 已提交
213

N
nkzawa 已提交
214
function toRoute (path) {
N
nkzawa 已提交
215 216 217
  return path.replace(/\/$/, '') || '/'
}

N
nkzawa 已提交
218
function toJSONUrl (route) {
D
Dan Zajdband 已提交
219
  return (route === '/' ? '/index' : route) + '.json'
N
nkzawa 已提交
220 221
}

N
nkzawa 已提交
222
function loadComponent (url, fn) {
N
nkzawa 已提交
223
  return loadJSON(url, (err, data) => {
N
nkzawa 已提交
224
    if (err) return fn(err)
N
nkzawa 已提交
225

N
nkzawa 已提交
226 227
    let module
    try {
N
nkzawa 已提交
228
      module = evalScript(data.component)
N
nkzawa 已提交
229 230 231
    } catch (err) {
      return fn(err)
    }
232

N
nkzawa 已提交
233
    const Component = module.default || module
N
nkzawa 已提交
234
    fn(null, { Component, err: data.err })
N
nkzawa 已提交
235
  })
236 237
}

N
nkzawa 已提交
238
function loadJSON (url, fn) {
D
Dan Zajdband 已提交
239
  const xhr = new window.XMLHttpRequest()
N
nkzawa 已提交
240 241 242 243 244 245 246 247 248 249 250 251 252
  xhr.onload = () => {
    let data

    try {
      data = JSON.parse(xhr.responseText)
    } catch (err) {
      fn(new Error('Failed to load JSON for ' + url))
      return
    }

    fn(null, data)
  }
  xhr.onerror = () => {
N
nkzawa 已提交
253
    fn(new Error('XHR failed. Status: ' + xhr.status))
N
nkzawa 已提交
254
  }
N
nkzawa 已提交
255 256 257 258 259
  xhr.onabort = () => {
    const err = new Error('XHR aborted')
    err.cancelled = true
    fn(err)
  }
N
nkzawa 已提交
260 261 262 263 264
  xhr.open('GET', url)
  xhr.send()

  return xhr
}