Web
Iterators and Client-Server
##
Overview of Today's Class - Arrays and Iterables - Client-Server Applications
##
Educational Objectives On completion of this part, students will be able to: - Read and use the Array object's methods. - Resolve problems using the Array object's methods. - Read and use the functional methods of the Array object. - Resolve problems using the functional methods of the Array object. - Read and use the Iterator and Generator objects. - Read and use the Map and Set objects. - Describe the anatomy of a web application. - Describe alternatives to ExpressJS.
Javascript Arrays and Iterables
##
Arrays methods Recall the Array object. Its prototype has many methods. Some of the useful ones are: - `concat()` concatenates two or more arrays and returns a new array. - `join()` joins all elements of an array into a string. - `pop()` removes the last element from an array and returns that element. - `push()` adds one or more elements to the end of an array and returns the new length of the array. - `reverse()` reverses the order of the elements of an array. - `shift()` removes the first element from an array and returns that element. - `slice()` selects a part of an array, and returns it as a new array. - `sort()` sorts the elements of an array. - `includes()` determines whether an array contains a specified element. - `flat()` flattens an array up to the specified depth. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
##
Arrays methods Recall the Array object. Its prototype has many methods. Some of the useful ones are: - `concat()` concatenates two or more arrays and returns a new array. ```js const a = ['a', 'b', 'c']; const b = ['d', 'e', 'f']; const c = a.concat(b); console.log(c); // ['a', 'b', 'c', 'd', 'e', 'f'] console.log(a); // ['a', 'b', 'c'] console.log(b); // ['d', 'e', 'f'] ``` - `join()` joins all elements of an array into a string. ```js const a = ['a', 'b', 'c']; const b = a.join(' - '); console.log(b); // 'a - b - c' const c = [{ name: 'John' }, { name: 'Jane' }]; const d = c.join(', '); console.log(d); // '[object Object], [object Object]' ``` - `pop()` removes the last element from an array and returns that element. ```js const a = ['a', 'b', 'c']; const b = a.pop(); console.log(b); // 'c' console.log(a); // ['a', 'b'] ```
##
Arrays methods (join) Here's an example of using the `join()` method to concatenate the string representations of the people in an array: ```js // Define a class with a custom toString() method class Person { constructor(name, age) { this.name = name; this.age = age; } toString() { return `${this.name} (${this.age})`; } } // Create instances of the Person class const person1 = new Person('Alice', 30); const person2 = new Person('Bob', 25); const person3 = new Person('Charlie', 35); // Create an array containing instances of the Person class const people = [person1, person2, person3]; // Use join() to concatenate the string representations of the people const result = people.join(', '); console.log(result); // Output: Alice (30), Bob (25), Charlie (35) ```
##
Arrays methods Recall the Array object. Its prototype has many methods. Some of the useful ones are: - `push()` adds one or more elements to the end of an array and returns the new length of the array. ```js const a = ['a', 'b', 'c']; const b = a.push('d'); console.log(b); // 4 console.log(a); // ['a', 'b', 'c', 'd'] ``` - `reverse()` reverses the order of the elements of an array. ```js const a = ['a', 'b', 'c']; const b = a.reverse(); console.log(b); // ['c', 'b', 'a'] console.log(a); // ['c', 'b', 'a'] ``` - `shift()` removes the first element from an array and returns that element. ```js const a = ['a', 'b', 'c']; const b = a.shift(); console.log(b); // 'a' console.log(a); // ['b', 'c'] ```
##
Arrays methods Recall the Array object. Its prototype has many methods. Some of the useful ones are: - `slice()` selects a part of an array, and returns it as a new array. ```js const a = ['a', 'b', 'c']; const b = a.slice(1, 2); console.log(b); // ['b'] console.log(a); // ['a', 'b', 'c'] ``` - `sort()` sorts the elements of an array. ```js const a = ['c', 'a', 'b']; const b = a.sort(); console.log(b); // ['a', 'b', 'c'] console.log(a); // ['a', 'b', 'c'] const c = [{ name: 'John' }, { name: 'Jane' }]; const d = c.sort((a, b) => a.name.localeCompare(b.name)); // yes, the 'a' and 'b' are the parameters of the function and not the arrays // const d = c.sort((x, y) => x.name.localeCompare(y.name)); // equivalent console.log(d); // [{ name: 'Jane' }, { name: 'John' }] ``` Source : https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
##
Arrays methods sort() ```js // Define a class with a toString() method class Person { constructor(name, age) { this.name = name; this.age = age; } toString() { return `${this.name} (${this.age})`; } // compare age and name static compare(a, b) { if (a.age < b.age) { return -1; } if (a.age > b.age) { return 1; } if (a.name < b.name) { return -1; } if (a.name > b.name) { return 1; } return 0; } } // Create instances of the Person class const person1 = new Person('Alice', 30); const person2 = new Person('Bob', 25); const person3 = new Person('Charlie', 25); // Create an array containing instances of the Person class const people = [person1, person2, person3]; // Use sort() to sort the people people.sort(Person.compare); console.log(people); // Output: [Bob (25), Charlie (25), Alice (30)] ```
##
Arrays methods Recall the Array object. Its prototype has many methods. Some of the useful ones are: - `includes()` determines whether an array contains a specified element. ```js const a = ['a', 'b', 'c']; const b = a.includes('b'); console.log(b); // true const c = [1, 2, 3]; const d = c.includes('2'); console.log(d); // false console.log(c['2']); // 3 console.log(c[2]); // 3 ``` - `flat()` flattens an array up to the specified depth. ```js const a = [1, 2, [3, 4, [5, 6]]]; const b = a.flat(); console.log(b); // [1, 2, 3, 4, [5, 6]] console.log(a); // [1, 2, [3, 4, [5, 6]]] ```
##
Array's functional methods (1/4) The Array object also has some functional methods, which are: `forEach()` executes a provided function once for each array element. ```js const a = ['a', 'b', 'c']; const b = a.forEach(element => console.log(element)); console.log(b); // undefined console.log(a); // ['a', 'b', 'c'] ``` `map()` creates a new array with the results of calling a provided function on every element. ```js var a = ["apple", "banana", "pear"]; const b = a.map(a => a.length) console.log(b); // [5, 6, 4] console.log(a); // ["apple", "banana", "pear"] ```
##
Array's functional methods (2/4) `flatMap()` maps each element using a mapping function, and then flattens the resulting array by one level. ```js var a = ['Yverdon is', 'a', 'beautiful city']; a.flatMap(s => s.split(" ")); // First executes map: [['Yverdon', 'is'], 'a', ['beautiful', 'city']] // Then flattens: ['Yverdon', 'is', 'a', 'beautiful', 'city']. ``` `filter()` creates a new array with all elements that pass the test implemented by the provided function. ```js const words = ['Yverdon', 'is', 'a', 'beautiful', 'city']; const result = words.filter(word => word.length > 6); console.log(words); // ['Yverdon', 'is', 'a', 'beautiful', 'city'] console.log(result); // ['Yverdon', 'beautiful'] ``` https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array Flattening an array by depth `n` means replacing any sub-array it contains with the elements contained by that subarray, `n` times. Note that a `flat` method exists in `Array.prototype` which does exactly that, meaning that `flatMap` is equivalent to calling `map` and then `flat`.
##
Array's functional methods (3/4) `reduce()` execute a reducer function on each element of the array, resulting in a single value. And reduceRight() does the same, but from right to left. ```js const array1 = [1, 2, 3, 4]; // 0 + 1 + 2 + 3 + 4 const initialValue = 0; const sumWithInitial = array1.reduce( (accumulator, currentValue) => accumulator + currentValue, initialValue, ); console.log(sumWithInitial); // Expected output: 10 ``` `every()` tests whether all elements in the array pass the provided predicate ```js var a = [1, 2, 3]; a.every(a => a > 0) // true ```
##
Array's functional methods (4/4) `some()` tests whether some element in the array passes the provided predicate ```js var a = [1, 2, 3]; console.log(a.some(a => a > 2)) // true ``` `find()` and `findIndex()` return the value (or its index, respectively) of the first element in the array that satisfies the provided predicate. ```js var a = [1, 2, 3]; console.log(a.find(a => a > 2)) // 3 console.log(a.findIndex(a => a > 2)) // 2 console.log(a.find(a => a > 3)) // undefined ``` Here is an example of using `map`, `filter` methods together: ```js [1,2,3].map(a => a**2).filter(a => a > 4) // [9] ``` https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
##
Exercise Inverted Index Given a list of documents, create an inverted index. An inverted index is a dictionary where each word is associated with a list of the document identifiers in which that word appears. ```js let documents = [ "Hello Everyone", "Hello, World!", "The sky is blue", "The sky is dark" ]; let stopWords = ["the", "is"]; let invertedIndex = "..."; ``` Use the `replace`, `split`, `filter`, `map`, `flatMap`, and `reduce` methods to create the inverted index so that `console.log(invertedIndex)` prints: ```js { "hello": [ 0, 1 ], "everyone": [ 0 ], "world": [ 1 ], "sky": [ 2, 3 ], "blue": [ 2 ], "dark": [ 3 ] } ```
##
Solution: Inverted Index ```js let documents = [ "Hello Everyone", "Hello, World!", "The sky is blue", "The sky is dark" ]; let stopWords = ["the", "is"]; let invertedIndex = documents .map( (doc, index) => doc .replace(/[.,!]/g, "") .split(" ") .filter(word => !stopWords.includes(word.toLowerCase())) .map(word => [word.toLowerCase(), index]) ) // [[['hello', 0], ['everyone', 0]], [['hello', 1], ['world', 1]], ...] .flat() // [['hello', 0], ['everyone', 0], ['hello', 1], ['world', 1], ...] .reduce((acc, [word, index]) => { if (acc[word]) { acc[word].push(index); } else { acc[word] = [index]; } return acc; }, {}); console.log(invertedIndex); ```
##
Iterators **Iterators** are objects that provide a `next()` method which returns an object with two properties: - `value`: the next value in the iterator, and - `done`: whether the last value has already been provided. **Iterables** are objects that have a `Symbol.iterator` method that returns an iterator over them. ```js let idGenerator = {}; idGenerator[Symbol.iterator] = function() { return { nextId: 0, next() { if (this.nextId < 10) { return { value: this.nextId++, done: false }; } else { return { done : true }; } } } } for (let id of idGenerator) { console.log(id); // 0 /n 1 /n 2 /n ... 9 } ``` *Recall: The `for...of` loop requires its second operand to be an iterable.*
##
Generators - Convenient iterators Allows the definition of a function that can "return multiple times" with the `yield` keyword: ```js[3-8] let idGenerator = {}; idGenerator[Symbol.iterator] = function* () { let nextId = 0; while (nextId < 10) { yield nextId++; } }; for (let id of idGenerator) { console.log(id); // 0 /n 1 /n 2 /n ... 9 } ``` - `function*` declares a generator, which is a type of iterator, *not* a function. - `yield` effectively pauses execution after returning its operand. On a next call to the iterator, it will resume execution. - `yield*` is followed by another generator or iterable object and delegates the generation to them until they are empty.
##
Generators - Convenient iterators ```js let idGenerator = {}; ``` ```js idGenerator[Symbol.iterator] = function() { return { nextId: 0, next() { if (this.nextId < 10) { return { value: this.nextId++, done: false }; } else { return { done : true }; } } } } ``` ```js idGenerator[Symbol.iterator] = function* () { let nextId = 0; while (nextId < 10) { yield nextId++; } }; ``` ```js for (let id of idGenerator) { console.log(id); // 0 /n 1 /n 2 /n ... 9 } ```
##
Generators - Convenient iterators - Remarks When used as a class method, since there is no `function` keyword, the asterisk is put before the name : ```js class IdGenerator { *[Symbol.iterator]() { let nextId = 0; while (nextId < 10) { yield nextId++; } } } // Create an instance of the IdGenerator class const idGenerator = new IdGenerator(); // Iterate over the idGenerator instance using for...of loop for (let id of idGenerator) { console.log(id); // Output: 0 /n 1 /n 2 /n ... 9 } ```
##
Generators - Convenient iterators - yield* When used as a class method, since there is no `function` keyword, the asterisk is put before the name : ```js class IdGenerator { *[Symbol.iterator]() { yield* Array.from({ length: 10 }, (_, index) => index); } } // Create an instance of the IdGenerator class const idGenerator = new IdGenerator(); // Iterate over the idGenerator instance using for...of loop for (let id of idGenerator) { console.log(id); // Output: 0 /n 1 /n 2 /n ... 9 } ```
##
Useful Built-in Iterables The `Map` and `Set` objects are iterable and have a `forEach` method. The `Map` object holds key-value pairs and remembers the original insertion order of the keys. ```js let map = new Map(); map.set("key", "value"); map.get("key"); map.delete("key"); ``` The `Set` object lets you store unique values of any type. ```js let set = new Set(); set.add("value"); set.has("value"); set.delete("value"); ``` https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Object
##
Useful Built-in Iterables The iterable for Map iterates over its entries, which are arrays of the form [key, value]. Hence, the `forEach` method takes a callback function with two arguments, the key and the value: ```js map.forEach((key, value) => console.log(key, value)); ``` Similarly if using the `for of` syntax: ```js for (let [key, value] of map) { console.log(key, value); } ```
##
Exercice : Flattening generator Implémenter un générateur `flatten` qui aplatit ("flattens") un array. Il ne doit pas d'abord flatten puis itérer dessus. En d'autres termes, aucune méthode de array ne doit être utilisée. ```js const arr = [1, [2, [3, 4], 5], 6]; const flattened = [...flatten(arr)]; console.log(flattened); // [1, 2, 3, 4, 5, 6] ``` Notes:
```js function* flatten(arr) { for (const elem of arr) { if (Array.isArray(elem)) { yield* flatten(elem); } else { yield elem; } } } ```
##
Solution : Flattening generator Implémenter un générateur `flatten` qui aplatit ("flattens") un array. Il ne doit pas d'abord flatten puis itérer dessus. En d'autres termes, aucune méthode de array ne doit être utilisée. ```js const arr = [1, [2, [3, 4], 5], 6]; const flattened = [...flatten(arr)]; console.log(flattened); // [1, 2, 3, 4, 5, 6] ``` ```js function* flatten(arr) { for (const elem of arr) { if (Array.isArray(elem)) { yield* flatten(elem); } else { yield elem; } } } ```
##
Exercice : Sum of squares Écrire une fonction qui, en une chaine d'appels de méthodes, retourne la somme des carrés des éléments du tableau donné en argument. ```js const arr = [1, 2, 3, 4, 5]; const sum = sumOfSquares(arr); console.log(sum); // 55 (1^2 + 2^2 + 3^2 + 4^2 + 5^2) ``` Même question si l'input est une matrice (tableau de tableaux). Notes:
Array ```js function sumOfSquares(arr) { return arr .map(num => num ** 2) .reduce((acc, num) => acc + num, 0); } ``` Matrice ```js function sumOfSquares(matrix) { return matrix.flat() .reduce((sum, num) => sum + num ** 2, 0); } ```
##
Solution : Sum of squares Array ```js function sumOfSquares(arr) { return arr .map(num => num ** 2) .reduce((acc, num) => acc + num, 0); } ``` Matrice ```js function sumOfSquares(matrix) { return matrix.flat() .reduce((sum, num) => sum + num ** 2, 0); } ```
##
Exercice : Counting words Écrire une fonction qui prend une string en argument et, en une seule instruction, retourne un objet contenant, pour chaque mot de la string, son nombre d'apparition. ```js const text = "the quick brown fox jumps over the lazy dog"; const wordCount = countWords(text); console.log(wordCount); // Output: // { // "the": 2, // "quick": 1, // "brown": 1, // "fox": 1, // "jumps": 1, // "over": 1, // "lazy": 1, // "dog": 1 // } ```
##
Solution : Counting words ```js const countWords = () => { return text.split(" ") .map(word => word.toLowerCase()) .filter(word => /^[a-z]+$/.test(word)) .reduce((count, word) => { count[word] = (count[word] || 0) + 1; return count; }, {}); } const text = "the quick brown fox jumps over the lazy dog"; const wordCount = countWords(text); console.log(wordCount); // Output: // { // "the": 2, // "quick": 1, // "brown": 1, // "fox": 1, // "jumps": 1, // "over": 1, // "lazy": 1, // "dog": 1 // } ```
Client-Server Applications
##
Anatomy of a web application
https://developer.mozilla.org/en-US/docs/Learn/Server-side/First_steps/Client-Server_overview Notes: 1. The client makes an HTTP GET request to the server 2. The server receives the request and forwards the requested data to the web application 3. The web application might query a database to obtain necessary data for the response 4. The web application generates an HTML page and sends it back to the web server 5. The web server sends the HTTP response containing the document to the client 6. The client renders the document. If it contains references to external resources, the client makes additional HTTP requests to the web server. 7. The web server might also retrieve files directly from the file system, for example images, CSS stylesheets, or JavaScript files.
##
HTML Forms (Client-side) HTML forms are the most common way to collect user input, send it to a server, and receive a response. The form itself is written in HTML, and the server's response is typically HTML as well. ```html
E-mail:
Title:
Content:
Save
``` - The form's action attribute defines where the form data is sent. - The form's method attribute defines how the data is sent. - The form's input elements define the data fields the form collects. - The form's button element defines the button the user clicks to send the data to the server. https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Your_first_HTML_form
##
HTML Forms (Server-side) The server-side code that processes the form data is typically written in a server-side language such as PHP, Python, or Java. ```php ``` Here, the server-side code retrieves the data from the form using the $_POST array. The data is then processed and a response is sent back to the client.
##
ExpressJS (Server-side) Today, Javascript is a popular choice for server-side programming. Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web applications. Web frameworks usually come with: - A **middleware** layer that processes HTTP requests and responses. - A **router** that helps at mapping HTTP requests to the appropriate code. - A **template engine** that helps at generating HTML pages. https://expressjs.com/ Notes: The line between middleware and router is sometimes blurry. The middleware layer is usually used to process the request and response, while the router is used to map the request to the appropriate code.
##
ExpressJS (Server-side) ```js const express = require('express') // Create an express app const app = express(); // Serve GET requests on a given url app.get(url, (req, res) => { // handle request (req.cookies, req.body, req.ip, req.query) let args = req.query // Object with one property per url parameter // handle response (res.status, res.redirect, res.json) res.status(200) res.send(...); // Sends an HTTP response }); // Serve POST requests on a given url. Similar exist for PUT and DELETE requests. app.post(url, (req, res) => { ... }); // Serve static files (images, css files, js files, etc) in a given directory app.use(express.static(dir_name)); // Launch the server app.listen(port); ``` https://github.com/web-classroom/example-express Notes: The callback functions passed to the app.get and app.post methods are called **middleware**. There can be multiple per route, and they are executed in the order they are defined, where the last one in the chain sends the response back to the client. One can also jump to the next middleware early using the next() function. They are used to process the request and response. Technically, the application's logic runs in the context of a middleware, but it is common to separate the code handling HTTP requests and responses from the code implementing the application's logic.
##
ExpressJS Alternatives Express is not the only web framework for Node.js. Here are some alternatives: - **Koa**: A smaller and more expressive framework. - **Fastify**: A fast and low overhead web framework. - **NestJS**: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications.
##
Multi-page applications (MPA) It is the traditional solution : for each request, a new page is retrieved and entirely rendered, even if the change affects only a small part of the page. Most common type of web application, strongly based on the client-server model, typically written in a server-side language (PHP, Python, or Java). When in doubt, start with a multi-page application. ### Advantages Easy to develop, maintain, scale, test, secure, reference, deploy, and debug. ### Disadvantages Not very user-friendly, not very responsive, not very interactive.
##
MVC Pattern The Model-View-Controller (MVC) architectural pattern is commonly used for separating concerns of (1) data, (2) user interface, and (3) program logic. - The **model** is responsible for managing the data. It notifies the view of any changes in the data, and reacts to notifications from the controller. - The **view** is responsible for displaying the data to the user. It receives data from the model and sends user actions (e.g. button clicks) to the controller. - The **controller** is responsible for handling user actions and updating the model and the view accordingly. This allows for separation of concerns, code reuse, and parallel development. It is often used to organize the code in a multi-page application.
##
MPA and MVC in the real world **Stackoverflow** is a multi-page application that uses the MVC pattern. **Hacker News** is a multi-page application that uses the MVC pattern. **Facebook** and **Reddit** started as a multi-page application that used the MVC pattern. When exposed to trendy Web technologies, do a quick sanity check on BuildWith. https://trends.builtwith.com/