From 405a4dd89a75df0bf9ee05a49b70b4fc8b8cd68c Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 15 Oct 2025 13:01:27 +0300 Subject: [PATCH 1/4] 25 --- .../eventEmitter/.codesandbox/workspace.json | 8 + lessons/lesson25/code/eventEmitter/index.html | 13 + .../lesson25/code/eventEmitter/package.json | 21 ++ .../eventEmitter/src/EventEmitter.test.ts | 80 +++++++ .../code/eventEmitter/src/EventEmitter.ts | 3 + .../lesson25/code/eventEmitter/src/index.ts | 21 ++ .../lesson25/code/eventEmitter/tsconfig.json | 13 + lessons/lesson25/images/EventBus.jpeg | Bin 0 -> 17375 bytes lessons/lesson25/images/ObservableUML.png | Bin 0 -> 28547 bytes lessons/lesson25/lesson.md | 226 +++++++++++++++++- 10 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 lessons/lesson25/code/eventEmitter/.codesandbox/workspace.json create mode 100644 lessons/lesson25/code/eventEmitter/index.html create mode 100644 lessons/lesson25/code/eventEmitter/package.json create mode 100644 lessons/lesson25/code/eventEmitter/src/EventEmitter.test.ts create mode 100644 lessons/lesson25/code/eventEmitter/src/EventEmitter.ts create mode 100644 lessons/lesson25/code/eventEmitter/src/index.ts create mode 100644 lessons/lesson25/code/eventEmitter/tsconfig.json create mode 100644 lessons/lesson25/images/EventBus.jpeg create mode 100644 lessons/lesson25/images/ObservableUML.png 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 0000000000000000000000000000000000000000..7948f818d491a2cce5e8b371eef966d799854c68 GIT binary patch literal 17375 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 literal 0 HcmV?d00001 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) + + + +### Опрос о занятии From 7f22ab3e285df61fd444adc7d7061cd00d6aa3d0 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 15 Oct 2025 13:07:55 +0300 Subject: [PATCH 2/4] 28 --- .../lesson28/code/routing-examples/README.md | 3 + .../routing-examples/examples/hash-api.js | 28 ++ .../routing-examples/examples/history-api.js | 28 ++ .../routing-examples/examples/practice.js | 44 +++ .../code/routing-examples/examples/router.js | 92 +++++ .../lesson28/code/routing-examples/index.html | 32 ++ .../code/routing-examples/package.json | 24 ++ .../code/routing-examples/sandbox.config.json | 3 + lessons/lesson28/lecture.md | 372 ++++++++++++++++++ 9 files changed, 626 insertions(+) create mode 100644 lessons/lesson28/code/routing-examples/README.md create mode 100644 lessons/lesson28/code/routing-examples/examples/hash-api.js create mode 100644 lessons/lesson28/code/routing-examples/examples/history-api.js create mode 100644 lessons/lesson28/code/routing-examples/examples/practice.js create mode 100644 lessons/lesson28/code/routing-examples/examples/router.js create mode 100644 lessons/lesson28/code/routing-examples/index.html create mode 100644 lessons/lesson28/code/routing-examples/package.json create mode 100644 lessons/lesson28/code/routing-examples/sandbox.config.json create mode 100644 lessons/lesson28/lecture.md 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.) From d519dee1fc8d282acf758caf2ceb200350b3bff2 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 15 Oct 2025 13:10:56 +0300 Subject: [PATCH 3/4] 29 --- lessons/lesson29/lesson.md | 247 ++++++++++++++++++++++++++++++++++++- 1 file changed, 246 insertions(+), 1 deletion(-) 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) From 23644f40d51b9e170d66b77835b9b3bef275e19b Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 15 Oct 2025 13:12:54 +0300 Subject: [PATCH 4/4] 30 --- lessons/lesson30/lesson.md | 825 ++++++++++++++++++++++++++++++++++++- 1 file changed, 824 insertions(+), 1 deletion(-) 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 +