Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changing XHR to Fetch API #1845

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ docker run --name devdocs -d -p 9292:9292 thibaut/devdocs

DevDocs aims to make reading and searching reference documentation fast, easy and enjoyable.

The app's main goals are to:
The app's main goals are to:

* Keep load times as short as possible
* Improve the quality, speed, and order of search results
* Maximize the use of caching and other performance optimizations
* Maintain a clean and readable user interface
* Maintain a clean and readable user interface
* Be fully functional offline
* Support full keyboard navigation
* Reduce “context switch” by using a consistent typography and design across all documentations
Expand All @@ -70,7 +70,7 @@ The app's main goals are to:

The web app is all client-side JavaScript, written in [CoffeeScript](http://coffeescript.org), and powered by a small [Sinatra](http://www.sinatrarb.com)/[Sprockets](https://github.com/rails/sprockets) application. It relies on files generated by the [scraper](#scraper).

Many of the code's design decisions were driven by the fact that the app uses XHR to load content directly into the main frame. This includes stripping the original documents of most of their HTML markup (e.g. scripts and stylesheets) to avoid polluting the main frame, and prefixing all CSS class names with an underscore to prevent conflicts.
Many of the code's design decisions were driven by the fact that the app uses Fetch to load content directly into the main frame. This includes stripping the original documents of most of their HTML markup (e.g. scripts and stylesheets) to avoid polluting the main frame, and prefixing all CSS class names with an underscore to prevent conflicts.

Another driving factor is performance and the fact that everything happens in the browser. A service worker (which comes with its own set of constraints) and `localStorage` are used to speed up the boot time, while memory consumption is kept in check by allowing the user to pick his/her own set of documentations. The search algorithm is kept simple because it needs to be fast even searching through 100,000 strings.

Expand Down Expand Up @@ -126,7 +126,7 @@ thor docs:clean # Delete documentation packages
thor console # Start a REPL
thor console:docs # Start a REPL in the "Docs" module

# Tests can be run quickly from within the console using the "test" command.
# Tests can be run quickly from within the console using the "test" command.
# Run "help test" for usage instructions.
thor test:all # Run all tests
thor test:docs # Run "Docs" tests
Expand Down
94 changes: 32 additions & 62 deletions assets/javascripts/lib/ajax.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,32 @@ MIME_TYPES =
applyDefaults(options)
serializeData(options)

xhr = new XMLHttpRequest()
xhr.open(options.type, options.url, options.async)

applyCallbacks(xhr, options)
applyHeaders(xhr, options)

xhr.send(options.data)

if options.async
abort: abort.bind(undefined, xhr)
else
parseResponse(xhr, options)
abortController = new AbortController()

timer = setTimeout =>
abortController.abort()
, options.timeout * 1000

fetch(
options.url,
headers: processHeaders(options)
method: options.type
contentType: options.dataType
signal: abortController.signal
).then((response) ->
if options.dataType == 'json'
response.json()
else
response.text()
Comment on lines +22 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think response.ok and response.status (200 <= xhr.status < 300) need to be checked.

).then((data) ->
if data?
onSuccess data, options
else
onError 'invalid', '', options
).catch((error) ->
onError 'error', error, options
).finally ->
clearTimeout timer

ajax.defaults =
async: true
Expand Down Expand Up @@ -51,19 +65,7 @@ serializeData = (options) ->
serializeParams = (params) ->
("#{encodeURIComponent key}=#{encodeURIComponent value}" for key, value of params).join '&'

applyCallbacks = (xhr, options) ->
return unless options.async

xhr.timer = setTimeout onTimeout.bind(undefined, xhr, options), options.timeout * 1000
xhr.onprogress = options.progress if options.progress
xhr.onreadystatechange = ->
if xhr.readyState is 4
clearTimeout(xhr.timer)
onComplete(xhr, options)
return
return

applyHeaders = (xhr, options) ->
processHeaders = (options) ->
options.headers or= {}

if options.contentType
Expand All @@ -74,45 +76,13 @@ applyHeaders = (xhr, options) ->

if options.dataType
options.headers['Accept'] = MIME_TYPES[options.dataType] or options.dataType
return options.headers

for key, value of options.headers
xhr.setRequestHeader(key, value)
onSuccess = (data, options) ->
options.success?.call options.context, data, options
return

onComplete = (xhr, options) ->
if 200 <= xhr.status < 300
if (response = parseResponse(xhr, options))?
onSuccess response, xhr, options
else
onError 'invalid', xhr, options
else
onError 'error', xhr, options
onError = (type, error, options) ->
options.error?.call options.context, type, error, options
Comment on lines +85 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are some usages for the error handler and progress callback:

return

onSuccess = (response, xhr, options) ->
options.success?.call options.context, response, xhr, options
return

onError = (type, xhr, options) ->
options.error?.call options.context, type, xhr, options
return

onTimeout = (xhr, options) ->
xhr.abort()
onError 'timeout', xhr, options
return

abort = (xhr) ->
clearTimeout(xhr.timer)
xhr.onreadystatechange = null
xhr.abort()
return
Comment on lines -105 to -109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's one usage of abort:


parseResponse = (xhr, options) ->
if options.dataType is 'json'
parseJSON(xhr.responseText)
else
xhr.responseText

parseJSON = (json) ->
try JSON.parse(json) catch