Frontend Intro

HTML5 Tags

Every single beginner loves copy-pasting HTML code from the web. I don't think that's wrong, actually. However, one should pay more attention to "where the code comes from".

Ask yourself several questions next time before pressing Ctrl + C and Ctrl + V 's:

  1. Is the code reviewed (if not recommended) by coding gurus?
  2. Does the code conform with the official documentation (usually in English)?
  3. When was the code written? Frontend technologies evolves at lightning speed, and the code you found can be outdated.
  4. does it contain depreciated usage of APIs / HTML tags? (e.g. <marquee>, <center>, ...)

Personally speaking, I'm strongly against copying HTML code from some public blog site (especially Wordpress and CSDN). That's because experienced frontend engineers tend to run their own blog sites. StackOverflow is a good place for green hands, since answers there are selected and edited by pros.

For details about HTML5 tags, do not hesitate to refer to Mozilla Developer Network. Articles there are eagerly translated into dozens of languages (including Chinese) by volunteers, so language won't be a problem.

stop using div tags everywhere

MDN Docs describe div as a generic container for flow content. In the years before HTML5, it had been used as a container for literally anything. However, this is not a good practice because HTML5 introduced content sectioning tags. Take a look at those elements on MDN.

Try it yourself!

Design a blog landing page. The page contains a header (with a h1 title inside), 2 articles and a footer (containing copyright notices, etc).
Note: articles should be wrapped by a main tag. You only need to code the parts inside body tag.

Devtools

In this section, I assume that you're using Google Chrome or Mozilla Firefox. The new Microsoft Edge also works in most cases, since it's a chromium-based browser.

How to open

Try it yourself!

Use devtools to modify the list below. Replace the second item with "Tacos".
Lunch
  • Sandwich
  • French fries
  • Spaghetti

Also, have a try with CSS styles...

Try it yourself!

Use devtools to modify the styles of contents below.
This is a simple text. This sentence should be red. This sentence shouldn't be visible.

Network tab

When it comes to frontend, speed is the most important factor. Poorly designed webpages can take a minute to load, leading to awful user experience. There're too much to talk about when it comes to page speed optimization - we won't cover it today, because some tricks can be advanced. However, you can have a basic idea about such optimization by simply navigating to "network" tab.

Question

How many font files are loaded by this page?

Debugging

The debugging tab can help with your debugging process. Have a try with the example below.

Try it yourself!

The correct answer is "foo". Change the JavaScript code underneath so any non-empty string is a correct answer.

JavaScript

Many find JavaScript to be annoying, but after understanding its basics you may feel like saved. If used properly, its dynamic weak typing can actually accelerate your development, despite some drawbacks.

Common Pitfalls and Solutions

JavaScript Coercion Meme

Null and Undefined

JavaScript has 2 categories of nullish values. One is null and another is undefined. However they are used in different ways. While undefined are usually used as a default value if nothing has been specified explicitly, null is used as a default value for objects.
> typeof null
'object'
> typeof undefined
'undefined'
You should be very careful with arguments of a function: they can be nullish values!

Useful operators when dealing with nullish values. Newer versions of JavaScript introduced some operators to simplify the handling of nullish values. The first is optional chaining(?.):

> function hasDimension(options) { return typeof (options?.dimension) == 'number'; }
> hasDimension(null)
false
> hasDimension({})
false
> hasDimension({ dimension: 4 })
true
Another operator is the nullish coalescing operator(??). This can provide a default value in case of nullish variables:
> const options = { distance: 'minkovski' }
> const dim = options.dimension ?? 2 
> dim // Suppose we'd like to provide a default in case of nullish values
2
> options.dimension || 2 // interesting enough, || also works here! However, limits apply.
2
> options.dimension = 0
0
> options.dimension ?? 2 // Output is 0 !!!
0
> options.dimension || 2 // This is WRONG.
2
Before ?? is a thing, programmers used ||. Its truthy value is calculated after converting each operand into boolean variables. However, its return value is selected from original operands.

Note: not every browser support these operators, but modern ones do. Notoriously, Internet Explorer do not support them. (btw who on earth is still using IE?)

Basic types

The types below are the basic types in JavaScript. Some common mistakes: first, pay attention to comparision and arithmetic operators. Quirks are that numbers can be casted into strings, and booleans can be casted into numbers.
> 1 == true
true
> 0 == +null // + is a unary operator
true
> 6 == '6'
true
> 1+2+3+4+5+'6'
'156'
To avoid these behaviors, you should change your =='s into ==='s.
> 6 == '6'
true
> 6 === '6'
false

What's more, avoid using the typeof operator on complex objects, since they'll always return "object".
> typeof (new Array())
'object'
> typeof (new Date())
'object'
> typeof (new Set())
'object'
Instead, you should use instanceof:
> const array = new Array()
> array instanceof Array
true
> array instanceof Object
true
> array instanceof Date
false

Try it yourself!

The following function calculated the distance to origin of a point \(u \in \R^n\), which is given as an array of numbers (if its not an array, or its length is 0, return undefined). The third argument of the function is an object (possibly null!) containing options for the function. If the 3rd argument contains distance: "manhattan" then the Manhattan distance should be calculated. If distance: "minkovski" then the Minkovski distance should be calculated. Otherwise(i.e. it does not contain distance, or options == null), the Euclidean distance will be calculated.

In the case of Minkovski distance, if another property dimension is given, then it will be used as the parameter \( p \) for Minkovski distance; otherwise it will fall back to \( p = 2 \), which is just Euclidean distance.

\begin{align} \text{Manhattan}(u) &= \sum |u_i|\\ \text{Minkovski}(u) &= \left(\sum u_i^p\right)^{1/p} \end{align} several examples here:
dist(null) // return undefined
dist([]) // return undefined
dist([3, 4]) // return: 5
dist([3, 4], {}) // return: 5
dist([3, 4], {distance: "manhattan"}) // returns: 7
dist([3, 4], {distance: "minkovski"}) // returns: 5
dist([3, 4], {distance: "minkovski", dimension: 2}) // returns: 5
dist([3, 4], {distance: "minkovski", dimension: 3}) // returns: about 4.49794
Code your solution below:
function dist(vec, options) {
}
The exercise above contains many points covered in this chapter. For your convenience, the answer is listed below.
A Possible Solution
function dist(vec, options) { // this line has been written for you.
  if (! (vec instanceof Array) || vec.length == 0) {
    return
  }
  const distanceType = options?.distance ?? "euclidean"
  let dimension = options?.dimension ?? 2

  switch (distanceType) {
    case "manhattan":
      return vec.reduce((s, x) => s + Math.abs(x))
    case "euclidean":
      dimension = 2
    case "minkovski":
      return Math.pow(vec.reduce((s, x) => s + Math.pow(x, dimension), 0),
                      1.0 / dimension)
  }
} // this line has been written for you.

Object Inheritance

In JavaScript, object inheritance, which is based on prototype rather than classes, is far different from traditional Object-Oriented practices. In fact there's no real "classes" in JavaScript! Each object is created with a function (and of course, new operator). When a property is not found on an object, a search along the prototype chain is performed. Read more about this on MDN.

However, newer JavaScript language specifications have defined the keyword class and extends, which can be used to emulate the behavior of inheritance based on classes.

class Vec2D {
  constructor (x = 0, y = 0) { // default parameter in case of undefined
    this.x = x; this.y = y;
  }
  length () {
    return Math.sqrt(x * x + y * y)
  }
}

class MyVec2D extends Vec2D {
  clear () {
    this.x = this.y = 0
  }
}

Another thing which is worth noticing is the this keyword. If a method of an object is called (e.g. obj.foo()), this is binded to the method itself (i.e. obj) in the function body. However this can also be manually binded.

const obj = {
  foo: 1
}
function testThis() {
  console.log(this)
}
testThis.apply(obj) // output is { foo: 1 }

There are actually 2 types of function in JavaScript, that is, "normal" functions and "arrow" functions. Arrow functions can be created like this:

const logEvent = (event) => { console.log(event.target) }
// logEvent is an arrow function (pay attention to curly braces)
const xPlus2 = x => x + 2
// xPlus2 receives a number and add 2 to it, then return the added value
// there's no curly braces, hence the value of the expression after => is returned.
Attention: You cannot bind this to an arrow function. They actually do not have their own lexical this.
const obj = {
  foo: 1
}
function testThis() {
  const t = (() => {
    console.log(this)
  }).bind({baz: 2}) // this will NOT work
  t()
}
testThis.apply(obj) // output is still { foo: 1 }

Syntactic Sugar 🍬

Talk is cheap, just look at the code below!
/* Object related */
const baz = 'foobar'
const obj = {
  foo () { // no need to write `"foo": function()`
    return 'bar'
  },
  bar: 1, // quotation marks in keys can be omitted in most cases
  baz // equivalent to "baz": baz
}

/* Unpacking */
const { bar } = obj // now bar == 1
const arr = [6, 2, 1]
const [a0, a1] = arr // now a0 == 6, a1 == 2, arr[2] is discarded
const powArgs = [3, 2]
console.log(Math.pow(...powArgs)) // argument unpacking, output is 9
function testFunc(...args) {
  console.log(args.length) // now args is an array containing all arguments.
  // NOTE: use this to replace the use of `arguments`
}
testFunc(3, 4, 5) // output is 3

/* For loops */
for (let index in arr) { // in -> index
  console.log(index)
} // 0 1 2
for (let value of arr) { // in -> index
  console.log(value)
} // 6 2 1

Hoisting

Let's just start with a question:

Question

function main() {
  var output = ''
  for (var i = 1; i <= 3; i++) {
    setTimeout(() => {output += String(i)}, 1000)
  }
  setTimeout(() => console.log(output), 1500)
}
main()

What's the output of the code above?

Variable declared with the keyword var have 2 types of lexical scopes.

There're no other scopes, and particularly, JavaScript will not create a new lexical scope of var for each pair of curly braces.

A mental model of var's is: all their declarations are moved to the start of function (or start of file) before any code is executed.

Most of the cases hoisting is not the expected behavior. Switching to let / const:

{
  let a = 1
  // console.log(b); // error!
  {
    let b = 2
    console.log(b)
  }
}
//console.log(a) // error!
const c = 4
//c = 5 // error!
const obj = {}
obj.foo = 'bar' // this works because the reference to object is not changed 
They just behave like variables in most languages, and global variabled declared with let / const will not become a property of globalThis. Sweet.

MapReduce

The methods map/forEach/reduce exist on any array instance (again, in modern browsers). Use them instead of for loops to simplify your code.
const arr = [1, 2, 3, 4]
console.log(arr.map(x => x + 2)) // [3, 4, 5, 6]
console.log(arr.reduce((s, x) => s + x)) // sum is 10
For more FP-style functions, try lodash.

Promise and Async functions

JavaScript code is ran in a single thread, however it is capable of handling a heavy workload. The magic behind is "event loop": methods of time-consuming actions, like HTTP Requests, accept a callback parameter. Once the action is done, the callback function will be called.

However, having too much callback functions is not a good thing. Look at the Node.js code below:

const fs = require('fs')
fs.readFile('./a.txt', (dataA) => {
  console.log(dataA.length)
  fs.readFile('./b.txt', (dataB) => {
    console.log(dataB.length)
    fs.readFile('./c.txt', (dataC) =>{
      console.log(dataC.length) // nested functions!
    })
  })
})
Promise provides a way of getting rid of nested functions.
const { readFile } = require('fs/promises');

readFile('./a.txt').then(
  (x) => {
    console.log(x.length)
    return readFile('./b.txt')
  }
).then(
  (x) => {
    console.log(x.length)
    return readFile('./c.txt')
  }
).then(
  (x) => {
    console.log(x.length)
  }
)
The code above used the Promise API of file reading in Node.js. A promise can be constructed in the following way:
const pr = new Promise((resolve, reject) => {
  // asynchronous operations
  // when ready, call resolve(results)
  // in case of error, call reject(error)
})
pr.then((results1) => {
  // what to do in the next step?
  // if another promise is returned, the next handler will wait for it
}).then((results2) => {
  // do something to the resolved result of the previous promise
})
Promises actually form a chain, just like callback chain, with nested functions replaced by resolve and reject.

Try it yourself!

Promisify the setTimeout API: create a function named timeout, which takes an integer (representing timeout in milliseconds) as argument, and returns a promise that resolves after the certain timeout.

Code your solution below:
function timeout(msecs) {
}
A possible solution
function timeout(msecs) {
  return new Promise(resolve => setTimeout(resolve, msecs))
}

Async and Await

These 2 keywords are just syntactic sugar; a certain async function

async foo() {
  // block 1
  let rv = await anotherAsyncFunction();
  // block 2
}
const pr = foo()
can be directly translated into:
const pr = new Promise((resolve, reject) => {
  // block 1
  return anotherAsyncFunction().then((rv) => {
    // block 2
  }).catch(e => reject(e))
})
This syntactic sugar can turn promise-based code into normal code that runs "sequentially".

Let's rewrite the code of reading 3 text files with async's!

const { readFile } = require('fs/promises');

async function main() {
  let x
  x = await readFile('./a.txt')
  console.log(x.length)
  x = await readFile('./b.txt')
  console.log(x.length)
  x = await readFile('./c.txt')
  console.log(x.length)
}

main()
Note: newer versions of V8 Engine support await outside of functions. However we'll just stick to using it in async functions.

A Brief Explanation on Package Management

There're too much to talk about on this matter: JavaScript had had no "import" before several years ago. If your code depend on other libraries, you should add the script tag of the library before the script tag of yours. Soon as the number of number of dependencies increase, your script tags become a mess, because they have to be loaded in the correct order.

Then tools like RequireJS appeared. It required all scripts following a specific module definition format (AMD/UMD/CommonJS), and the tool handles all the importing for you. An example of AMD Module looks like this:

define(['jquery'], function ($) {
  $(function() {
    // blahblahblah
  }) // run after the page is loaded
})
However this is still annoying: you have to download all dependencies of your code manually from the web, and loading too much dependencies is certainly detrimental to the load speed.

People then turned to Node.js, since it has an package management system called npm. They coded build tools for frontend code, which are called bundlers. The popular bundlers are: Webpack, Rollup(used by vue-cli), Browserify, to name but a few. Bundlers came out and dominated frontend development till now. They build the production-ready code from your original codebase, help with CommonJS Style require statements, and "polyfill" your code when needed. As a result, you're able to use require in frontend code just as in Node.js, and you can use the latest syntax of JavaScript without breaking backward compatibility.

To understand the configuration of bundlers and CLI build programs (like vue-cli), you should have some knowledge about Node.js. It's not hard to learn since the language is the same as that in browsers, with some API changes. Refer to their document, and you'll learn about packages.json, node_modules and many more.

The ES Modules are introduced by ECMAScript 6 in 2015. They provided support for modules in the language itself, rather than an addon or a build tool. Most bundlers also support ES Module Syntax (just like you've probably seen in Vue/React projects). However, the syntax used in bundlers (path or package in node_modules) is slightly different to the language specification. If you want to use imports directly in browsers, you should use a relative or absolute path to the module, because there's no such thing as node_modules in this context.

Note: do not try to use import directly on CommonJS modules in the browser. If you're using a bundler, then everything's fine since the bundler will generate some code to help with the interoperability. However, the native import statement is incompatible with CommonJS modules (because you don't have require in a browser). Use Skypack to help you translate CommonJS modules to ES modules. This service allows you to import any package on NPM Registry into your code directly, since it does the transpilation under the hood.

Fact: The successor of Node.js, Deno, abandoned the design of node_modules and switched to importing from URLs with ES6 import.

Try it yourself!

Use ES6 syntax to import 2 modules: one at https://cdn.skypack.dev/canvas-confetti, another at https://cdn.skypack.dev/parse-color. Then inspect the second URL in a new tab manually to see how skypack transpiled the original code and removed calls to require.

Code your solution below:
console.log(parseColor('#ffa500'))
confetti()

Useful APIs

Since the time of this workshop is limited, we'll only have a look at Element.querySelector and fetch. Again, refer to MDN when you're puzzled by API-related problems.
document.querySelector('pre.shiki') // select all <pre> tags with class shiki

fetch('https://jsonplaceholder.typicode.com/posts')
  .then(x => x.json()).then(x => console.log(x.length)) // perform a GET request to the server
fetch('https://jsonplaceholder.typicode.com/posts', { // perform a POST request to the server
  method: 'POST',
  body: JSON.stringify({
    title: 'foo',
    body: 'bar',
    userId: 1,
  }),
  headers: {
    'Content-type': 'application/json',
  },
})
.then(x => x.json())
.then(x => console.log(x))

Styling

A Good Tutorial

Box Model

Open the Devtools to have a look: where's margin / padding / border?
* {
  box-sizing: border-box;
}

Block vs Inline

There're generally 2 basic types of display in CSS. Block elements take all the width of its parent element, and have properties like margin / padding. For inline ones margin-top/margin-bottom simply don't work, and they do not expand horizontally like block elements.

BEM Naming Convention

Most of the CSS Classes are named like
.block__element--modifier
where block is the parent container, element is the class name for target, and modifier indicates its state.

How to vertically center an element?

Use the following class on its parent element:
.vert-center {
  display: grid;
  align-items: center;
}
An example is given here:

Here CSS Grids are used. CSS Tricks have a complete guide on this. You may be interested in display: flex, which is also new in CSS3.

Style a button

We will cover some basic usage of CSS by styling a toggle button.

Responsive Web Design and CSS Media Queries

To start with, use the following code in your HTML Header:

And then apply different style to different screen sizes:
/* Part of CSS Code for this tutorial */
body {
  margin: 20px;
  background-color: #eee;
  font-family: 'Montserrat';
}

@media only screen and (min-width: 850px) {
  body {
    margin-left: 20vw;
    margin-right: 20vw;
  }
}
Open this page on your phone, and find out the differences in styling. Tutorial

HTTP Basics

The basic methods, along with their usage in RESTful APIs, are listed below. Example of JSONPlaceholder.

Alternatives to RESTful API

GraphQL is a good alternative to RESTful API, since you can get loads of resources in a single request. On the other hand, RESTful APIs sometimes send too much requests, making your webpage laggy.