We've made a couple of dynamic, database driven web applications.
How did they work? What made the request? How was the response generated? What did the response consist of? →
Let's make a quick diagram of how that all worked. A run-of-the-mill database driven web application. →
We've made traditional web applications.
How does this user experience differ from native desktop and mobile apps? →
Another way to deliver a web application is as a single page:
How do you think this is achieved? What processes would need to take place? →
This is possible using combination of client side technologies, commonly called AJAX… →
AJAX is short for asynchronous JavaScript and XML. It's basically a bunch of interrelated technologies and techniques used to create asynchronous web applications:
Let's draw out how all of these technologies may come together as a single page web app… →
When these technologies are combined, our client side web applications can make incremental updates to the user interface without reloading the entire page. Great!
We know most of these technologies. What's missing? →
(most of you already knew this coming into class, and we did a quick review)
(we went over this a lot)
(Express)
Hey. So. Remember that time when we actually used some Server Side Javascript to make HTTP Requests? →
In client-side JavaScript, there's a feature analogous to the request module.
XMLHttpRequest
is JavaScript object that allows browser based JavaScript to make http requests!
THIS IS AMAZING! (though the api is kind of terrible)
(Again, you can check out the fetch api, for a newer, but less supported way of making background http requests)
We need a campfire. We don't have one. But here it goes: →
How does it work? →
Create an XMLHttpRequest object using the constructor:
const req = new XMLHttpRequest();
Use the open
method to configure the object. It takes 3 arguments:
req.open('GET', url, true);
Specify what it should do on error vs onload.
You can also use the addEventListener interface that we just learned:
req.addEventListener('load') { ... };
req.addEventListener('error') { ... };
There's also the older style onerror
and onload
properties on the XMLHttpRequest object:
req.onload = function() { ... };
req.onerror = function() { ... };
Your XMLHttpRequest object has a couple of useful properties:
status
- the response status code (for example, 200)responseText
- the actual body of the response
Typically, you would use the status to determine if the request were successful:
if (req.status >= 200 && req.status < 400) {
// do some cool stuff
}
Use send()
to actually send your request.
req.send();
Using this json file:
Some setup to serve this exercise, as well as a few others: →
hello.html
in your public folder
hello.js
in your public/javascripts folderIn your JavaScript file, hello.js
:
'http://localhost:3000/hello.json'
const url = 'http://localhost:3000/hello.json';
const req = new XMLHttpRequest();
req.open('GET', url, true);
Once we've successfully received data, add each object's message as a div to the body of the blank document. →
req.addEventListener('load', function() {
if (req.status >= 200 && req.status < 400) {
const messages = JSON.parse(req.responseText);
messages.forEach(function(obj) {
document.body.appendChild(
document.createElement('div')).
textContent = obj.message;
});
}
});
If there's an error… just create a text node for now: →
req.addEventListener('error', function(e) {
document.body.appendChild(document.createTextNode('uh-oh, something went wrong ' + e));
});
Lastly… send the request. →
req.send();
Hey - we started out with a completely blank html page, but it has some text in it!?
Let's check out the network tab and refresh the page. →
hello.html
hello.js
hello.json
hello!
How do you think we can get an error to show up? →
else {
document.body.appendChild(
document.createTextNode(
'request received a ' + req.status));
}
We could also modify the code so that the request is only made when a user clicks a button. Let's start off by adding this markup. →
<h1>Messages</h1>
<input type="button" id="get-messages-button" value="GET MAH MSGS">
How would we change our JavaScript so that whenever you click the button above, it requests the messages in the json file and inserts them into the DOM? →
Just get the button and add a "click" event listener.
const button = document.getElementById('get-messages-button');
button.addEventListener('click', function() {
.
.
.
});
You can drop in all of you request code in the callback above.
Let's try a little experiment:
What do you think will happen? (Ignore the fact that the documents don't match at all, and we're not getting back json; there's something else off) →
Here's the error that we get:
XMLHttpRequest cannot load ...
'No Access-Control-Allow-Origin' header is present on
the requested resource. Origin 'http://localhost:3000'
is therefore not allowed access.
What do you think it means? →
No, really, what does it mean? →
We were not given access to the content, because we're not allowed to make cross domain requests to that server.
The two ideas that govern this are:
The same origin policy is a policy implemented by browsers that restricts how a document, script or data from one origin can interact with a document, script or data from another origin.
What does same site or same origin mean exactly? →
From MDN…
http://store.company.com/dir/page.html
http://store.company.com/dir2/other.html
Yeshttp://store.company.com/dir/inner/another.html
Yeshttps://store.company.com/secure.html
No - different protocolhttp://store.company.com:81/dir/etc.html
No - different porthttp://news.company.com/dir/other.html
No- different hostThat seems weirdly restrictive, right? Why do you think same origin policy exists? What is it trying to prevent? →
A hint from wikipedia: "This mechanism bears a particular significance for modern web applications that extensively depend on HTTP cookies to maintain authenticated user sessions"
Imagine that you're logged into your bank account: →
XMLHttpRequest
to…I think we can agree that Same Origin Policy is kind of… um important.
But… how do we share data across sites? How do APIs work? →
Let's try a quick experiment with githubs public API. →
You can actually get info from a GitHub user through GitHub's api by using the following URL:
https://api.github.com/users/[a username]/repos
Let's try checking out what it says about one of my GitHub accounts by hitting the actual api url: https://api.github.com/users/foureyes/repos
[{
"id": 26084780,
"name": "bjorklund",
"full_name": "foureyes/bjorklund",
"owner": {
"login": "foureyes",
"avatar_url": "https://avatars.githubusercontent.com/u/356512?v=3",
"url": "https://api.github.com/users/foureyes",
.
.
},
"private": false,
.
.
}]
We could probably build a quick form that takes in a username →
Some more docs regarding the GitHub API: →
Our repository viewer will have: →
It might look like this:
Typically, API documentation will specify:
Let's take a look at the two end points we'll be using. →
The endpoint / URL for retrieving info about the rate limit is:
GET /rate_limit
Using this URL with my account: https://api.github.com/rate_limit
We're interested in resources.core.limit… this is what we get back:
{
"resources": {
"core": {
"limit": 60,
"remaining": 58,
"reset": 1447761547
},
"search": {
"limit": 10,
"remaining": 10,
"reset": 1447759711
}
},
"rate": {
"limit": 60,
"remaining": 58,
"reset": 1447761547
}
}
The endpoint / URL for retrieving repository info from GitHub is:
GET /:username/repos
Using this URL with my account: https://api.github.com/users/foureyes/repos
We get back…
[{
"id": 26084780,
"name": "bjorklund",
"full_name": "foureyes/bjorklund",
"owner": {
"login": "foureyes",
"avatar_url": "https://avatars.githubusercontent.com/u/356512?v=3",
"url": "https://api.github.com/users/foureyes",
.
.
},
"private": false,
.
.
}]
We'll need:
Here's our HTML:
<h2>Repository Viewer</h2>
<input type="button" id="get-rate-limit" name="get-rate-limit" value="Get Rate Limit">
<pre id="rate-limit">
</pre>
<label for="username">GitHub Username</label>
<input type="text" id="username" name="username">
<input type="button" id="get-repos" name="get-repos" value="Get Repositories">
<div id="container">
<ul></ul>
</div>
Let's gather the buttons and add event listeners to them. →
document.addEventListener('DOMContentLoaded', init);
function init() {
console.log('init');
var button = document.getElementById('get-repos'),
rateLimitButton = document.getElementById('get-rate-limit');
button.addEventListener('click', handleClick);
rateLimitButton.addEventListener('click', handleRateLimitClick);
}
To get the rate limit, we can use the following url http://api.github.com/rate_limit. Let's set up the request in our handleRateLimitClick
function →
function handleRateLimitClick() {
var req = new XMLHttpRequest(),
url = 'http://api.github.com/rate_limit';
req.open('GET', url, true);
req.addEventListener('load', handleRateLimitResponse);
req.send();
}
Let's define a function that populates an element, the pre tag, with the data from the API Limit call. →
function handleRateLimitResponse() {
var pre = document.getElementById('rate-limit'),
response = JSON.parse(this.responseText);
if (this.status >= 200 && this.status < 400) {
pre.textContent = response.rate.limit + ' Limit, '
+ response.rate.remaining + ' Remaining, '
+ new Date(response.rate.reset * 1000);
}
}
When clicking view repositories, we should retrieve the repositories for the user in the text field. First, let's set up our click handler… and configure a request within it. →
function handleClick(evt) {
var req = new XMLHttpRequest(),
url = 'http://api.github.com/users/' +
document.getElementById('username').value + '/repos';
req.open('GET', url, true);
req.addEventListener('load', handleResponse);
req.send();
}
We can use the response from the API to drop in the repositories. Create a function that gets called when the data from the request has loaded. →
function handleResponse() {
if (this.status >= 200 && this.status < 400) {
var div = document.getElementById('container'),
oldList = document.querySelector('#container ul'),
ul = document.createElement('ul'),
repos = JSON.parse(this.responseText);
repos.forEach(function(obj) {
ul.appendChild(document.createElement('li')).textContent = obj.name;
});
div.replaceChild(ul, oldList);
}
}
See anything strange? →
Yeah… wait a second. Those are different domains? Aren't cross domain requests allowed? →
Cross Origin Resource Sharing (CORS) is a mechanism that allows resources, such as JSON, fonts, etc. to be requested from a domain from a different origin. From mdn:
How do you think we were able to contact GitHub's api? →
So… how does this work behind the scenes. →
Going to our example of cross-site request forgery, issuing a background POST via scripting … to another domain seems like it'll work, because the request will actually go through!
Then… how is CSRF prevented, and how does SOP/CORS help in preventing CSRF
However, you may not always have access or contact with the server that's running the services. So, maybe they won't set the CORS headers for you. What are some other options around cross domain requests? →
Yes. We can make our own APIs with Express.
The secret is:
res.json()
This returns a json response of the object that is passed in.
Let's try one where we read message objects out of MongoDB →
Just a run-of-the-mill message:
const Message = new mongoose.Schema({
message: String,
dateSent: Date
});
Get all messages as JSON…
router.get('/api/messages', function(req, res) {
Message.find({}, function(err, messages, count) {
res.json(messages.map(function(ele) {
return {
'message': ele.message,
'date': ele.dateSent
};
}));
});
});