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

export default class Router {
  constructor (initialData) {
N
nkzawa 已提交
7 8
    // represents the current component key
    this.route = toRoute(location.pathname)
N
nkzawa 已提交
9 10

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

N
nkzawa 已提交
13 14
    this.subscriptions = new Set()
    this.componentLoadCancel = null
N
nkzawa 已提交
15
    this.onPopState = this.onPopState.bind(this)
N
nkzawa 已提交
16

N
nkzawa 已提交
17 18 19 20 21
    window.addEventListener('popstate', this.onPopState)
  }

  onPopState (e) {
    this.abortComponentLoad()
N
nkzawa 已提交
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

    const route = (e.state || {}).route || toRoute(location.pathname)

    Promise.resolve()
    .then(async () => {
      const data = await this.fetchComponent(route)
      let props
      if (route !== this.route) {
        props = await this.getInitialProps(data.Component)
      }

      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
      location.reload()
    })
N
nkzawa 已提交
44 45
  }

N
nkzawa 已提交
46
  async update (route, data) {
N
nkzawa 已提交
47 48 49 50
    data.Component = evalScript(data.component).default
    delete data.component
    this.components[route] = data

N
nkzawa 已提交
51 52 53 54 55 56 57 58 59 60 61
    if (route === this.route) {
      let props
      try {
        props = await this.getInitialProps(data.Component)
      } catch (err) {
        if (err.cancelled) return false
        throw err
      }
      this.notify({ ...data, props })
    }
    return true
N
nkzawa 已提交
62 63 64 65 66 67
  }

  back () {
    history.back()
  }

N
nkzawa 已提交
68 69
  push (route, url) {
    return this.change('pushState', route, url)
N
nkzawa 已提交
70 71
  }

N
nkzawa 已提交
72 73
  replace (route, url) {
    return this.change('replaceState', route, url)
N
nkzawa 已提交
74 75
  }

N
nkzawa 已提交
76 77 78
  async change (method, route, url) {
    if (!route) route = toRoute(parse(url).pathname)

N
nkzawa 已提交
79 80
    this.abortComponentLoad()

N
nkzawa 已提交
81 82 83 84 85 86 87 88 89 90
    let data
    let props
    try {
      data = await this.fetchComponent(route)
      if (route !== this.route) {
        props = await this.getInitialProps(data.Component)
      }
    } catch (err) {
      if (err.cancelled) return false
      throw err
N
nkzawa 已提交
91 92
    }

N
nkzawa 已提交
93 94 95 96
    history[method]({ route }, null, url)
    this.route = route
    this.set(url, { ...data, props })
    return true
N
nkzawa 已提交
97 98
  }

N
nkzawa 已提交
99
  set (url, data) {
N
nkzawa 已提交
100
    const parsed = parse(url, true)
N
nkzawa 已提交
101

N
nkzawa 已提交
102 103 104
    if (this.urlIsNew(parsed)) {
      this.pathname = parsed.pathname
      this.query = parsed.query
N
nkzawa 已提交
105
      this.notify(data)
N
nkzawa 已提交
106 107 108 109 110 111 112
    }
  }

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

N
nkzawa 已提交
113 114 115 116 117
  async fetchComponent (url) {
    const route = toRoute(parse(url).pathname)

    let data = this.components[route]
    if (data) return data
N
nkzawa 已提交
118

N
nkzawa 已提交
119
    let cancel
N
nkzawa 已提交
120 121
    let cancelled = false

N
nkzawa 已提交
122 123 124 125 126 127 128 129 130
    const componentUrl = toJSONUrl(route)
    data = await new Promise((resolve, reject) => {
      this.componentLoadCancel = cancel = () => {
        cancelled = true
        if (componentXHR.abort) componentXHR.abort()

        const err = new Error('Cancelled')
        err.cancelled = true
        reject(err)
N
nkzawa 已提交
131 132
      }

N
nkzawa 已提交
133 134 135
      const componentXHR = loadComponent(componentUrl, (err, data) => {
        if (err) return reject(err)
        resolve(data)
N
nkzawa 已提交
136
      })
N
nkzawa 已提交
137 138 139 140
    })

    if (cancel === this.componentLoadCancel) {
      this.componentLoadCancel = null
N
nkzawa 已提交
141 142
    }

N
nkzawa 已提交
143 144
    // we update the cache even if cancelled
    if (data) this.components[route] = data
N
nkzawa 已提交
145

N
nkzawa 已提交
146 147 148 149 150
    if (cancelled) {
      const err = new Error('Cancelled')
      err.cancelled = true
      throw err
    }
N
nkzawa 已提交
151

N
nkzawa 已提交
152 153 154 155 156 157
    return data
  }

  async getInitialProps (Component) {
    let cancelled = false
    const cancel = () => { cancelled = true }
N
nkzawa 已提交
158
    this.componentLoadCancel = cancel
N
nkzawa 已提交
159 160 161 162 163 164 165 166 167 168 169 170 171 172

    const props = await (Component.getInitialProps ? Component.getInitialProps({}) : {})

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

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

    return props
N
nkzawa 已提交
173 174 175 176 177 178 179 180 181
  }

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

N
nkzawa 已提交
182 183
  notify (data) {
    this.subscriptions.forEach((fn) => fn(data))
N
nkzawa 已提交
184 185 186
  }

  subscribe (fn) {
N
nkzawa 已提交
187 188
    this.subscriptions.add(fn)
    return () => this.subscriptions.delete(fn)
N
nkzawa 已提交
189 190 191
  }
}

N
nkzawa 已提交
192 193 194
function getURL () {
  return location.pathname + (location.search || '') + (location.hash || '')
}
N
nkzawa 已提交
195

N
nkzawa 已提交
196
function toRoute (path) {
N
nkzawa 已提交
197 198 199
  return path.replace(/\/$/, '') || '/'
}

N
nkzawa 已提交
200
function toJSONUrl (route) {
N
nkzawa 已提交
201 202 203
  return ('/' === route ? '/index' : route) + '.json'
}

N
nkzawa 已提交
204
function loadComponent (url, fn) {
N
nkzawa 已提交
205
  return loadJSON(url, (err, data) => {
N
nkzawa 已提交
206
    if (err) return fn(err)
N
nkzawa 已提交
207

N
nkzawa 已提交
208 209 210 211 212 213 214 215
    const { component } = data

    let module
    try {
      module = evalScript(component)
    } catch (err) {
      return fn(err)
    }
216

N
nkzawa 已提交
217 218 219
    const Component = module.default || module
    fn(null, { Component })
  })
220 221
}

N
nkzawa 已提交
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
function loadJSON (url, fn) {
  const xhr = new XMLHttpRequest()
  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 已提交
237
    fn(new Error('XHR failed. Status: ' + xhr.status))
N
nkzawa 已提交
238 239 240 241 242 243
  }
  xhr.open('GET', url)
  xhr.send()

  return xhr
}