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:
- Is the code reviewed (if not recommended) by coding gurus?
- Does the code conform with the official documentation (usually in English)?
- When was the code written? Frontend technologies evolves at lightning speed, and the code you found can be outdated.
- 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
- Windows/Linux: F12
- Mac OS: ⌥+⌘+I
Try it yourself!
Use devtools to modify the list below. Replace the second item with "Tacos".- Sandwich
- French fries
- Spaghetti
Also, have a try with CSS styles...
Try it yourself!
Use devtools to modify the styles of contents below.
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
Null and Undefined
JavaScript has 2 categories of nullish values. One isnull
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 }) trueAnother 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. 2Before
??
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.- String
- Boolean
- Number
- BigInt
- Symbol
- Function
- Object
- Null
- Undefined
> 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' falseWhat'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.
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.49794Code your solution below:
function dist(vec, options) {
}
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?
var
have 2 types of lexical scopes.
-
Global Scope(when declared outside of any function):
the variable then becomes a property of
globalThis
(window
in browsers andglobal
in Node.js) - Functional Scope(when declared in a function): the variable is accessible in the whole function
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 changedThey 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 10For 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.
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
.
console.log(parseColor('#ffa500')) confetti()
Useful APIs
Since the time of this workshop is limited, we'll only have a look atElement.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 TutorialBox 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 ofdisplay
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--modifierwhere 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.- GET: Accepts no body, but search parameters. Only used to retrieve information / get a list of resources.
- POST: Accepts a body, and use the data in body to create new resources on the server.
- PUT: Accepts a body, and use the body to replace a resource on the server.
- PATCH: Accepts a body, and use the body to partially edit a resource on the server.
- DELETE: remove a resource on the server.
- OPTIONS: Preflight requests. Used in CORS.