diff --git a/lessons/lesson25/code/eventEmitter/.codesandbox/workspace.json b/lessons/lesson25/code/eventEmitter/.codesandbox/workspace.json new file mode 100644 index 0000000..1870c7a --- /dev/null +++ b/lessons/lesson25/code/eventEmitter/.codesandbox/workspace.json @@ -0,0 +1,8 @@ +{ + "responsive-preview": { + "Mobile": [320, 675], + "Tablet": [1024, 765], + "Desktop": [1400, 800], + "Desktop HD": [1920, 1080] + } +} diff --git a/lessons/lesson25/code/eventEmitter/index.html b/lessons/lesson25/code/eventEmitter/index.html new file mode 100644 index 0000000..9f6c78c --- /dev/null +++ b/lessons/lesson25/code/eventEmitter/index.html @@ -0,0 +1,13 @@ + + + Parcel Sandbox + + + + +

Event Emitter / Event Bus

+
+ + + + diff --git a/lessons/lesson25/code/eventEmitter/package.json b/lessons/lesson25/code/eventEmitter/package.json new file mode 100644 index 0000000..b82ef30 --- /dev/null +++ b/lessons/lesson25/code/eventEmitter/package.json @@ -0,0 +1,21 @@ +{ + "name": "event-emitter", + "version": "1.0.0", + "description": "", + "main": "index.html", + "scripts": { + "start": "parcel index.html --open", + "build": "parcel build index.html" + }, + "dependencies": { + "@types/jest": "26.0.21", + "parcel-bundler": "^1.6.1" + }, + "devDependencies": { + "typescript": "4.2.3" + }, + "resolutions": { + "@babel/preset-env": "7.13.8" + }, + "keywords": [] +} diff --git a/lessons/lesson25/code/eventEmitter/src/EventEmitter.test.ts b/lessons/lesson25/code/eventEmitter/src/EventEmitter.test.ts new file mode 100644 index 0000000..92618f8 --- /dev/null +++ b/lessons/lesson25/code/eventEmitter/src/EventEmitter.test.ts @@ -0,0 +1,80 @@ +import { EventEmitter } from "./EventEmitter"; + +describe("EventEmitter", () => { + describe("formal public interface", () => { + it("is a constructor", () => { + expect(typeof EventEmitter).toBe("function"); + expect(new EventEmitter() instanceof EventEmitter).toBe(true); + }); + + it("has public methods", () => { + const eventEmitter = new EventEmitter(); + + expect(typeof eventEmitter.on).toBe("function"); + expect(typeof eventEmitter.off).toBe("function"); + expect(typeof eventEmitter.trigger).toBe("function"); + }); + }); + + describe("runtime logic", () => { + let eventEmitter: EventEmitter; + const eventName = "eventName"; + const eventPayload = { name: "Bob" }; + + beforeEach(() => { + eventEmitter = new EventEmitter(); + }); + it("allows to listen to the events", () => { + const spy = jest.fn(); + eventEmitter.on(eventName, spy); + expect(spy).not.toHaveBeenCalled(); + eventEmitter.trigger(eventName, eventPayload); + expect(spy).toHaveBeenCalledWith(eventPayload); + }); + + it("supports multiple listeners for the event", () => { + const spy1 = jest.fn(); + const spy2 = jest.fn(); + eventEmitter.on(eventName, spy1); + eventEmitter.on(eventName, spy2); + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).not.toHaveBeenCalled(); + eventEmitter.trigger(eventName, eventPayload); + expect(spy1).toHaveBeenCalledWith(eventPayload); + expect(spy2).toHaveBeenCalledWith(eventPayload); + }); + + it("allows to unsubscribe from the events", () => { + const spy1 = jest.fn(); + const spy2 = jest.fn(); + eventEmitter.on(eventName, spy1); + eventEmitter.on(eventName, spy2); + eventEmitter.off(eventName, spy1); + eventEmitter.trigger(eventName, eventPayload); + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).toHaveBeenCalledWith(eventPayload); + }); + + describe("edge cases", () => { + it("handles events with no listeners", () => { + expect(() => { + eventEmitter.trigger(eventName, eventPayload); + }).not.toThrowError(); + }); + + it("handles invalid unsubscriptions", () => { + eventEmitter.on("x", jest.fn()); + expect(() => { + eventEmitter.off("x", () => {}); + eventEmitter.off(eventName, () => {}); + }).not.toThrowError(); + }); + + it("handles triggers with no subscriptions", () => { + expect(() => { + eventEmitter.trigger(eventName, eventPayload); + }).not.toThrowError(); + }); + }); + }); +}); diff --git a/lessons/lesson25/code/eventEmitter/src/EventEmitter.ts b/lessons/lesson25/code/eventEmitter/src/EventEmitter.ts new file mode 100644 index 0000000..c573ed4 --- /dev/null +++ b/lessons/lesson25/code/eventEmitter/src/EventEmitter.ts @@ -0,0 +1,3 @@ +export class EventEmitter { + // @todo: put your code here +} diff --git a/lessons/lesson25/code/eventEmitter/src/index.ts b/lessons/lesson25/code/eventEmitter/src/index.ts new file mode 100644 index 0000000..f0727cd --- /dev/null +++ b/lessons/lesson25/code/eventEmitter/src/index.ts @@ -0,0 +1,21 @@ +// import { EventEmitter } from "./EventEmitter"; + +// (document.querySelector("#app") as HTMLElement).innerHTML = ` +// +//

+// +// `; +// const input1 = document.querySelector("input[name=input1") as HTMLInputElement; +// const input2 = document.querySelector("input[name=input2") as HTMLInputElement; +// const header = document.querySelector("h1") as HTMLHeadingElement; + +// const eventEmitter = new EventEmitter(); + +// eventEmitter.on("changeText", (text) => (header.innerHTML = text)); + +// input1.addEventListener("keypress", (ev) => +// eventEmitter.trigger("changeText", (ev.target as HTMLInputElement).value) +// ); +// input2.addEventListener("keypress", (ev) => +// eventEmitter.trigger("changeText", (ev.target as HTMLInputElement).value) +// ); diff --git a/lessons/lesson25/code/eventEmitter/tsconfig.json b/lessons/lesson25/code/eventEmitter/tsconfig.json new file mode 100644 index 0000000..587af1c --- /dev/null +++ b/lessons/lesson25/code/eventEmitter/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "module": "commonjs", + "jsx": "preserve", + "esModuleInterop": true, + "sourceMap": true, + "allowJs": true, + "lib": ["es6", "dom"], + "rootDir": "src", + "moduleResolution": "node" + } +} diff --git a/lessons/lesson25/images/EventBus.jpeg b/lessons/lesson25/images/EventBus.jpeg new file mode 100644 index 0000000..7948f81 Binary files /dev/null and b/lessons/lesson25/images/EventBus.jpeg differ diff --git a/lessons/lesson25/images/ObservableUML.png b/lessons/lesson25/images/ObservableUML.png new file mode 100644 index 0000000..44f574a Binary files /dev/null and b/lessons/lesson25/images/ObservableUML.png differ diff --git a/lessons/lesson25/lesson.md b/lessons/lesson25/lesson.md index 6fb9e08..d81329c 100644 --- a/lessons/lesson25/lesson.md +++ b/lessons/lesson25/lesson.md @@ -1 +1,225 @@ -# Lesson 25 +--- +title: Занятие 25 +description: Связь модулей - от интерфейсов до EventBus +--- + +# OTUS + +## Javascript Basic + + + +## Вопросы? + + + +## Связь модулей - от интерфейсов до EventBus + + + +### Разберемся с задачей + + + +Для начала два термина - **связность(_cohesion_)** и **связанность(_coupling_)**. + +[Связность]() - на сколько составные части направлены на решение одной задачи. + +[Связанность]() - на сколько одни модули зависят от других (и как много они знают друг о друге) + + + +[Качественный дизайн обладает слабой связанностью (low coupling) и сильной связностью (high cohesion).](https://medium.com/german-gorelkin/low-coupling-high-cohesion-d36369fb1be9) + +Это значит, что программный компонент имеет небольшое число внешних связей и отвечает за решение близких по смыслу задач. + + + +**Слабое зацепление (Low Coupling)** и **Высокая связность (High Cohesion)** это 2 из 9 [**шаблонов GRASP**]() + + + +Высокая связность говорит об эффективности программы (или ее отдельных модулей). + +Низкая связанность означает легкость рефакторинга и переиспользуемость кода. + + + +### Вопросы? + + + +### Наблюдатель (Observer) + + + +[Наблюдатель](https://refactoring.guru/ru/design-patterns/observer) - подход (паттерн), позволяющий одним объектам следить и реагировать на события, происходящие в других объектах. + + + + + + + +На самом деле вы с ним уже работали - это [EventTarget](https://developer.mozilla.org/ru/docs/Web/API/EventTarget) + + + +Данный шаблон часто применяют в ситуациях, в которых отправителя сообщений не интересует, что делают получатели с предоставленной им информацией. + + + +Может быть представлен как + + + +```ts +IObservable { + addObserver(event, handler) + removeObserver(event, handler) + notifyObserver(event, data) +} +``` + + + +или + + + +```ts +EventTarget { + addEventListener(event, handler) + removeEventListener(event, handler) + dispatchEvent(event) +} +``` + + + +или + + + +```ts +Backbone.Events { + on(event, handler) + off(event, handler) + trigger(event) +} +``` + + + +Иногда могут добавлять вспомогательные методы, например + + + +```ts +Backbone.Events { + // ... + once(event, handler) +} +``` + + + +```ts +document.querySelector(element).addEventListener("click", (ev) => { + alert("Boom!"); +}); +``` + + + +Оговорка: чаще всего обработчиком события является функция. Но это также может быть и объект ([EventListener](https://developer.mozilla.org/ru/docs/Web/API/EventListener)) - в зависимости от реализации. + + + +### Вопросы? + + + +### Посредник (Mediator) + + + +[Посредник](https://refactoring.guru/ru/design-patterns/mediator) - это поведенческий паттерн проектирования, который позволяет уменьшить связанность множества классов между собой, благодаря перемещению этих связей в один класс-посредник. + + + +**Задача:** Обеспечить взаимодействие множества объектов, сформировав при этом слабую связанность и избавив объекты от необходимости явно ссылаться друг на друга. + +**Решение:** Создать объект, инкапсулирующий способ взаимодействия множества объектов. + +**Преимущества:** Устраняется связанность между "Коллегами", централизуется управление. + + + +Самый распространенный (и простой) вариант реализации паттерна - с использованием **EventEmitter** интерфейса (**Event Bus** - Шина событий). + + + +Разница, по сравнению с обычным использованием EventTarget: + +- события в EventTarget генерирует сам объект, при работе с EventBus это делают сторонние объекты +- список событий при работе с EventTarget ограничен устройством объекта, при работе с EventBus он определяется участниками + + + + + + + +При этом, чтобы избежать коллизии имен событий, зачастую вводят `namespaces`, в формате **{NAMESPACE}:{EVENT NAME}**. Например `user:add`, `searchHistory:add`. + +Нужно отметить, что по-хорошему, префиксы делаются на основе сущностей, а не на основе модулей (иначе происходит раскрытие структуры системы). + + + +```ts +const eventBus = new EventBus(); + +// module 1 +eventBus.on("city:changed", (cityName) => console.log(`New city: ${cityName}`)); + +// module 2 +eventBus.trigger("city:changed", "Minsk"); +``` + + + +Как мы могли бы применить это к уже сделанным домашним заданиям? + + + +### Вопросы? + + + +### Практика + + + +[Реализовать Event Emitter](https://codesandbox.io/s/github/vvscode/otus--javascript-basic/tree/master/lessons/lesson33/code/eventEmitter) + + + +Реализовать поверх существующего функционала метод **once** (для одноразового вызова обработчика). + + + +### Вопросы? + + + +Дополнительные материалы: + +- [Backbone Events](https://backbonejs.org/#Events) и [исходники](https://backbonejs.org/docs/backbone.html#section-17) +- [EventTarget simple implementation](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) +- [Паттерны проектирования понятным языком](https://refactoring.guru/ru/design-patterns) +- [Design patterns for humans!](https://github.com/sohamkamani/javascript-design-patterns-for-humans) + + + +### Опрос о занятии diff --git a/lessons/lesson28/code/routing-examples/README.md b/lessons/lesson28/code/routing-examples/README.md new file mode 100644 index 0000000..000d747 --- /dev/null +++ b/lessons/lesson28/code/routing-examples/README.md @@ -0,0 +1,3 @@ +# vanilla-client-side-routing-examples + +Created with CodeSandbox diff --git a/lessons/lesson28/code/routing-examples/examples/hash-api.js b/lessons/lesson28/code/routing-examples/examples/hash-api.js new file mode 100644 index 0000000..939ad80 --- /dev/null +++ b/lessons/lesson28/code/routing-examples/examples/hash-api.js @@ -0,0 +1,28 @@ +/* eslint-disable */ + +const render = () => { + const route = location.hash.replace("#", "") || "/"; + document.getElementById("root").innerHTML = `

"${route}" page

`; +}; + +// 1. Handle initial page load +window.addEventListener("load", () => { + render(); // 👈 +}); + +// 2. Handle hash changes +window.addEventListener("hashchange", () => { + render(); // 👈 +}); + +// 3. Catch tag clicks +document.body.addEventListener("click", (event) => { + if (!event.target.matches("a")) { + return; + } + event.preventDefault(); + const url = event.target.getAttribute("href"); + location.hash = url; // doesn't reload page + // location.href = url; // reloads page + // location.replace(url); // reloads page +}); diff --git a/lessons/lesson28/code/routing-examples/examples/history-api.js b/lessons/lesson28/code/routing-examples/examples/history-api.js new file mode 100644 index 0000000..7be0621 --- /dev/null +++ b/lessons/lesson28/code/routing-examples/examples/history-api.js @@ -0,0 +1,28 @@ +/* eslint-disable */ + +const render = () => { + const route = location.pathname; + document.getElementById("root").innerHTML = `

"${route}" page

`; +}; + +// 1. Handle initial page load +window.addEventListener("load", () => { + render(); // 👈 +}); + +// 2. Handle history navigations. alternative "window.onpopstate" +window.addEventListener("popstate", (event) => { + render(); +}); + +// 3. Catch
tag clicks + trigger change handler +document.body.addEventListener("click", (event) => { + if (!event.target.matches("a")) { + return; + } + event.preventDefault(); + let url = event.target.getAttribute("href"); + history.pushState({ foo: "bar" }, document.title, url); + // history.replaceState({ foo: "bar" }, url, url); + render(); // 👈 +}); diff --git a/lessons/lesson28/code/routing-examples/examples/practice.js b/lessons/lesson28/code/routing-examples/examples/practice.js new file mode 100644 index 0000000..a7ebba4 --- /dev/null +++ b/lessons/lesson28/code/routing-examples/examples/practice.js @@ -0,0 +1,44 @@ +/* eslint-disable */ + +/** + * TODO: modify router.js to support + * 1. unsubscribe function. + * Hint: inside Router.go function return unsubscribe function, + * which will remove listener by id + * 2. onLeave callback + * Hint: Add 3rd 'onLeave' parameter to Router.on + save in listener object + * Check in Router.handleListener if previousPath matches listener + */ + +const render = (content) => + (document.getElementById("root").innerHTML = `

${content}

`); + +const createLogger = + (content, shouldRender = true) => + (...args) => { + console.log(`LOGGER: ${content} args=${JSON.stringify(args)}`); + if (shouldRender) { + render(content); + } + }; + +const router = Router(); + +const unsubscribe = router.on(/.*/, createLogger("/.*")); +router.on( + (path) => path === "/contacts", + createLogger("/contacts"), // onEnter + createLogger("[leaving] /contacts", false) // onLeave +); +router.on("/about", createLogger("/about")); +router.on("/about/us", createLogger("/about/us")); + +document.body.addEventListener("click", (event) => { + if (!event.target.matches("a")) { + return; + } + event.preventDefault(); + let url = event.target.getAttribute("href"); + router.go(url); + unsubscribe(); +}); diff --git a/lessons/lesson28/code/routing-examples/examples/router.js b/lessons/lesson28/code/routing-examples/examples/router.js new file mode 100644 index 0000000..edd14ea --- /dev/null +++ b/lessons/lesson28/code/routing-examples/examples/router.js @@ -0,0 +1,92 @@ +/* eslint-disable */ + +// IMPLEMENTATION +function Router() { + let listeners = []; + let currentPath = location.pathname; + let previousPath = null; + + const isMatch = (match, path) => + (match instanceof RegExp && match.test(path)) || + (typeof match === "function" && match(path)) || + (typeof match === "string" && match === path); + + const handleListener = ({ match, onEnter, onLeave }) => { + const args = { currentPath, previousPath, state: history.state }; + + isMatch(match, currentPath) && onEnter(args); + onLeave && isMatch(match, previousPath) && onLeave(args); + }; + + const handleAllListeners = () => listeners.forEach(handleListener); + + const generateId = () => { + const getRandomNumber = () => + Math.floor(Math.random() * listeners.length * 1000); + const doesExist = (id) => listeners.find((listener) => listener.id === id); + + let id = getRandomNumber(); + while (doesExist(id)) { + id = getRandomNumber(); + } + return id; + }; + + const on = (match, onEnter, onLeave) => { + const id = generateId(); + + const listener = { id, match, onEnter, onLeave }; + listeners.push(listener); + handleListener(listener); + + return () => { + listeners = listeners.filter((listeners) => listeners.id !== id); + }; + }; + + const go = (url, state) => { + previousPath = currentPath; + history.pushState(state, url, url); + currentPath = location.pathname; + + handleAllListeners(); + }; + + window.addEventListener("popstate", handleAllListeners); + + return { on, go }; +} + +// USAGE +const render = (content) => + (document.getElementById("root").innerHTML = `

${content}

`); + +const createLogger = + (content, shouldRender = true) => + (...args) => { + console.log(`LOGGER: ${content} args=${JSON.stringify(args)}`); + if (shouldRender) { + render(content); + } + }; + +const router = Router(); + +const unsubscribe = router.on(/.*/, createLogger("/.*")); +router.on( + (path) => path === "/contacts", + createLogger("/contacts"), // onEnter + createLogger("[leaving] /contacts", false) // onLeave +); +router.on("/about", createLogger("/about")); +router.on("/about/us", createLogger("/about/us")); + +document.body.addEventListener("click", (event) => { + if (!event.target.matches("a")) { + return; + } + event.preventDefault(); + let url = event.target.getAttribute("href"); + router.go(url); + unsubscribe(); +}); diff --git a/lessons/lesson28/code/routing-examples/index.html b/lessons/lesson28/code/routing-examples/index.html new file mode 100644 index 0000000..06129c6 --- /dev/null +++ b/lessons/lesson28/code/routing-examples/index.html @@ -0,0 +1,32 @@ + + + + + + + Client-Side URL change examples + + +
+

Client-side URL change examples

+
+
+
+ + + + + + + + diff --git a/lessons/lesson28/code/routing-examples/package.json b/lessons/lesson28/code/routing-examples/package.json new file mode 100644 index 0000000..eb165e0 --- /dev/null +++ b/lessons/lesson28/code/routing-examples/package.json @@ -0,0 +1,24 @@ +{ + "name": "vanilla-client-side-routing-examples", + "version": "1.0.0", + "description": "", + "main": "index.html", + "scripts": { + "start": "serve", + "build": "echo This is a static template, there is no bundler or bundling involved!" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/codesandbox-app/static-template.git" + }, + "keywords": [], + "author": "Ives van Hoorne", + "license": "MIT", + "bugs": { + "url": "https://github.com/codesandbox-app/static-template/issues" + }, + "homepage": "https://github.com/codesandbox-app/static-template#readme", + "devDependencies": { + "serve": "^11.2.0" + } +} diff --git a/lessons/lesson28/code/routing-examples/sandbox.config.json b/lessons/lesson28/code/routing-examples/sandbox.config.json new file mode 100644 index 0000000..5866ed7 --- /dev/null +++ b/lessons/lesson28/code/routing-examples/sandbox.config.json @@ -0,0 +1,3 @@ +{ + "template": "static" +} diff --git a/lessons/lesson28/lecture.md b/lessons/lesson28/lecture.md new file mode 100644 index 0000000..52591c9 --- /dev/null +++ b/lessons/lesson28/lecture.md @@ -0,0 +1,372 @@ +--- +title: Занятие 28 +description: Использование клиентского роутинга для создания одностраничных приложений. Деплой одностраничных приложений +--- + +# OTUS + +## Javascript Basic + + + +## Клиентский роутинг + +Как строится одностраничное приложение (SPA) + + + +### Проверка + +- Хорошо ли видно и слышно? +- Проверить идёт ли запись + + + +## Клиентский роутинг + +Как строится одностраничное приложение (SPA) + + + +### Цели занятия + +- Разобраться какие API можно использовать для организации SPA + +- Научиться создавать клиентский роутинг + + + +### Маршрут вебинара + +- Введение +- Hash API +- History API +- Router +- Практика +- Итоги + + + +## Введение + + + +**Что такое URL** + + + +> Единый указатель ресурса (англ. [**Uniform Resource Locator**](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_URL), URL) — единообразный локатор (определитель местонахождения) ресурса. + +> Ранее назывался Universal Resource Locator — универсальный указатель ресурса. URL служит стандартизированным способом записи **адреса ресурса в сети Интернет**. + + + +``` +scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment] +``` + +``` +https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_URL#anchor +``` + + + +[**Что происходит, когда вы вводите URL в браузере**](http://wsvincent.com/what-happens-when-url/) + + + +**Что такое клиентский роутинг** + + + +> Клиентский роутинг (**client-side routing**) это, когда пользователь перемещается по приложению/веб-сайту, и при этом **не происходит полной перезагрузки страницы**, даже если URL-адрес страницы изменяется. Вместо этого **используется JavaScript для обновления URL-адреса**, а также для извлечения и отображения нового содержимого. + + + +**Способы управления URL на клиенте** + + + +- Hash API +- History API + + + +## `Hash API` + + + +\*Старый способ. До появления HTML5. + + + +> Способ управления состояниям фрагмента URL + +``` +scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment] +``` + + + +- window.location.hash +- `window.onhashchange` / `"hashchange"` event + + + +[Пример](https://codesandbox.io/s/vigorous-black-vzbit?file=/index.html) + +```js +document.body.addEventListener("click", (event) => { + if (!event.target.matches("a")) { + return; + } + event.preventDefault(); + let url = event.target.getAttribute("href"); + location.hash = url; // <= set only hash or URL +}); + +window.addEventListener("hashchange", () => { + // <= handle/catch hash changes + console.log(`hashchange: ${location.hash}`); +}); +``` + + + +## `History API` + + + +[Новое API](https://caniuse.com/?search=history) HTML5 + + + +> [**History API**](https://developer.mozilla.org/en-US/docs/Web/API/History_API) опирается на один DOM интерфейс — объект **History** + +> Каждая вкладка браузера имеет уникальный объект History, который находится в `window.history` + + + +> [**History**](https://developer.mozilla.org/en-US/docs/Web/API/History) имеет несколько методов, событий и свойств, которыми мы можем **управлять из JavaScript**. + + + + + +```js +/* Количество записей в текущей сессии истории */ +window.history.length + +/* Возвращает текущий объект состояния истории */ +window.history.state + +/* Метод, позволяющий гулять по истории. + * В качестве аргумента передается смещение, относительно текущей позиции. + * Если передан 0, то будет обновлена текущая страница. + * Если индекс выходит за пределы истории, то ничего не произойдет. */ +window.history.go(n) + +/* Метод, идентичный вызову go(-1) */ +window.history.back() + +/* Метод, идентичный вызову go(1) */ +window.history.forward() + +/* Добавляет элемент истории */ +window.history.pushState(data, title [, url]) + +/* Обновляет текущий элемент истории */ +window.history.replaceState(data, title [, url]) +``` + + + +```js +/* Триггерится при `history.go/back/forward` или при браузерных кликах */ +window.addEventListener("popstate", (event) => console.log(event.state)); +``` + + + +[Пример](https://codesandbox.io/s/vigorous-black-vzbit?file=/index.html) + +```js +document.body.addEventListener("click", (event) => { + if (!event.target.matches("a")) { + return; + } + event.preventDefault(); + let url = event.target.getAttribute("href"); + history.pushState({}, url, url); // <-- +}); + +/* Триггерится при `history.go/back/forward` или при браузерных кликах */ +window.addEventListener("popstate", (event) => { + console.log( + "location: " + document.location + ", state: " + JSON.stringify(event.state) + ); +}); +``` + + + +> \*Нужна настройка сервера, т.к. при обновлении / передаче ссылки должна загрузиться начальная страница + + + +## `Router` + + + +> Обработчик URL - называется роутером (**Router**) + +> Router определяет какой код должен выполняться в зависимости от адреса. +> Логика `router`'а может быть завязана на параметры. + + + +\*Бывает серверный и **браузерный** роутинг/роутер. + + + +\*Router **не встроенные API**, а скорее общепринятый термин. + + + +Очень много готовых библиотек/статей: + +- [Pilot: многофункциональный JavaScript роутер](https://habrahabr.ru/company/mailru/blog/172333/) +- [Роутер на JavaScript](http://blog.byndyu.ru/2009/09/javascript.html) +- [A modern JavaScript router in 100 lines](http://krasimirtsonev.com/blog/article/A-modern-JavaScript-router-in-100-lines-history-api-pushState-hash-url) +- [A simple minimalistic JavaScript router with a fallback for older browsers.](https://github.com/krasimir/navigo) +- [router.js](https://github.com/tildeio/router.js/) + + + +Example. Simple Route Interface + +```sh +# Interface of Route +IRoute { + match # String | RegExp | function + onEnter([data]) # function +} +``` + +```js +// Example +const route = { + match: "/", + onEnter: () => console.log("onEnter index"), +}; +``` + + + +Example. Advanced Route Interface + +```sh +# Interface of Route +IRoute { + match # String | RegExp | function + onEnter([data]) # function + onLeave([data]) # function + onBeforeEnter([data]) # function +} +``` + +```js +// Example +const route = { + match: "/", + onEnter: () => console.log("onEnter index"), + onLeave: () => console.log("onLeave index"), +}; +``` + + + +Example. Router Interface 1 + +```sh +# Interface of Route +IRouter { + add(route) # function + remove(route) # function + go(url, [param]) # function +} +``` + + + +Example. Router Interface 2 + +```sh +# Interface of Route +IRouter { + on(match, onEnter) # function + go(url, [params]) # function +} +``` + + + +[Пример](https://codesandbox.io/s/vigorous-black-vzbit?file=/index.html) + + + +## Практика + + + +To-do: + +1. Fork sandbox +1. Implement unsubscribe/remove functionality +1. Add support for "onLeave" callback + +```sh +# Interface of Route +IRouter { + on(match, onEnter, [onLeave]) # function -> function + go(url, [params]) # function +} +``` + + + +[codesandbox](https://codesandbox.io/s/vigorous-black-vzbit?file=/examples/practice.js) + + + +## Итоги + +> 1. **Клиентский роутинг** - навигацию по приложению/веб-сайту без **перезагрузки страницы** + +> 2. Способы управления URL на клиенте: +> (old) **Hash API** и (new) **History API** + +> 3. **Router** (термин)- обработчик URL, определяет какой код должен выполняться в зависимости от адреса. **\*Не встроённое API** + + + + + +### Деплой одностраничных приложений + +- [devServer.historyApiFallback](https://webpack.js.org/configuration/dev-server/#devserverhistoryapifallback) +- [output.publicPath](https://webpack.js.org/configuration/output/#outputpublicpath) +- [WebpackHTMLPlugin filename option](https://github.com/jantimon/html-webpack-plugin#options) +- [404 page for Github Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-custom-404-page-for-your-github-pages-site) +- [Пример настройки webpack для работы с history API](https://github.com/vvscode/webpack-gh-pages/pulls) +- [Пример работы роутера с параметризацией](https://gzh7s.csb.app/#city=Moscow) + + + +### Вопросы? + + + +### Ссылки + +- [Understanding client side routing by implementing a router in Vanilla JS](https://www.willtaylor.blog/client-side-routing-in-vanilla-js/#:~:text=What%20is%20client%20side%20routing,fetch%20and%20display%20new%20content.) diff --git a/lessons/lesson29/lesson.md b/lessons/lesson29/lesson.md index e9a408e..b0bc87f 100644 --- a/lessons/lesson29/lesson.md +++ b/lessons/lesson29/lesson.md @@ -1 +1,246 @@ -# Lesson 29 +--- +title: Занятие 29 +description: Различие между стандартами языка, инструменты транспиляции, проблемы типизации +--- + +# OTUS + +## Javascript Basic + + + +### Вопросы? + + + +## Различие между стандартами языка, инструменты транспиляции, проблемы типизации + + + +### Различие между стандартами языка, инструменты транспиляции + + + +Какая текущая версия языка ECMAScript? + + + +Какие новые удобные вещи появились в сравнении с ES5 ? + + + +[1](https://medium.com/@dupski/what-major-new-features-were-in-each-javascript-version-what-version-should-i-target-25526c498687), [2](http://es6-features.org/), [3](https://github.com/lukehoban/es6features), [4](https://www.w3schools.com/js/js_versions.asp) + + + +В каком году появилась версия Javascript следующая за ES5 ? + + + +Какие инструменты позволяют использовать новые версии, в окружении, которое работает со старыми версиями языка? + + + +В каком году появился Babel.js? + + + + + +До того, как язык начал развиваться, его пытались модернизировать или заменить другими языками ([которые бы транспилировались в Javascript](https://github.com/jashkenas/coffeescript/wiki/List-of-languages-that-compile-to-JS)) + + + +Примерно там же (в 2012) появился язык [TypeScript](https://ru.wikipedia.org/wiki/TypeScript). + +Разработчиком языка TypeScript является Андерс Хейлсберг (англ. Anders Hejlsberg), создавший ранее Turbo Pascal, Delphi и C#. + + + +Главное чем отличался TS: + +- был **надмножеством** Javascript +- поддерживал классы +- добавлял работу с типизацией + + + +### Вопросы? + + + +### Проблемы типизации + + + +Виды типизации: + +- динамическая / статическая +- слабая / сильная +- неявная / явная + +Какая типизация в Javascript? + + + +Большая часть проблем в разработке связанна с незначительными факторами: + +- опечатки _(неправильно назвали переменную или свойство)_ +- невнимательность _(не передали параметр, или передали неправильный параметр)_ +- потеря контекста _(не учли контекст вызова)_ +- мелкие упущения _(не учли все места, которые затрагиваются изменениями)_ + + + +Это характерно для всех языков, но особенности типизации дают для этого хорошую почву. + + + +Изначальная идея TypeScript - добавить явное указание типов в код. Что должно значительно уменьшить число таких проблем (или сделать их обнаружение более быстрым, без запуска кода). + + + +```ts +function getCityMap(city) { + return `https://map.com?q=${city}`; +} +``` + +Что с этим кодом может пойти не так? + + + +```ts +function getCityMap(city: string): string { + return `https://map.com?q=${city}`; +} +``` + + + +Помимо работы с стандартными типами для Javascript, TS позволял определить тип как класс / интерфейс / форму (shape). + +[Например](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgArQM4HsTIN4BQyxyIcAthAFzIZhSgDmA3AQL4EFgCeADigGVycKGHRRsuALz4iJMpRp0GIFnOLAAjjRABXcgCNorDgQQ46yA1gM0hIsZhzIZhEqQrVkAcgBCN7wAadWQtGgB2AA52VjMLMCsbcUkaZOcZawNmIA) описать свойства и их тип, что было полезно для языка, где классами пользоваться еще не привыкли, а вот объекты были в ходу уже давно. + + + +Со временем функционал расширялся, в нем появлялись новые вещи (вроде декораторов) и обработчик языка становился умнее (например появился вывод типов). + + + +Какую еще проблему решает Typescript? + + + +```js +/** + * Returns city map url + * @param {string} city + * @return {string} url + */ +function getCityMap(city) { + return `https://map.com?q=${city}`; +} +``` + +Какая проблема с этим кодом? + + + +Указание типов + +- документирует код описанием контрактов +- позволяет проверять использование кода на соответствие контрактам (и чем больше кода, тем это сложнее и важнее) + + + +**Важно!** Typescript _(как и другие языки транспилирующиеся в JS)_ **НЕ работает в браузере**. + +Т.е. все его проверки работают только в момент написания или сборки кода! _(в браузере будет работать уже javascript, полученный при транспиляции Typescript)_ + + + +### Вопросы? + + + +### Зачем мы будем работать с Typescript + + + +1. Экономит нервы и время (поначалу код на typescript может писаться дольше, но со временем эта проблема уходит) +1. Когда то _был_ еще [flow language](https://flow.org/), но сейчас Typescript самая популярная "альтернатива" js +1. _"Необходим"_ для больших проектов (и очень рекомендован для маленьких) +1. Востребован при поиске работы +1. Улучшает понимание работы кода и понимание некоторых паттернов + разработки + + + +Что мы разберем на курсе: + +- как использовать базовые возможности языка (базовые типы, интерфейсы, типизация функций) +- как настроить окружение для работы +- как применять некоторые "продвинутые" темы (немного generic, вывод типов,кастинг) + + + +Базовые темы мы разберем отдельным занятием, остальное будем смотреть по ходу остальных занятий. + + + +Дальше код будет писаться на Typescript. + + + +### Вопросы? + + + +### Установка, настройка и запуск Typescript + + + +1. Создать новый npm-проект +1. Установить пакет [`typescript`](https://www.npmjs.com/package/typescript) +1. Инициализировать конфигурацию для Typescript +1. Создать файлы +1. Выполнить команду `npx tsc` (Для проверки, без сборки `npx tsc --noEmit`) + + + +```bash +mkdir typescript-test +cd typescript-test + +npm init -y + +npm install typescript --save-dev + +npx tsc --init + +echo "export function x(n: number) { console.log('x with n', n); }" > "x.ts" +echo "import { x } from './x'; " > "index.ts" +echo "function y() { x(1); }" >> "index.ts" +echo "y();" >> "index.ts" + +npx tsc +``` + + + +### Вопросы? + + + +Дополнительные материалы: + +- [The TypeScript Handbook (30 минут для быстрого старта)](https://www.typescriptlang.org/docs/handbook/intro.html) +- [Основное про типы в TS](https://canonium.com/category/typescript) +- TypeScript как будущее энтерпрайзного JavaScript [1](https://dou.ua/lenta/articles/typescript-1/) и [2](https://dou.ua/lenta/articles/typescript-2/) +- [Глубокое погружение в TypeScript](https://github.com/etroynov/typescript-book) diff --git a/lessons/lesson30/lesson.md b/lessons/lesson30/lesson.md index c879a05..f71e6b5 100644 --- a/lessons/lesson30/lesson.md +++ b/lessons/lesson30/lesson.md @@ -1 +1,824 @@ -# Lesson 30 +--- +title: Занятие 30 +description: Что такое React, JSX, настройка окружения +--- + +# OTUS + +## Javascript Basic + + + +Вопросы? + + + +### [React](https://ru.reactjs.org/) + + + +#### Проблемы шаблонизации + +- скорость разработки приложения +- скорость работы приложения + + + +#### React + +- декларативный +- основан на [компонентах](https://reactjs.org/docs/components-and-props.html) +- научитесь однажды — пишите где угодно + + + +Особенности **React** + +- с компонентами (но не [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components)) +- оптимизация обновлений через [vDOM](https://reactjs.org/docs/faq-internals.html) +- минимальный синтаксис шаблонов — [JSX](https://facebook.github.io/jsx/) + + + +Идея скорости — [**Virtual DOM**](https://reactjs.org/docs/faq-internals.html) + +- архитектура React — отрисовать минимально необходимые изменения +- работа с DOM напрямую — медленно +- vDOM — модель настоящего DOM на легких js-объектах + + + + + + + +### React - абстрактная идея + +- может использоваться на разных платформах (не только Web, но и нативные системы и что-угодно еще представляющее UI - [React Native](https://reactnative.dev/), [React for terminal](https://github.com/vadimdemedes/ink)) +- сам React никак не связан ни с Web ни с DOM +- для работы с DOM(HTML) используется отдельный пакет — [react-dom](https://reactjs.org/docs/react-dom.html) + + + +### Из чего состоит приложение на React? + +[React-элементы](https://ru.reactjs.org/docs/glossary.html) — это составляющие блоки React-приложений. Их **можно перепутать с более известной концепцией «компонентов»**, но в отличие от компонента, элемент описывает то, что вы хотите увидеть на экране. +React-элементы иммутабельны. + + + +Представление React элементов через [`createElement`](https://ru.reactjs.org/docs/react-api.html#createelement) + +```jsx +React.createElement(type, [props], [...children]); +``` + + + +```tsx [1-30] +React.createElement( + "div", + { + className: "wrapper", + style: "margin: 5px", + }, + React.createElement( + "button", + { + onClick: function onClick() { + return alert("Click"); + }, + className: "btn", + }, + "Click me" + ) +); +``` + + + +```jsx [1-30] +// jsx: +// https://bit.ly/3gCKQOI +
+ +
+``` + + + +[React-компоненты](https://ru.reactjs.org/docs/glossary.html) — это маленькие, повторно используемые части кода, которые возвращают React-элементы для отображения на странице. Самый простой React-компонент — JavaScript функция, которая возвращает элементы React. + + + +```tsx [1-50] +// компонент +const Button = ({ name }) => { + return React.createElement( + "button", + { + onClick: function onClick() { + return alert("Click"); + }, + className: "btn", + }, + name + ); +}; + +// применение компонента +React.createElement( + "div", + { + className: "wrapper", + style: "margin: 5px", + }, + React.createElement(Button, { + name: "Click me", + }) +); +``` + + + +```jsx [1-30] +const Button = ({ name }) => ( + +); + +// применение компонента +
+
; +``` + + + +Чтобы [отрисовать элемент на странице](https://ru.reactjs.org/docs/rendering-elements.html) нужен пакет [`react-dom`](https://ru.reactjs.org/docs/react-dom.html) + +```jsx [1-10] +const element =

Hello, world

; + +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render(element); +``` + + + +### [Пример работы с React элементами](https://stackblitz.com/edit/basic-react-dom-novgwc?file=index.js) + + + +Для поддержки синтаксиса `JSX` нужен пакет [@babel/preset-react](https://babeljs.io/docs/en/babel-preset-react). Но при этом вполне можно обходиться и [вообще без JSX](https://ru.reactjs.org/docs/react-without-jsx.html) + + + +### Вопросы? + + + +### JSX + + + +`JSX` — синтаксический сахар для функции `React.createElement` + +> JSX — расширение языка JavaScript. Мы рекомендуем использовать его, когда требуется объяснить React, как должен выглядеть UI. JSX напоминает язык шаблонов, наделённый силой JavaScript. + + + +[Знакомство с JSX](https://ru.reactjs.org/docs/introducing-jsx.html) + +[JSX в деталях](https://ru.reactjs.org/docs/jsx-in-depth.html) + + + +#### [Немного о правилах JSX](https://stackblitz.com/edit/react-basic-jsx-otus?file=jsxExamples.jsx) + + + +```tsx [1-30] +// 1. Объявление компонентов + +// 1.1 Как функция +const Cmp1 = (props) =>
{props.name}
; + +// 1.2 Как класс +class Cmp2 extends React.Component { + render() { + return ; + } +} +``` + + + +```tsx [1-30] +// 2. Использование компонентов + +// 2.1 С закрывающим тегов + + +// 2.2 С самозакрывающимся тегом (если нет дочерних элементов) + +``` + + + +```tsx [1-30] +// 3. Передача свойств в компонент + +// 3.1 Все свойства пишутся в {} + + + +// 3.2 Строковые литералы передаются как-есть + + +// 3.3 Булевые true свойства можно передавать просто указанием + +``` + + + +```tsx [1-30] +// 4 Комментарий + +// 4.1 Вокруг компонентов/их методов - обычный js +// так +const Cmp3 = (props /* или так */) =>
{props.name}
; + +// 4.2 Внутри jsx +const Cmp4 = (props) => ( +
+ {/* вот так */} + {props.name} +
+); +``` + + + +```tsx [1-30] +// 5 Выражения + +// jsx - subset js, выражения пишутся внутри {} (как в примере с свойствами компонентов) +const Cmp5 = (props) => ( +
+ {props.disabled ? No way : } + {props.names.map((name) => ( + + ))} + {props.someFlag && } +
+); +``` + + + +```tsx +// Важно! (ну или было важно) +import React from "react"; +``` + +Начиная с React 17 появилась [возможность](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) использования новой JSX трансформации + + + +### Вопросы? + + + +### Тестирование + + + +- Подход от реализации - [Enzyme](https://enzymejs.github.io/enzyme/) + +- Подход от пользователя - [RTL](https://testing-library.com/docs/react-testing-library/intro/) + + + +### React-testing-library + +> The React Testing Library is a very light-weight solution for testing React components. It provides light utility functions on top of react-dom and react-dom/test-utils + + + +```js [1-30] +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import App from "./App"; + +describe("App", () => { + test("renders App component", () => { + // Render component + render(); + // Debug info + screen.debug(); + // For debugging using testing-playground, + // screen exposes this convenient method + // which logs a URL that can be opened in a browser + screen.logTestingPlaygroundURL(); + }); +}); +``` + + + +#### Вывод + +> RTL используется для взаимодействия с вашими компонентами React так, как это делает человек. То, что видит человек, - это просто визуализированный HTML из ваших компонентов React, поэтому вы видите эту структуру HTML как результат + +```html [1-30] + +
+
Hello React
+
+ +``` + + + +### Выбор элементов + + + +[jest-dom](https://github.com/testing-library/jest-dom/) + +```js [1-30] +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import App from "./App"; + +describe("App", () => { + test("renders App component", () => { + render(); + + expect(screen.getByText("Search:")).toBeInTheDocument(); + }); +}); +``` + + + +```js [1-30] +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import App from "./App"; + +describe("App", () => { + test("renders App component", () => { + render(); + + // неявная проверка + // getByText выбрасывает исключение если элемент не найден + screen.getByText("Search:"); + + // явная проверка - рекомендуется + expect(screen.getByText("Search:")).toBeInTheDocument(); + }); +}); +``` + + + +```js [1-30] +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import App from "./App"; + +describe("App", () => { + test("renders App component", () => { + render(); + + // упадет + expect(screen.getByText("Search")).toBeInTheDocument(); + + // пройдет + expect(screen.getByText("Search:")).toBeInTheDocument(); + + // пройдет + expect(screen.getByText(/Search/)).toBeInTheDocument(); + }); +}); +``` + + + +### Возможности для поиска + +`getByRole` используется для поиска по [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label). Но, [у некоторых HTML элементов есть неявные роли](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles) + +```js [1-30] +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import App from "./App"; + +describe("App", () => { + test("renders App component", () => { + render(); + + screen.getByRole(""); + }); +}); +``` + + + +```[1-30] +Unable to find an accessible element with the role "" + +Here are the accessible roles: + +document: + +Name "": + + +-------------------------------------------------- +textbox: + +Name "Search:": + + +-------------------------------------------------- +``` + + + +```js +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import App from "./App"; + +describe("App", () => { + test("renders App component", () => { + render(); + + expect(screen.getByRole("textbox")).toBeInTheDocument(); + }); +}); +``` + + + +### Есть более специфичные способы поиска: + +`getByLabelText`: + +```html +