Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Practice creating your own promises
You may have used promises provided by libraries or built-in functions before. For example:
A promise is an object with a .then
method. This method takes a callback function that it will call with the result when the promise is finished (or "resolved"). You can imagine a promise object looks something like this:
But how do you create your own promise objects?
You can create your own promise objects with new Promise()
. You have to pass in a function that defines when the promise will resolve or reject. This function is passed two arguments: resolve
and reject
. These are functions you call with the value you want to resolve/reject with.
For example:
You could use the above just like any other promise-returning function:
You're going to create a promisified version of setTimeout
, called wait
. It should take a number of millliseconds to wait as an argument, set a timeout for that long, then resolve the promise.
It should be usable like this:
You can run the tests to check if your solution works:
You're going to create your own promisified wrapper of Node's fs.readFile
method. It usually takes a callback to be run when it finishes its asynchronous task.
Implement the readFilePromise
function so that it returns a new promise. It should use fs.readFile
to read whatever file path is passed in, then resolve with the result. It should reject with any error that occurred. For example:
You can run the tests to check if your solution works:
Learn how to use cookies to persist information across HTTP requests
Cookies are an important part of authentication. We're going to learn how they allow your server to "remember" information about previous requests.
HTTP is a "stateless" protocol. This means each new request to your server is totally independent of any other. There is no way by default for a request to contain information from previous requests. Unfortunately it's quite hard to build a website without being able to remember things. For example "what has this user added to their shopping cart?" and "has this user already logged in?".
Cookies were introduced in 1994 as a way for web browsers to store information on behalf of the server. The response to one request can contain a cookie; the browser will store this cookie and automatically include it on all future requests to the same domain.
A cookie is just a standard HTTP header. A cookie can be set by a server including the set-cookie
header in a response. Here's an example HTTP response:
That set-cookie
header tells the browser to store a cookie with a name of "userid"
and a value of "1234"
.
This cookie is then sent on all future requests to this domain via the cookie
request header. Here's an example HTTP request:
The server would receive this second request, read the cookie
header and know that this request was made by the same user as before (with a "userid"
of "1234"
).
Cookies also support extra attributes to customise their behaviour. These can be set after the cookie value itself, like this:
By default a cookie only lasts as long as the user is browsing. As soon as they close their tabs the cookie will be deleted by the browser. This is useful for certain features (like a shopping cart), but less useful for keeping a user logged in.
The server can specify an expiry time for the cookie. This tells the browser to keep it around (even if the user closes their tabs) until the time runs out. There are two ways to control this: Expires
and Max-Age
. Expires
lets you set a specific date it should expire on; Max-Age
lets you specify how many seconds from now the cookie should last.
Cookies often contain sensitive information. There are a few options that should be specified to make them more secure.
The HttpOnly
option stops client-side JavaScript from accessing cookies. This can prevent malicious JS code (e.g. from a browser extension) from reading your cookies (this is know as "Cross-site Scripting" or XSS).
The Same-Site
option stops the cookie from being sent on requests made from other domains. You probably want to set it to "Lax" (which is the default starting with Chrome v84). Otherwise there's a risk of other sites pretending to act on behalf of a logged in user (this is know as "Cross-site Request Forgery" or CSRF).
The Secure
option will ensure the cookie is only set for secure encrypted (https
) connections. You shouldn't use this in development (since your localhost
server doesn't use https
) but it's a very good idea in production.
Lets see how to set and read cookies using Node.
Download the starter files and cd
in
npm install
npm run dev
First lets set a cookie by adding a "set-cookie" header to a response manually. Add a new handler for the GET /example
route:
Visit http://localhost:3000/example. You should be redirected back to the homepage. Open dev tools and look at the "Application" tab. Click on "Cookies" in the sidebar and you should be able to see the cookie you just set.
You can read the cookie on the server by looking at the "cookie" header. Edit your home handler:
If you refresh the page now you should see "hello=this is my cookie" logged. If you delete the cookie using the Application tab of dev tools and refresh again the cookie log should be gone.
Working with cookies this way is quite awkward—everything is just a big string, and we'd have to manually parse any values we needed. Luckily Express comes with some built-in cookie methods.
Express' response
object has a cookie
method. It takes three arguments, the name, the value, and an optional object for all the cookie options. It handles creating the "set-cookie" header string automatically.
Update your /example
handler to use Express' cookie helper:
This should create the exact same cookie as before.
Since reading cookies isn't something every server needs Express doesn't come with it built-in. They provide an optional middleware you need to install and use.
This middleware works like the built-in body-parsing one. It grabs the "cookie" header, parses it into a nice object, then attaches it to the request
for you to use.
So now you can access all the cookies sent on this request at request.cookies
:
You should see an object like this logged:
Express also provides the response.clearCookie
method for removing cookies. It takes the name of the cookie to remove. When the browser receives this response it will delete the matching cookie. Add a new route to your server:
If you visit http://localhost:3000/remove in your browser you should be redirected back to the home page, but the cookie will be gone.
Cookies are useful for ensuring users don't have to keep verifying their identity on every request. Once a user has proved who they are (usually by entering a password only they know) it's important to remember that information.
There are two ways to use cookies for authenticating users. The first is often known as "stateless" auth. We can store all the information we need to know in the cookie itself. For example:
When the user first logs in we set a cookie containing the user's information. On subsequent requests our server can check for this cookie. If it is present we can assume the user has previously logged in, and so we allow them to see protected content.
Unfortunately this has a pretty serious security problem: we can't trust cookies sent to us. Since a cookie is just an HTTP header anybody could send a cookie that looks like anything. E.g. anyone can use curl
to send such a request:
It's also easy to edit cookie values in dev tools—a user could simply change their ID/username to another.
However there is a way we can trust cookies: we can sign them. In cryptography signing refers to using a mathematical operation based on a secret string to transform a value. This signature will always be the same assuming the same secret and the same input value. Without the secret it is impossible to reproduce the same signature.
If we sign our cookie we can validate that it has not been tampered with, since only our server knows the secret required to create a valid signature. Implementing this from scratch would be complex and easy to mess up—luckily the cookie-parser
middleware supports signed cookies.
You need to pass a random secret string to the cookieParser()
middleware function. Then you can specify signed: true
in the cookie options. Signed cookies are available at a different request key to normal cookies: request.signedCookies
.
Add a GET /login
route that sets a signed cookie containing some user information, then redirects to the home page
Add a GET /logout
route that removes the cookie, then redirects to the home page
Log the signed cookies in the home route
If you visit /login
you should see the cookie data you set logged in your terminal. In the browser's dev tools the cookie will have some extra random stuff attached to it. This is the signature.
If you edit the cookie in dev tools and then refresh you should instead see your server log false
for that cookie name. This is because the signature no longer matches, so the server does not trust the cookie.
If you visit /logout
the cookie should be removed from your browser.
Storing all the information we need inside the cookie like this is very convenient. However there are some downsides:
Cookies have a 4kb size limit, so you can't fit too much info in them.
The cookie is the "source of truth". This means the server cannot invalidate a cookie, it has to wait for the cookie to expire. The server cannot log users out—as long as their cookie is valid they can keep making requests.
The other way to keep users logged in is to keep track of that state on the server. The cookie just stores a unique ID. This ID refers to some data that lives on the server and stores all the user info.
For example once a user logs in you might set a cookie containing a session ID like this:
and then store the relevant user info using that sid
as a key:
Here we're just putting the session data in an object, which means it will get deleted whenever the server restarts. Ideally this information would be stored in a database so it persists.
On subsequent requests the server would read the session ID cookie, then use that to look up the user info from the sessions
object.
This allows the server to control the "session"—if it needs to log a user out it can simply delete that entry from the sessions
storage.
It's important that the session ID is a long random string, so that nobody can guess them. Here's a good way to generate a random 18 byte long string in Node:
Also although we aren't directly storing user info in the cookie we still need to sign it. Otherwise if someone did find a way to guess the session IDs they could edit their cookie.
Change your GET /login
route to set a signed session ID cookie. This cookie should just contain a long random string
Store the user data in a global sessions object
Change the home handler to read the session ID cookie, look the user info up in the global sessions object, then log it
Change the GET /logout
route to remove the cookie and delete the session from the global sessions object
Practice rendering DOM elements using three different techniques.
Download starter files
Run npx servor workshop
to start a dev server
We'll be using three different methods to render the same dynamic UI to compare them. The UI will include a static single element (the title), plus a list of dynamic elements rendered from an array.
There is an array of dog objects in workshop/dogs.js
. In each challenge you'll need to import that data and render the following UI:
with a list item for each dog in the array.
The HTML document contains a single container to render all the UI into: <div id="app"></div>
.
document.createElement
The standard way to create new DOM elements in JavaScript is the document.createElement
method. You pass in a string for the HTML element you want to create and it returns the new DOM node. You can then manipulate the node to add attributes and content.
Once you've created DOM nodes you have to append them to their parent (and eventually to a node that is actually on the page). The classic way to do this is element.appendChild(newNode)
. This puts newNode
inside the element
node. If element
is already on the page then newNode
is rendered.
This has a big drawback: you can only append one thing at a time. This can lead to inefficient rendering. Each time you append a new element to the page the browser has to re-render everything. It's better to get all your DOM nodes ready then append them to the page in one go.
This is powerful when combined with the spread operator, as it means you can append an array of elements in one go:
Open app.js
and import the dogs array
Use document.createElement
and append
to render:
a page title
an unordered list
a list item for every dog in the array
Put all these elements inside the <div id="app">
in the HTML
createElement
We can write our own function to make it simpler to create DOM elements. Ideally we'll be able to pass in a tag name, some properties and some children, and have all the document.createElement
stuff handled automatically. E.g.:
We'll create this in a new file create-element.js
, so we can re-use it in multiple places if we need to.
First we need to create a new element using the tag
argument:
then we need to append all the properties from the props
object onto the DOM element:
Finally we need to append all the children to the new element
. We already saw how append
combines with the spread operator to add a whole array of children at once:
Don't forget to return the new element! We now have a nice helper function that we can export to use in our other file.
Use your new createEl
function to refactor your previous solution. Does it simplify the code?
innerHTML
This method almost feels like cheating. If you set an element's innerHTML
property to a string the browser will render it. This makes it a quick way to render a chunk of DOM, especially combined with template literals:
There are a couple of downsides to this method. First innerHTML
is considered a security risk. If you ever insert user input into an HTML string (like above) you run the risk of XSS attacks (cross-site scripting). A user could insert <script src="steal-credit-cards.js"></script>
as the name
variable, and your code would render that to the page, causing it to immediately execute.
It can also potentially be slow, since every time you change a node's innerHTML
property the browser must completely scrap and recreate its entire DOM tree. If you (for example) keep appending to innerHTML
in a loop you'll cause a lot of unnecessary re-renders. Nowadays browsers are so fast this is less of a concern.
Use innerHTML
and template literals to create the same UI as before.
<template>
elementThe template element is a special HTML element designed for rendering dynamic UI with JavaScript. The template (and all its contents) don't appear on the page. It's like a reusable stamp: you have to use JS to make a copy of the template, fill in the blanks, then append the copy to the page.
This is useful because we don't have to dynamically create elements: we can use the ones already created inside the template.
Use the template element to create the same UI. You'll need to edit the HTML file too.
It's a little annoying that templates have to be defined in the HTML file. We're doing all our rendering within JavaScript, so it would be nice to keep all the templates there too.
We can work around this by combining all three of our rendering methods. We can create a new template element within our JS, set its content using innerHTML
, then clone that template whenever we need a copy. The template is never actually on the page, it just lives inside our JS.
This also avoid the problems with innerHTML
, since we won't be passing user input into it. Our only use of innerHTML
will be the initial static markup.
Remove your template elements from the HTML file and instead create them with JavaScript. Refactor your previous solution to use this technique.
All of these techniques are valid, and all have their place. It's good to understand the platform you're working with, even if you end up using a framework like React that handles lower-level DOM manipulation for you.
There's a newer method with a nicer API: . This is supported by all browsers but IE11. It can take as many elements to append as you like, and it even supports strings to set text content.
The ...
is the —it gathers any additional arguments into an array. Any arguments after the props
object will go into a single array named children
.