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

6
export default class Router extends EventEmitter {
N
Naoyuki Kanezawa 已提交
7
  constructor (pathname, query, { Component, ErrorComponent, ctx } = {}) {
8
    super()
N
nkzawa 已提交
9
    // represents the current component key
N
Naoyuki Kanezawa 已提交
10
    this.route = toRoute(pathname)
N
nkzawa 已提交
11 12

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

N
Naoyuki Kanezawa 已提交
15
    this.ErrorComponent = ErrorComponent
N
Naoyuki Kanezawa 已提交
16 17
    this.pathname = pathname
    this.query = query
N
nkzawa 已提交
18
    this.subscriptions = new Set()
19

N
nkzawa 已提交
20
    this.componentLoadCancel = null
N
nkzawa 已提交
21
    this.onPopState = this.onPopState.bind(this)
N
nkzawa 已提交
22

D
Dan Zajdband 已提交
23
    if (typeof window !== 'undefined') {
24 25 26 27
      // in order for `e.state` to work on the `onpopstate` event
      // we have to register the initial route upon initialization
      this.replace(format({ pathname, query }), getURL())

N
nkzawa 已提交
28 29
      window.addEventListener('popstate', this.onPopState)
    }
N
nkzawa 已提交
30 31
  }

32
  async onPopState (e) {
N
nkzawa 已提交
33
    this.abortComponentLoad()
N
nkzawa 已提交
34

35
    const { url, as } = e.state
N
Naoyuki Kanezawa 已提交
36
    const { pathname, query } = parse(url, true)
37

38 39 40 41 42
    if (!this.urlIsNew(pathname, query)) {
      this.emit('routeChangeStart', as)
      this.emit('routeChangeComplete', as)
      return
    }
43

N
Naoyuki Kanezawa 已提交
44
    const route = toRoute(pathname)
N
nkzawa 已提交
45

46 47 48 49 50 51 52 53 54 55 56 57 58
    this.emit('routeChangeStart', as)
    const {
      data,
      props,
      error
    } = await this.getRouteInfo(route, pathname, query)

    if (error) {
      this.emit('routeChangeError', error, as)
      // We don't need to throw here since the error is already logged by
      // this.getRouteInfo
      return
    }
N
Naoyuki Kanezawa 已提交
59

60 61 62
    this.route = route
    this.set(pathname, query, { ...data, props })
    this.emit('routeChangeComplete', as)
N
nkzawa 已提交
63 64
  }

N
nkzawa 已提交
65 66 67 68
  update (route, Component) {
    const data = this.components[route] || {}
    const newData = { ...data, Component }
    this.components[route] = newData
N
nkzawa 已提交
69

N
nkzawa 已提交
70
    if (route === this.route) {
N
nkzawa 已提交
71
      this.notify(newData)
N
nkzawa 已提交
72
    }
N
nkzawa 已提交
73 74
  }

N
nkzawa 已提交
75 76 77 78 79
  async reload (route) {
    delete this.components[route]

    if (route !== this.route) return

80 81
    const url = window.location.href
    const { pathname, query } = parse(url, true)
N
Naoyuki Kanezawa 已提交
82

83 84 85 86 87 88
    this.emit('routeChangeStart', url)
    const {
      data,
      props,
      error
    } = await this.getRouteInfo(route, pathname, query)
N
Naoyuki Kanezawa 已提交
89

90 91 92
    if (error) {
      this.emit('routeChangeError', error, url)
      throw error
N
nkzawa 已提交
93 94 95
    }

    this.notify({ ...data, props })
N
Naoyuki Kanezawa 已提交
96

97
    this.emit('routeChangeComplete', url)
N
nkzawa 已提交
98 99
  }

N
nkzawa 已提交
100
  back () {
D
Dan Zajdband 已提交
101
    window.history.back()
N
nkzawa 已提交
102 103
  }

N
Naoyuki Kanezawa 已提交
104 105
  push (url, as = url) {
    return this.change('pushState', url, as)
N
nkzawa 已提交
106 107
  }

N
Naoyuki Kanezawa 已提交
108 109
  replace (url, as = url) {
    return this.change('replaceState', url, as)
N
nkzawa 已提交
110 111
  }

N
Naoyuki Kanezawa 已提交
112
  async change (method, url, as) {
113
    this.abortComponentLoad()
N
Naoyuki Kanezawa 已提交
114
    const { pathname, query } = parse(url, true)
115 116

    if (!this.urlIsNew(pathname, query)) {
117
      this.emit('routeChangeStart', as)
118
      changeState()
119
      this.emit('routeChangeComplete', as)
120 121 122
      return true
    }

N
Naoyuki Kanezawa 已提交
123
    const route = toRoute(pathname)
N
nkzawa 已提交
124

125 126 127 128
    this.emit('routeChangeStart', as)
    const {
      data, props, error
    } = await this.getRouteInfo(route, pathname, query)
N
Naoyuki Kanezawa 已提交
129

130 131 132
    if (error) {
      this.emit('routeChangeError', error, as)
      throw error
N
nkzawa 已提交
133 134
    }

135
    changeState()
136

N
nkzawa 已提交
137
    this.route = route
N
Naoyuki Kanezawa 已提交
138
    this.set(pathname, query, { ...data, props })
N
Naoyuki Kanezawa 已提交
139

140
    this.emit('routeChangeComplete', as)
N
nkzawa 已提交
141
    return true
142 143 144

    function changeState () {
      if (method !== 'pushState' || getURL() !== as) {
145
        window.history[method]({ url, as }, null, as)
146 147
      }
    }
N
nkzawa 已提交
148 149
  }

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
  async getRouteInfo (route, pathname, query) {
    const routeInfo = {}

    try {
      const data = routeInfo.data = await this.fetchComponent(route)
      const ctx = { ...data.ctx, pathname, query }
      routeInfo.props = await this.getInitialProps(data.Component, ctx)
    } catch (err) {
      if (err.cancelled) {
        return { error: err }
      }

      const data = routeInfo.data = { Component: this.ErrorComponent, ctx: { err } }
      const ctx = { ...data.ctx, pathname, query }
      routeInfo.props = await this.getInitialProps(data.Component, ctx)

      routeInfo.error = err
      console.error(err)
    }

    return routeInfo
  }

N
Naoyuki Kanezawa 已提交
173
  set (pathname, query, data) {
174 175 176
    this.pathname = pathname
    this.query = query
    this.notify(data)
N
nkzawa 已提交
177 178
  }

N
Naoyuki Kanezawa 已提交
179
  urlIsNew (pathname, query) {
N
nkzawa 已提交
180 181 182
    return this.pathname !== pathname || !shallowEquals(query, this.query)
  }

N
Naoyuki Kanezawa 已提交
183
  async fetchComponent (route) {
N
nkzawa 已提交
184
    let data = this.components[route]
N
nkzawa 已提交
185 186 187 188 189
    if (!data) {
      let cancel

      data = await new Promise((resolve, reject) => {
        this.componentLoadCancel = cancel = () => {
190 191 192 193 194 195
          if (xhr.abort) {
            xhr.abort()
            const error = new Error('Fetching componenet cancelled')
            error.cancelled = true
            reject(error)
          }
N
nkzawa 已提交
196 197
        }

N
Naoyuki Kanezawa 已提交
198 199
        const url = `/_next/pages${route}`
        const xhr = loadComponent(url, (err, data) => {
N
nkzawa 已提交
200
          if (err) return reject(err)
N
nkzawa 已提交
201 202 203 204
          resolve({
            Component: data.Component,
            ctx: { xhr, err: data.err }
          })
N
nkzawa 已提交
205
        })
N
nkzawa 已提交
206 207
      })

N
nkzawa 已提交
208 209 210
      if (cancel === this.componentLoadCancel) {
        this.componentLoadCancel = null
      }
N
nkzawa 已提交
211

N
nkzawa 已提交
212
      this.components[route] = data
N
nkzawa 已提交
213 214 215 216
    }
    return data
  }

N
nkzawa 已提交
217
  async getInitialProps (Component, ctx) {
N
nkzawa 已提交
218 219
    let cancelled = false
    const cancel = () => { cancelled = true }
N
nkzawa 已提交
220
    this.componentLoadCancel = cancel
N
nkzawa 已提交
221

222
    const props = await (Component.getInitialProps ? Component.getInitialProps(ctx) : {})
N
nkzawa 已提交
223 224 225 226 227 228

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

    if (cancelled) {
229
      const err = new Error('Loading initial props cancelled')
N
nkzawa 已提交
230 231 232 233 234
      err.cancelled = true
      throw err
    }

    return props
N
nkzawa 已提交
235 236 237 238 239 240 241 242 243
  }

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

N
nkzawa 已提交
244 245
  notify (data) {
    this.subscriptions.forEach((fn) => fn(data))
N
nkzawa 已提交
246 247 248
  }

  subscribe (fn) {
N
nkzawa 已提交
249 250
    this.subscriptions.add(fn)
    return () => this.subscriptions.delete(fn)
N
nkzawa 已提交
251 252 253
  }
}

N
nkzawa 已提交
254
function getURL () {
D
Dan Zajdband 已提交
255
  return window.location.pathname + (window.location.search || '') + (window.location.hash || '')
N
nkzawa 已提交
256
}
N
nkzawa 已提交
257

N
nkzawa 已提交
258
function toRoute (path) {
N
nkzawa 已提交
259 260 261
  return path.replace(/\/$/, '') || '/'
}

N
nkzawa 已提交
262
function loadComponent (url, fn) {
N
nkzawa 已提交
263
  return loadJSON(url, (err, data) => {
N
nkzawa 已提交
264
    if (err) return fn(err)
N
nkzawa 已提交
265

N
nkzawa 已提交
266 267
    let module
    try {
N
nkzawa 已提交
268
      module = evalScript(data.component)
N
nkzawa 已提交
269 270 271
    } catch (err) {
      return fn(err)
    }
272

N
nkzawa 已提交
273
    const Component = module.default || module
N
nkzawa 已提交
274
    fn(null, { Component, err: data.err })
N
nkzawa 已提交
275
  })
276 277
}

N
nkzawa 已提交
278
function loadJSON (url, fn) {
D
Dan Zajdband 已提交
279
  const xhr = new window.XMLHttpRequest()
N
nkzawa 已提交
280 281 282 283 284 285 286 287 288 289 290 291 292
  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 已提交
293
    fn(new Error('XHR failed. Status: ' + xhr.status))
N
nkzawa 已提交
294
  }
N
nkzawa 已提交
295 296 297 298 299
  xhr.onabort = () => {
    const err = new Error('XHR aborted')
    err.cancelled = true
    fn(err)
  }
N
nkzawa 已提交
300
  xhr.open('GET', url)
N
Naoyuki Kanezawa 已提交
301
  xhr.setRequestHeader('Accept', 'application/json')
N
nkzawa 已提交
302 303 304 305
  xhr.send()

  return xhr
}