diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/04-dom-events-async/01-query-selectors/README.md b/04-dom-events-async/01-query-selectors/README.md new file mode 100644 index 0000000..7ee016a --- /dev/null +++ b/04-dom-events-async/01-query-selectors/README.md @@ -0,0 +1,28 @@ +# 练习 1:获取元素 + +## 目标 + +学会用几种常见方式拿到页面元素。 + +## 你要练什么 + +- `getElementById` +- `querySelector` +- `querySelectorAll` +- 元素文本读取 + +## 任务 + +请基于页面结构完成以下操作: + +- 选中主标题 +- 选中“开始学习”按钮 +- 选中全部学习卡片 +- 在控制台输出标题文字、按钮文字和卡片数量 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/01-query-selectors/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/01-query-selectors/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/01-query-selectors/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/01-query-selectors/answer.js) diff --git a/04-dom-events-async/01-query-selectors/answer.html b/04-dom-events-async/01-query-selectors/answer.html new file mode 100644 index 0000000..e89f82a --- /dev/null +++ b/04-dom-events-async/01-query-selectors/answer.html @@ -0,0 +1,52 @@ + + + + + + 获取元素 + + + +
+

DOM 获取元素练习

+ + +
+
获取标题
+
获取按钮
+
获取一组卡片
+
+
+ + + + diff --git a/04-dom-events-async/01-query-selectors/answer.js b/04-dom-events-async/01-query-selectors/answer.js new file mode 100644 index 0000000..b9ab505 --- /dev/null +++ b/04-dom-events-async/01-query-selectors/answer.js @@ -0,0 +1,7 @@ +const title = document.getElementById("page-title"); +const button = document.querySelector(".start-btn"); +const cards = document.querySelectorAll(".card"); + +console.log("标题:", title.textContent); +console.log("按钮:", button.textContent); +console.log("卡片数量:", cards.length); diff --git a/04-dom-events-async/01-query-selectors/starter.html b/04-dom-events-async/01-query-selectors/starter.html new file mode 100644 index 0000000..b1eda62 --- /dev/null +++ b/04-dom-events-async/01-query-selectors/starter.html @@ -0,0 +1,52 @@ + + + + + + 获取元素 + + + +
+

DOM 获取元素练习

+ + +
+
获取标题
+
获取按钮
+
获取一组卡片
+
+
+ + + + diff --git a/04-dom-events-async/01-query-selectors/starter.js b/04-dom-events-async/01-query-selectors/starter.js new file mode 100644 index 0000000..44e5864 --- /dev/null +++ b/04-dom-events-async/01-query-selectors/starter.js @@ -0,0 +1,5 @@ +// 任务: +// 1. 用 getElementById 获取标题 +// 2. 用 querySelector 获取按钮 +// 3. 用 querySelectorAll 获取全部卡片 +// 4. 在控制台输出标题文字、按钮文字和卡片数量 diff --git a/04-dom-events-async/02-text-class-style/README.md b/04-dom-events-async/02-text-class-style/README.md new file mode 100644 index 0000000..29d17de --- /dev/null +++ b/04-dom-events-async/02-text-class-style/README.md @@ -0,0 +1,25 @@ +# 练习 2:修改文本、类名和样式 + +## 目标 + +学会改元素内容、切换类名和设置简单样式。 + +## 你要练什么 + +- `textContent` +- `classList.add` +- `classList.remove` +- `style` + +## 任务 + +- 把标题改成“今天已完成 DOM 练习” +- 给状态标签加上完成样式 +- 修改说明文字颜色 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/02-text-class-style/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/02-text-class-style/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/02-text-class-style/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/02-text-class-style/answer.js) diff --git a/04-dom-events-async/02-text-class-style/answer.html b/04-dom-events-async/02-text-class-style/answer.html new file mode 100644 index 0000000..e9a5ec4 --- /dev/null +++ b/04-dom-events-async/02-text-class-style/answer.html @@ -0,0 +1,47 @@ + + + + + + 修改文本、类名和样式 + + + +
+

今天的学习状态

+

当前还没有更新进度。

+ 进行中 +
+ + + + diff --git a/04-dom-events-async/02-text-class-style/answer.js b/04-dom-events-async/02-text-class-style/answer.js new file mode 100644 index 0000000..b434195 --- /dev/null +++ b/04-dom-events-async/02-text-class-style/answer.js @@ -0,0 +1,9 @@ +const title = document.getElementById("title"); +const description = document.getElementById("description"); +const badge = document.getElementById("badge"); + +title.textContent = "今天已完成 DOM 练习"; +description.textContent = "已经练习了元素选择、文本修改和样式切换。"; +description.style.color = "#2563eb"; +badge.textContent = "已完成"; +badge.classList.add("done"); diff --git a/04-dom-events-async/02-text-class-style/starter.html b/04-dom-events-async/02-text-class-style/starter.html new file mode 100644 index 0000000..28af723 --- /dev/null +++ b/04-dom-events-async/02-text-class-style/starter.html @@ -0,0 +1,47 @@ + + + + + + 修改文本、类名和样式 + + + +
+

今天的学习状态

+

当前还没有更新进度。

+ 进行中 +
+ + + + diff --git a/04-dom-events-async/02-text-class-style/starter.js b/04-dom-events-async/02-text-class-style/starter.js new file mode 100644 index 0000000..2966eeb --- /dev/null +++ b/04-dom-events-async/02-text-class-style/starter.js @@ -0,0 +1,5 @@ +// 任务: +// 1. 选中标题、描述、状态标签 +// 2. 修改标题文字 +// 3. 给标签加上 done 类名 +// 4. 修改描述文字颜色 diff --git a/04-dom-events-async/03-create-and-remove/README.md b/04-dom-events-async/03-create-and-remove/README.md new file mode 100644 index 0000000..bf5e8f9 --- /dev/null +++ b/04-dom-events-async/03-create-and-remove/README.md @@ -0,0 +1,24 @@ +# 练习 3:创建和删除节点 + +## 目标 + +学会动态新增和删除页面节点。 + +## 你要练什么 + +- `createElement` +- `appendChild` +- `remove` +- 事件绑定 + +## 任务 + +- 点击“新增任务”时往列表里加一个新项 +- 点击“删除最后一项”时删除最后一个列表项 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/03-create-and-remove/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/03-create-and-remove/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/03-create-and-remove/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/03-create-and-remove/answer.js) diff --git a/04-dom-events-async/03-create-and-remove/answer.html b/04-dom-events-async/03-create-and-remove/answer.html new file mode 100644 index 0000000..90a810b --- /dev/null +++ b/04-dom-events-async/03-create-and-remove/answer.html @@ -0,0 +1,47 @@ + + + + + + 创建和删除节点 + + + +
+

任务列表

+ + + + +
+ + + + diff --git a/04-dom-events-async/03-create-and-remove/answer.js b/04-dom-events-async/03-create-and-remove/answer.js new file mode 100644 index 0000000..39488ec --- /dev/null +++ b/04-dom-events-async/03-create-and-remove/answer.js @@ -0,0 +1,20 @@ +const addButton = document.getElementById("add-btn"); +const removeButton = document.getElementById("remove-btn"); +const taskList = document.getElementById("task-list"); + +let taskIndex = 3; + +addButton.addEventListener("click", function () { + const item = document.createElement("li"); + item.textContent = `新任务 ${taskIndex}`; + taskList.appendChild(item); + taskIndex += 1; +}); + +removeButton.addEventListener("click", function () { + const lastItem = taskList.lastElementChild; + + if (lastItem) { + lastItem.remove(); + } +}); diff --git a/04-dom-events-async/03-create-and-remove/starter.html b/04-dom-events-async/03-create-and-remove/starter.html new file mode 100644 index 0000000..2bb0658 --- /dev/null +++ b/04-dom-events-async/03-create-and-remove/starter.html @@ -0,0 +1,47 @@ + + + + + + 创建和删除节点 + + + +
+

任务列表

+ + + + +
+ + + + diff --git a/04-dom-events-async/03-create-and-remove/starter.js b/04-dom-events-async/03-create-and-remove/starter.js new file mode 100644 index 0000000..d5abcb4 --- /dev/null +++ b/04-dom-events-async/03-create-and-remove/starter.js @@ -0,0 +1,4 @@ +// 任务: +// 1. 获取新增按钮、删除按钮和列表 +// 2. 点击新增按钮时创建一个新的 li 并追加到列表 +// 3. 点击删除按钮时删除最后一个 li diff --git a/04-dom-events-async/04-click-counter/README.md b/04-dom-events-async/04-click-counter/README.md new file mode 100644 index 0000000..ecf284a --- /dev/null +++ b/04-dom-events-async/04-click-counter/README.md @@ -0,0 +1,25 @@ +# 练习 4:点击计数器 + +## 目标 + +学会给按钮绑定点击事件,并更新页面数据。 + +## 你要练什么 + +- `addEventListener` +- 点击事件 +- 数字状态更新 +- DOM 渲染 + +## 任务 + +- 点击加一按钮时计数加 1 +- 点击减一按钮时计数减 1 +- 点击重置按钮时恢复为 0 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/04-click-counter/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/04-click-counter/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/04-click-counter/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/04-click-counter/answer.js) diff --git a/04-dom-events-async/04-click-counter/answer.html b/04-dom-events-async/04-click-counter/answer.html new file mode 100644 index 0000000..437bc7d --- /dev/null +++ b/04-dom-events-async/04-click-counter/answer.html @@ -0,0 +1,51 @@ + + + + + + 点击计数器 + + + +
+

点击计数器

+

0

+
+ + + +
+
+ + + + diff --git a/04-dom-events-async/04-click-counter/answer.js b/04-dom-events-async/04-click-counter/answer.js new file mode 100644 index 0000000..b2d3af9 --- /dev/null +++ b/04-dom-events-async/04-click-counter/answer.js @@ -0,0 +1,25 @@ +let count = 0; + +const value = document.getElementById("value"); +const decreaseButton = document.getElementById("decrease-btn"); +const increaseButton = document.getElementById("increase-btn"); +const resetButton = document.getElementById("reset-btn"); + +function render() { + value.textContent = count; +} + +decreaseButton.addEventListener("click", function () { + count -= 1; + render(); +}); + +increaseButton.addEventListener("click", function () { + count += 1; + render(); +}); + +resetButton.addEventListener("click", function () { + count = 0; + render(); +}); diff --git a/04-dom-events-async/04-click-counter/starter.html b/04-dom-events-async/04-click-counter/starter.html new file mode 100644 index 0000000..ee6997c --- /dev/null +++ b/04-dom-events-async/04-click-counter/starter.html @@ -0,0 +1,51 @@ + + + + + + 点击计数器 + + + +
+

点击计数器

+

0

+
+ + + +
+
+ + + + diff --git a/04-dom-events-async/04-click-counter/starter.js b/04-dom-events-async/04-click-counter/starter.js new file mode 100644 index 0000000..6a57a2b --- /dev/null +++ b/04-dom-events-async/04-click-counter/starter.js @@ -0,0 +1,6 @@ +let count = 0; + +// 任务: +// 1. 获取数字元素和 3 个按钮 +// 2. 点击按钮时更新 count +// 3. 每次修改后,把最新 count 渲染到页面 diff --git a/04-dom-events-async/05-form-submit-and-prevent-default/README.md b/04-dom-events-async/05-form-submit-and-prevent-default/README.md new file mode 100644 index 0000000..32dbcea --- /dev/null +++ b/04-dom-events-async/05-form-submit-and-prevent-default/README.md @@ -0,0 +1,26 @@ +# 练习 5:表单提交和 preventDefault + +## 目标 + +学会拦截表单默认提交,并把输入内容渲染到页面。 + +## 你要练什么 + +- `submit` 事件 +- `preventDefault()` +- 表单值读取 +- 动态追加列表项 + +## 任务 + +- 提交表单时阻止页面刷新 +- 读取输入框内容 +- 把内容加入到待办列表 +- 提交后清空输入框 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/05-form-submit-and-prevent-default/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/05-form-submit-and-prevent-default/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/05-form-submit-and-prevent-default/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/05-form-submit-and-prevent-default/answer.js) diff --git a/04-dom-events-async/05-form-submit-and-prevent-default/answer.html b/04-dom-events-async/05-form-submit-and-prevent-default/answer.html new file mode 100644 index 0000000..14df416 --- /dev/null +++ b/04-dom-events-async/05-form-submit-and-prevent-default/answer.html @@ -0,0 +1,51 @@ + + + + + + 表单提交和 preventDefault + + + +
+

学习待办

+ +
+ + +
+ + +
+ + + + diff --git a/04-dom-events-async/05-form-submit-and-prevent-default/answer.js b/04-dom-events-async/05-form-submit-and-prevent-default/answer.js new file mode 100644 index 0000000..afacfc7 --- /dev/null +++ b/04-dom-events-async/05-form-submit-and-prevent-default/answer.js @@ -0,0 +1,18 @@ +const form = document.getElementById("todo-form"); +const input = document.getElementById("todo-input"); +const list = document.getElementById("todo-list"); + +form.addEventListener("submit", function (event) { + event.preventDefault(); + + const value = input.value.trim(); + + if (!value) { + return; + } + + const item = document.createElement("li"); + item.textContent = value; + list.appendChild(item); + input.value = ""; +}); diff --git a/04-dom-events-async/05-form-submit-and-prevent-default/starter.html b/04-dom-events-async/05-form-submit-and-prevent-default/starter.html new file mode 100644 index 0000000..1eb5213 --- /dev/null +++ b/04-dom-events-async/05-form-submit-and-prevent-default/starter.html @@ -0,0 +1,51 @@ + + + + + + 表单提交和 preventDefault + + + +
+

学习待办

+ +
+ + +
+ + +
+ + + + diff --git a/04-dom-events-async/05-form-submit-and-prevent-default/starter.js b/04-dom-events-async/05-form-submit-and-prevent-default/starter.js new file mode 100644 index 0000000..1e1d825 --- /dev/null +++ b/04-dom-events-async/05-form-submit-and-prevent-default/starter.js @@ -0,0 +1,6 @@ +// 任务: +// 1. 获取表单、输入框和列表 +// 2. 监听 submit 事件 +// 3. 用 preventDefault() 阻止默认提交 +// 4. 读取输入框内容,创建新 li,追加到列表 +// 5. 清空输入框 diff --git a/04-dom-events-async/06-bubbling-and-delegation/README.md b/04-dom-events-async/06-bubbling-and-delegation/README.md new file mode 100644 index 0000000..a29ad63 --- /dev/null +++ b/04-dom-events-async/06-bubbling-and-delegation/README.md @@ -0,0 +1,25 @@ +# 练习 6:冒泡、委托和 stopPropagation + +## 目标 + +理解事件会冒泡,并学会在列表里使用事件委托。 + +## 你要练什么 + +- 事件冒泡 +- `event.target` +- 事件委托 +- `stopPropagation()` + +## 任务 + +- 点击外层面板时输出一条日志 +- 点击列表项时,通过事件委托切换激活状态 +- 点击列表项里的删除按钮时,阻止冒泡并删除当前项 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/06-bubbling-and-delegation/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/06-bubbling-and-delegation/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/06-bubbling-and-delegation/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/06-bubbling-and-delegation/answer.js) diff --git a/04-dom-events-async/06-bubbling-and-delegation/answer.html b/04-dom-events-async/06-bubbling-and-delegation/answer.html new file mode 100644 index 0000000..4c3e581 --- /dev/null +++ b/04-dom-events-async/06-bubbling-and-delegation/answer.html @@ -0,0 +1,58 @@ + + + + + + 冒泡、委托和 stopPropagation + + + +
+

事件委托练习

+

点击列表项可以切换高亮,点击删除按钮可以移除当前项。

+ + +
+ + + + diff --git a/04-dom-events-async/06-bubbling-and-delegation/answer.js b/04-dom-events-async/06-bubbling-and-delegation/answer.js new file mode 100644 index 0000000..5d4c457 --- /dev/null +++ b/04-dom-events-async/06-bubbling-and-delegation/answer.js @@ -0,0 +1,27 @@ +const panel = document.getElementById("panel"); +const lessonList = document.getElementById("lesson-list"); + +panel.addEventListener("click", function () { + console.log("点击到了外层面板"); +}); + +lessonList.addEventListener("click", function (event) { + const removeButton = event.target.closest(".remove-btn"); + + if (removeButton) { + event.stopPropagation(); + const currentItem = removeButton.closest(".lesson-item"); + + if (currentItem) { + currentItem.remove(); + } + + return; + } + + const currentItem = event.target.closest(".lesson-item"); + + if (currentItem) { + currentItem.classList.toggle("active"); + } +}); diff --git a/04-dom-events-async/06-bubbling-and-delegation/starter.html b/04-dom-events-async/06-bubbling-and-delegation/starter.html new file mode 100644 index 0000000..8a9c3f0 --- /dev/null +++ b/04-dom-events-async/06-bubbling-and-delegation/starter.html @@ -0,0 +1,58 @@ + + + + + + 冒泡、委托和 stopPropagation + + + +
+

事件委托练习

+

点击列表项可以切换高亮,点击删除按钮可以移除当前项。

+ + +
+ + + + diff --git a/04-dom-events-async/06-bubbling-and-delegation/starter.js b/04-dom-events-async/06-bubbling-and-delegation/starter.js new file mode 100644 index 0000000..fcdadae --- /dev/null +++ b/04-dom-events-async/06-bubbling-and-delegation/starter.js @@ -0,0 +1,5 @@ +// 任务: +// 1. 给 panel 绑定点击事件,输出一条日志 +// 2. 给 lesson-list 绑定点击事件,使用事件委托 +// 3. 点击 li 时切换 active 类名 +// 4. 点击删除按钮时,阻止冒泡并删除当前 li diff --git a/04-dom-events-async/07-timers-and-async-order/README.md b/04-dom-events-async/07-timers-and-async-order/README.md new file mode 100644 index 0000000..626ff25 --- /dev/null +++ b/04-dom-events-async/07-timers-and-async-order/README.md @@ -0,0 +1,25 @@ +# 练习 7:setTimeout 和异步顺序 + +## 目标 + +理解同步代码和异步回调的执行先后顺序。 + +## 你要练什么 + +- `setTimeout` +- 同步顺序 +- 异步回调 +- DOM 日志输出 + +## 任务 + +- 点击按钮后先输出“开始执行” +- 再立刻输出“同步代码结束” +- 然后延迟输出“异步回调完成” + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/07-timers-and-async-order/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/07-timers-and-async-order/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/07-timers-and-async-order/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/07-timers-and-async-order/answer.js) diff --git a/04-dom-events-async/07-timers-and-async-order/answer.html b/04-dom-events-async/07-timers-and-async-order/answer.html new file mode 100644 index 0000000..0b4117d --- /dev/null +++ b/04-dom-events-async/07-timers-and-async-order/answer.html @@ -0,0 +1,38 @@ + + + + + + setTimeout 和异步顺序 + + + +
+

异步顺序练习

+ + +
+ + + + diff --git a/04-dom-events-async/07-timers-and-async-order/answer.js b/04-dom-events-async/07-timers-and-async-order/answer.js new file mode 100644 index 0000000..ef2ec3b --- /dev/null +++ b/04-dom-events-async/07-timers-and-async-order/answer.js @@ -0,0 +1,20 @@ +const runButton = document.getElementById("run-btn"); +const logList = document.getElementById("log-list"); + +function appendLog(text) { + const item = document.createElement("li"); + item.textContent = text; + logList.appendChild(item); +} + +runButton.addEventListener("click", function () { + logList.innerHTML = ""; + + appendLog("开始执行"); + + setTimeout(function () { + appendLog("异步回调完成"); + }, 600); + + appendLog("同步代码结束"); +}); diff --git a/04-dom-events-async/07-timers-and-async-order/starter.html b/04-dom-events-async/07-timers-and-async-order/starter.html new file mode 100644 index 0000000..a92d8cd --- /dev/null +++ b/04-dom-events-async/07-timers-and-async-order/starter.html @@ -0,0 +1,38 @@ + + + + + + setTimeout 和异步顺序 + + + +
+

异步顺序练习

+ + +
+ + + + diff --git a/04-dom-events-async/07-timers-and-async-order/starter.js b/04-dom-events-async/07-timers-and-async-order/starter.js new file mode 100644 index 0000000..4a10696 --- /dev/null +++ b/04-dom-events-async/07-timers-and-async-order/starter.js @@ -0,0 +1,6 @@ +// 任务: +// 1. 获取按钮和日志列表 +// 2. 点击按钮后清空旧日志 +// 3. 先追加“开始执行” +// 4. 用 setTimeout 延迟追加“异步回调完成” +// 5. 再立刻追加“同步代码结束” diff --git a/04-dom-events-async/08-promise-and-render/README.md b/04-dom-events-async/08-promise-and-render/README.md new file mode 100644 index 0000000..d328c7a --- /dev/null +++ b/04-dom-events-async/08-promise-and-render/README.md @@ -0,0 +1,26 @@ +# 练习 8:Promise 和渲染 + +## 目标 + +学会在 Promise 完成后把结果渲染到页面。 + +## 你要练什么 + +- Promise +- `.then()` +- `catch()` +- loading 状态 + +## 任务 + +- 点击按钮后显示“加载中” +- 等待 Promise 返回数据 +- 把课程名称渲染到列表 +- 如果失败,显示错误信息 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/08-promise-and-render/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/08-promise-and-render/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/08-promise-and-render/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/08-promise-and-render/answer.js) diff --git a/04-dom-events-async/08-promise-and-render/answer.html b/04-dom-events-async/08-promise-and-render/answer.html new file mode 100644 index 0000000..15f6fcc --- /dev/null +++ b/04-dom-events-async/08-promise-and-render/answer.html @@ -0,0 +1,35 @@ + + + + + + Promise 和渲染 + + + +
+

Promise 数据渲染

+ +

等待加载

+ +
+ + + + diff --git a/04-dom-events-async/08-promise-and-render/answer.js b/04-dom-events-async/08-promise-and-render/answer.js new file mode 100644 index 0000000..5b08732 --- /dev/null +++ b/04-dom-events-async/08-promise-and-render/answer.js @@ -0,0 +1,30 @@ +function fakeFetchCourses() { + return new Promise(function (resolve) { + setTimeout(function () { + resolve(["DOM 获取元素", "事件监听", "异步基础"]); + }, 800); + }); +} + +const loadButton = document.getElementById("load-btn"); +const statusText = document.getElementById("status-text"); +const courseList = document.getElementById("course-list"); + +loadButton.addEventListener("click", function () { + statusText.textContent = "加载中..."; + courseList.innerHTML = ""; + + fakeFetchCourses() + .then(function (courses) { + courses.forEach(function (course) { + const item = document.createElement("li"); + item.textContent = course; + courseList.appendChild(item); + }); + + statusText.textContent = "加载完成"; + }) + .catch(function () { + statusText.textContent = "加载失败"; + }); +}); diff --git a/04-dom-events-async/08-promise-and-render/starter.html b/04-dom-events-async/08-promise-and-render/starter.html new file mode 100644 index 0000000..f6b33b2 --- /dev/null +++ b/04-dom-events-async/08-promise-and-render/starter.html @@ -0,0 +1,35 @@ + + + + + + Promise 和渲染 + + + +
+

Promise 数据渲染

+ +

等待加载

+ +
+ + + + diff --git a/04-dom-events-async/08-promise-and-render/starter.js b/04-dom-events-async/08-promise-and-render/starter.js new file mode 100644 index 0000000..9a0df22 --- /dev/null +++ b/04-dom-events-async/08-promise-and-render/starter.js @@ -0,0 +1,14 @@ +function fakeFetchCourses() { + return new Promise(function (resolve) { + setTimeout(function () { + resolve(["DOM 获取元素", "事件监听", "异步基础"]); + }, 800); + }); +} + +// 任务: +// 1. 获取按钮、状态文字、列表 +// 2. 点击按钮后显示“加载中” +// 3. 调用 fakeFetchCourses() +// 4. 用 then 渲染课程列表 +// 5. 用 catch 处理错误 diff --git a/04-dom-events-async/09-async-await-panel/README.md b/04-dom-events-async/09-async-await-panel/README.md new file mode 100644 index 0000000..ecc806a --- /dev/null +++ b/04-dom-events-async/09-async-await-panel/README.md @@ -0,0 +1,26 @@ +# 练习 9:async / await 面板 + +## 目标 + +学会用 `async` / `await` 写一个更直观的异步流程。 + +## 你要练什么 + +- `async` +- `await` +- `try...catch` +- DOM 更新 + +## 任务 + +- 点击按钮后进入加载状态 +- 等待异步函数返回用户信息 +- 把结果渲染到卡片里 +- 如果失败,显示错误信息 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/09-async-await-panel/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/09-async-await-panel/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/09-async-await-panel/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/09-async-await-panel/answer.js) diff --git a/04-dom-events-async/09-async-await-panel/answer.html b/04-dom-events-async/09-async-await-panel/answer.html new file mode 100644 index 0000000..437d0b0 --- /dev/null +++ b/04-dom-events-async/09-async-await-panel/answer.html @@ -0,0 +1,35 @@ + + + + + + async / await 面板 + + + +
+

用户信息面板

+ +

等待加载

+
+
+ + + + diff --git a/04-dom-events-async/09-async-await-panel/answer.js b/04-dom-events-async/09-async-await-panel/answer.js new file mode 100644 index 0000000..d688245 --- /dev/null +++ b/04-dom-events-async/09-async-await-panel/answer.js @@ -0,0 +1,35 @@ +function fakeFetchUser() { + return new Promise(function (resolve) { + setTimeout(function () { + resolve({ + name: "林晨", + role: "前端学习者", + focus: "DOM + 事件 + 异步", + }); + }, 900); + }); +} + +const loadButton = document.getElementById("load-user-btn"); +const statusText = document.getElementById("user-status"); +const userCard = document.getElementById("user-card"); + +async function loadUser() { + statusText.textContent = "加载中..."; + userCard.innerHTML = ""; + + try { + const user = await fakeFetchUser(); + + userCard.innerHTML = ` +

姓名:${user.name}

+

身份:${user.role}

+

当前重点:${user.focus}

+ `; + statusText.textContent = "加载完成"; + } catch (error) { + statusText.textContent = "加载失败"; + } +} + +loadButton.addEventListener("click", loadUser); diff --git a/04-dom-events-async/09-async-await-panel/starter.html b/04-dom-events-async/09-async-await-panel/starter.html new file mode 100644 index 0000000..21f511c --- /dev/null +++ b/04-dom-events-async/09-async-await-panel/starter.html @@ -0,0 +1,35 @@ + + + + + + async / await 面板 + + + +
+

用户信息面板

+ +

等待加载

+
+
+ + + + diff --git a/04-dom-events-async/09-async-await-panel/starter.js b/04-dom-events-async/09-async-await-panel/starter.js new file mode 100644 index 0000000..26538b6 --- /dev/null +++ b/04-dom-events-async/09-async-await-panel/starter.js @@ -0,0 +1,18 @@ +function fakeFetchUser() { + return new Promise(function (resolve) { + setTimeout(function () { + resolve({ + name: "林晨", + role: "前端学习者", + focus: "DOM + 事件 + 异步", + }); + }, 900); + }); +} + +// 任务: +// 1. 获取按钮、状态文字、信息面板 +// 2. 写一个 async 函数 +// 3. 用 await 等待 fakeFetchUser() +// 4. 渲染用户信息 +// 5. 用 try...catch 处理异常 diff --git a/04-dom-events-async/10-final-dashboard/README.md b/04-dom-events-async/10-final-dashboard/README.md new file mode 100644 index 0000000..5f77577 --- /dev/null +++ b/04-dom-events-async/10-final-dashboard/README.md @@ -0,0 +1,34 @@ +# 练习 10:综合页面 + +## 目标 + +把 DOM、事件和异步知识拼起来,做一个可以真实互动的小页面。 + +## 项目名称 + +学习面板 + +## 任务 + +请完成一个控制台之外可见的页面交互,要求至少包含: + +- 一个课程列表区域 +- 一个表单区域 +- 一个添加课程的交互 +- 一个点击课程切换完成状态的交互 +- 一个异步加载提示或远程数据模拟 +- 清晰的状态文案更新 + +## 建议顺序 + +1. 先看 HTML 结构 +2. 先写元素获取和渲染函数 +3. 再写表单提交和点击事件 +4. 最后补异步加载逻辑 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/10-final-dashboard/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/10-final-dashboard/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/10-final-dashboard/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/10-final-dashboard/answer.js) diff --git a/04-dom-events-async/10-final-dashboard/answer.html b/04-dom-events-async/10-final-dashboard/answer.html new file mode 100644 index 0000000..a32e647 --- /dev/null +++ b/04-dom-events-async/10-final-dashboard/answer.html @@ -0,0 +1,82 @@ + + + + + + 学习面板 + + + +
+
+

DOM + 事件 + 异步学习面板

+

正在初始化页面...

+
+ +
+

课程列表

+ + +
+ +
+

添加课程

+
+ + +
+
+
+ + + + diff --git a/04-dom-events-async/10-final-dashboard/answer.js b/04-dom-events-async/10-final-dashboard/answer.js new file mode 100644 index 0000000..40623cf --- /dev/null +++ b/04-dom-events-async/10-final-dashboard/answer.js @@ -0,0 +1,84 @@ +const lessons = [ + { title: "获取元素", done: false }, + { title: "修改 DOM", done: true }, +]; + +function fakeLoadExtraLessons() { + return new Promise(function (resolve) { + setTimeout(function () { + resolve([ + { title: "事件委托", done: false }, + { title: "异步渲染", done: false }, + ]); + }, 900); + }); +} + +const statusText = document.getElementById("status-text"); +const loadButton = document.getElementById("load-btn"); +const lessonList = document.getElementById("lesson-list"); +const lessonForm = document.getElementById("lesson-form"); +const lessonInput = document.getElementById("lesson-input"); + +function renderLessons() { + lessonList.innerHTML = ""; + + lessons.forEach(function (lesson, index) { + const item = document.createElement("li"); + item.className = "lesson-item"; + + if (lesson.done) { + item.classList.add("done"); + } + + item.dataset.index = index; + item.textContent = lesson.title; + lessonList.appendChild(item); + }); +} + +lessonList.addEventListener("click", function (event) { + const currentItem = event.target.closest(".lesson-item"); + + if (!currentItem) { + return; + } + + const index = Number(currentItem.dataset.index); + lessons[index].done = !lessons[index].done; + renderLessons(); +}); + +lessonForm.addEventListener("submit", function (event) { + event.preventDefault(); + + const value = lessonInput.value.trim(); + + if (!value) { + return; + } + + lessons.push({ + title: value, + done: false, + }); + + lessonInput.value = ""; + statusText.textContent = "已新增一门课程"; + renderLessons(); +}); + +loadButton.addEventListener("click", async function () { + statusText.textContent = "正在异步加载课程..."; + + const newLessons = await fakeLoadExtraLessons(); + newLessons.forEach(function (lesson) { + lessons.push(lesson); + }); + + statusText.textContent = "额外课程加载完成"; + renderLessons(); +}); + +statusText.textContent = "页面初始化完成"; +renderLessons(); diff --git a/04-dom-events-async/10-final-dashboard/starter.html b/04-dom-events-async/10-final-dashboard/starter.html new file mode 100644 index 0000000..6406afd --- /dev/null +++ b/04-dom-events-async/10-final-dashboard/starter.html @@ -0,0 +1,82 @@ + + + + + + 学习面板 + + + +
+
+

DOM + 事件 + 异步学习面板

+

正在初始化页面...

+
+ +
+

课程列表

+ + +
+ +
+

添加课程

+
+ + +
+
+
+ + + + diff --git a/04-dom-events-async/10-final-dashboard/starter.js b/04-dom-events-async/10-final-dashboard/starter.js new file mode 100644 index 0000000..b0cdcfc --- /dev/null +++ b/04-dom-events-async/10-final-dashboard/starter.js @@ -0,0 +1,22 @@ +const lessons = [ + { title: "获取元素", done: false }, + { title: "修改 DOM", done: true }, +]; + +function fakeLoadExtraLessons() { + return new Promise(function (resolve) { + setTimeout(function () { + resolve([ + { title: "事件委托", done: false }, + { title: "异步渲染", done: false }, + ]); + }, 900); + }); +} + +// 任务: +// 1. 获取页面里的关键元素 +// 2. 写一个 renderLessons 函数,把 lessons 渲染到列表 +// 3. 点击列表项时切换 done 状态 +// 4. 提交表单时阻止默认提交,并新增课程 +// 5. 点击加载按钮时显示加载状态,并把异步返回的课程追加到列表 diff --git a/04-dom-events-async/11-input-live-preview/README.md b/04-dom-events-async/11-input-live-preview/README.md new file mode 100644 index 0000000..9f0a312 --- /dev/null +++ b/04-dom-events-async/11-input-live-preview/README.md @@ -0,0 +1,25 @@ +# 练习 11:input 实时预览 + +## 目标 + +学会处理输入类事件,让页面随着用户输入实时变化。 + +## 你要练什么 + +- `input` 事件 +- `change` 事件 +- 表单值读取 +- 实时 DOM 更新 + +## 任务 + +- 在输入框输入昵称时,实时更新预览标题 +- 在文本域输入学习目标时,实时更新预览内容 +- 切换学习阶段下拉框时,更新阶段标签 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/11-input-live-preview/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/11-input-live-preview/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/11-input-live-preview/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/11-input-live-preview/answer.js) diff --git a/04-dom-events-async/11-input-live-preview/answer.html b/04-dom-events-async/11-input-live-preview/answer.html new file mode 100644 index 0000000..df61394 --- /dev/null +++ b/04-dom-events-async/11-input-live-preview/answer.html @@ -0,0 +1,86 @@ + + + + + + input 实时预览 + + + +
+
+

编辑资料

+ + + + + + +
+ +
+

入门阶段

+

未填写昵称

+

这里会显示你的学习目标。

+
+
+ + + + diff --git a/04-dom-events-async/11-input-live-preview/answer.js b/04-dom-events-async/11-input-live-preview/answer.js new file mode 100644 index 0000000..6ce5f3b --- /dev/null +++ b/04-dom-events-async/11-input-live-preview/answer.js @@ -0,0 +1,21 @@ +const nicknameInput = document.getElementById("nickname-input"); +const goalInput = document.getElementById("goal-input"); +const stageSelect = document.getElementById("stage-select"); + +const previewStage = document.getElementById("preview-stage"); +const previewName = document.getElementById("preview-name"); +const previewGoal = document.getElementById("preview-goal"); + +nicknameInput.addEventListener("input", function () { + const value = nicknameInput.value.trim(); + previewName.textContent = value || "未填写昵称"; +}); + +goalInput.addEventListener("input", function () { + const value = goalInput.value.trim(); + previewGoal.textContent = value || "这里会显示你的学习目标。"; +}); + +stageSelect.addEventListener("change", function () { + previewStage.textContent = stageSelect.value; +}); diff --git a/04-dom-events-async/11-input-live-preview/starter.html b/04-dom-events-async/11-input-live-preview/starter.html new file mode 100644 index 0000000..550dfb8 --- /dev/null +++ b/04-dom-events-async/11-input-live-preview/starter.html @@ -0,0 +1,86 @@ + + + + + + input 实时预览 + + + +
+
+

编辑资料

+ + + + + + +
+ +
+

入门阶段

+

未填写昵称

+

这里会显示你的学习目标。

+
+
+ + + + diff --git a/04-dom-events-async/11-input-live-preview/starter.js b/04-dom-events-async/11-input-live-preview/starter.js new file mode 100644 index 0000000..9ee6472 --- /dev/null +++ b/04-dom-events-async/11-input-live-preview/starter.js @@ -0,0 +1,6 @@ +// 任务: +// 1. 获取昵称输入框、目标文本域、阶段下拉框 +// 2. 监听昵称和目标的 input 事件 +// 3. 把输入内容实时渲染到右侧预览 +// 4. 监听阶段下拉框的 change 事件 +// 5. 更新阶段标签文字 diff --git a/04-dom-events-async/12-prepend-remove-and-link-default/README.md b/04-dom-events-async/12-prepend-remove-and-link-default/README.md new file mode 100644 index 0000000..c2f1bc5 --- /dev/null +++ b/04-dom-events-async/12-prepend-remove-and-link-default/README.md @@ -0,0 +1,25 @@ +# 练习 12:prepend、classList.remove 和链接默认行为 + +## 目标 + +补齐几个常见但容易漏掉的 DOM 细节操作。 + +## 你要练什么 + +- `prepend()` +- `classList.remove()` +- 链接点击事件 +- `preventDefault()` + +## 任务 + +- 点击“插入到最前面”时,把一条新消息插入列表顶部 +- 点击“清除高亮”时,移除全部高亮类名 +- 点击帮助链接时,阻止默认跳转,并在页面显示提示信息 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/12-prepend-remove-and-link-default/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/12-prepend-remove-and-link-default/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/12-prepend-remove-and-link-default/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/12-prepend-remove-and-link-default/answer.js) diff --git a/04-dom-events-async/12-prepend-remove-and-link-default/answer.html b/04-dom-events-async/12-prepend-remove-and-link-default/answer.html new file mode 100644 index 0000000..b6b4aa3 --- /dev/null +++ b/04-dom-events-async/12-prepend-remove-and-link-default/answer.html @@ -0,0 +1,62 @@ + + + + + + prepend、classList.remove 和链接默认行为 + + + +
+

补漏练习

+
+ + + 查看帮助 +
+ +

这里会显示操作提示。

+ + +
+ + + + diff --git a/04-dom-events-async/12-prepend-remove-and-link-default/answer.js b/04-dom-events-async/12-prepend-remove-and-link-default/answer.js new file mode 100644 index 0000000..cd259b6 --- /dev/null +++ b/04-dom-events-async/12-prepend-remove-and-link-default/answer.js @@ -0,0 +1,31 @@ +const prependButton = document.getElementById("prepend-btn"); +const clearActiveButton = document.getElementById("clear-active-btn"); +const helpLink = document.getElementById("help-link"); +const hintText = document.getElementById("hint-text"); +const messageList = document.getElementById("message-list"); + +let messageIndex = 1; + +prependButton.addEventListener("click", function () { + const item = document.createElement("li"); + item.className = "item active"; + item.textContent = `新插入的提醒 ${messageIndex}`; + messageList.prepend(item); + hintText.textContent = "已把一条消息插入到最前面"; + messageIndex += 1; +}); + +clearActiveButton.addEventListener("click", function () { + const items = document.querySelectorAll(".item"); + + items.forEach(function (item) { + item.classList.remove("active"); + }); + + hintText.textContent = "已移除全部高亮状态"; +}); + +helpLink.addEventListener("click", function (event) { + event.preventDefault(); + hintText.textContent = "已阻止默认跳转"; +}); diff --git a/04-dom-events-async/12-prepend-remove-and-link-default/starter.html b/04-dom-events-async/12-prepend-remove-and-link-default/starter.html new file mode 100644 index 0000000..f8f3b56 --- /dev/null +++ b/04-dom-events-async/12-prepend-remove-and-link-default/starter.html @@ -0,0 +1,62 @@ + + + + + + prepend、classList.remove 和链接默认行为 + + + +
+

补漏练习

+
+ + + 查看帮助 +
+ +

这里会显示操作提示。

+ + +
+ + + + diff --git a/04-dom-events-async/12-prepend-remove-and-link-default/starter.js b/04-dom-events-async/12-prepend-remove-and-link-default/starter.js new file mode 100644 index 0000000..37879aa --- /dev/null +++ b/04-dom-events-async/12-prepend-remove-and-link-default/starter.js @@ -0,0 +1,6 @@ +// 任务: +// 1. 获取两个按钮、帮助链接、提示文字和列表 +// 2. 点击 prepend-btn 时创建新 li,并插入到列表最前面 +// 3. 点击 clear-active-btn 时移除所有 item 的 active 类名 +// 4. 点击帮助链接时,用 preventDefault() 阻止跳转 +// 5. 在提示文字里输出“已阻止默认跳转” diff --git a/04-dom-events-async/README.md b/04-dom-events-async/README.md new file mode 100644 index 0000000..c83ae34 --- /dev/null +++ b/04-dom-events-async/README.md @@ -0,0 +1,176 @@ +# DOM + 事件 + 异步 + +这部分只解决一个问题:你能不能让页面里的元素被 JavaScript 真正控制起来,并对用户操作和异步结果做出响应。 + +## 学完后你应该掌握 + +- 如何获取页面元素 +- 如何修改文本、样式和类名 +- 如何创建、插入和删除节点 +- 如何监听点击、输入、提交等事件 +- `preventDefault()` 和 `stopPropagation()` 的常见场景 +- 事件委托的基本写法 +- `prepend()`、`classList.remove()` 的基础使用 +- `setTimeout` 的异步顺序 +- Promise 的基础用法 +- `async` / `await` 的基础写法 +- 如何把 DOM、事件、异步组合成一个小页面 + +## 这一章在解决什么 + +前面三章分别解决了: + +- 页面有什么结构 +- 页面长什么样 +- 代码逻辑怎么写 + +这一章开始,JavaScript 不再只是打印到控制台,而是要真正操作页面。 + +它回答的是: + +- 我怎么拿到某个按钮或输入框 +- 用户点击后页面怎么变 +- 表单提交后怎么处理 +- 数据晚一点回来时页面怎么更新 + +## 必须建立的 5 个核心意识 + +### 1. 先选中元素,再操作元素 + +DOM 操作第一步不是“改”,而是“找到”。 + +常见方式: + +- `getElementById` +- `querySelector` +- `querySelectorAll` + +### 2. 事件是页面和用户的连接点 + +页面本身不会“自动响应”。 + +是你通过事件监听告诉浏览器: + +- 点了按钮要做什么 +- 输入框变化后要做什么 +- 提交表单要做什么 + +### 3. 改页面就是改 DOM + +常见 DOM 改动包括: + +- 改文字:`textContent` +- 改类名:`classList` +- 改样式:`style` +- 增删节点:`appendChild`、`remove` + +### 4. 异步不是“慢一点执行”,而是“先不阻塞主流程” + +比如: + +- `setTimeout` +- Promise +- `async` / `await` + +这些都意味着:当前代码先继续往下走,结果稍后回来再处理。 + +### 5. 一个完整交互通常是“事件 + DOM + 状态 + 异步” + +例如点击“加载数据”按钮时: + +1. 监听点击事件 +2. 更新 loading 状态 +3. 等待异步结果 +4. 把结果渲染到页面 + +## 高频 API 速记 + +### 获取元素 + +- `document.getElementById()` +- `document.querySelector()` +- `document.querySelectorAll()` + +### 修改元素 + +- `textContent` +- `innerHTML` +- `style` +- `classList.add()` +- `classList.remove()` +- `classList.toggle()` + +### 节点操作 + +- `document.createElement()` +- `appendChild()` +- `prepend()` +- `remove()` + +### 事件 + +- `addEventListener()` +- `event.target` +- `preventDefault()` +- `stopPropagation()` + +### 异步 + +- `setTimeout()` +- `Promise` +- `.then()` +- `async` +- `await` + +## 学习顺序 + +1. 获取元素 +2. 修改文本、类名、样式 +3. 创建和删除节点 +4. 点击事件 +5. 表单提交和 `preventDefault` +6. 冒泡、委托和 `stopPropagation` +7. `setTimeout` 和异步顺序 +8. Promise 渲染 +9. `async` / `await` +10. 综合小页面 +11. `input` / `change` 事件 +12. `prepend`、`classList.remove` 和链接默认行为 + +## 练习目录 + +- [01-query-selectors/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/01-query-selectors/README.md) +- [02-text-class-style/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/02-text-class-style/README.md) +- [03-create-and-remove/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/03-create-and-remove/README.md) +- [04-click-counter/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/04-click-counter/README.md) +- [05-form-submit-and-prevent-default/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/05-form-submit-and-prevent-default/README.md) +- [06-bubbling-and-delegation/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/06-bubbling-and-delegation/README.md) +- [07-timers-and-async-order/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/07-timers-and-async-order/README.md) +- [08-promise-and-render/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/08-promise-and-render/README.md) +- [09-async-await-panel/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/09-async-await-panel/README.md) +- [10-final-dashboard/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/10-final-dashboard/README.md) +- [11-input-live-preview/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/11-input-live-preview/README.md) +- [12-prepend-remove-and-link-default/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/12-prepend-remove-and-link-default/README.md) + +## 过关标准 + +如果你能独立做到下面这些,就说明这一章已经基本过关: + +- 能正确选中页面里的元素 +- 能改文字、类名和内联样式 +- 能动态创建和删除列表项 +- 能给按钮、表单绑定事件 +- 能理解事件冒泡和事件委托 +- 能在需要时使用 `preventDefault()` 和 `stopPropagation()` +- 能处理 `input` / `change` 这类常见表单事件 +- 能使用 `prepend()`、`classList.remove()` 完成简单 DOM 调整 +- 能理解 `setTimeout` 的异步顺序 +- 能用 Promise 和 `async` / `await` 处理一个简单异步流程 +- 能做出一个有真实交互的小页面 + +## 学习建议 + +- 每个练习都用浏览器打开 `starter.html` +- 打开开发者工具,边看页面边看控制台 +- 一个交互没反应时,先确认元素有没有选中 +- 一个异步结果不对时,先打印中间状态 diff --git a/05-es6-plus/01-template-and-destructuring/README.md b/05-es6-plus/01-template-and-destructuring/README.md new file mode 100644 index 0000000..0b052a2 --- /dev/null +++ b/05-es6-plus/01-template-and-destructuring/README.md @@ -0,0 +1,24 @@ +# 练习 1:模板字符串和解构 + +## 目标 + +学会用模板字符串拼接信息,并用解构快速读取对象和数组数据。 + +## 你要练什么 + +- 模板字符串 +- 对象解构 +- 数组解构 + +## 任务 + +- 从 `student` 对象里解构出姓名和阶段 +- 从 `scores` 数组里解构出前两个分数 +- 用模板字符串把这些信息渲染到页面 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/01-template-and-destructuring/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/01-template-and-destructuring/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/01-template-and-destructuring/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/01-template-and-destructuring/answer.js) diff --git a/05-es6-plus/01-template-and-destructuring/answer.html b/05-es6-plus/01-template-and-destructuring/answer.html new file mode 100644 index 0000000..00fc6a2 --- /dev/null +++ b/05-es6-plus/01-template-and-destructuring/answer.html @@ -0,0 +1,16 @@ + + + + + + 模板字符串和解构 + + +
+

ES6+ 练习 1

+

等待渲染

+
+ + + + diff --git a/05-es6-plus/01-template-and-destructuring/answer.js b/05-es6-plus/01-template-and-destructuring/answer.js new file mode 100644 index 0000000..abf190f --- /dev/null +++ b/05-es6-plus/01-template-and-destructuring/answer.js @@ -0,0 +1,11 @@ +const student = { + name: "林晨", + stage: "ES6+", +}; + +const scores = [88, 92, 95]; + +const { name, stage } = student; +const [firstScore, secondScore] = scores; + +document.getElementById("output").textContent = `${name} 正在学习 ${stage},前两次练习分数分别是 ${firstScore} 和 ${secondScore}。`; diff --git a/05-es6-plus/01-template-and-destructuring/starter.html b/05-es6-plus/01-template-and-destructuring/starter.html new file mode 100644 index 0000000..876c16e --- /dev/null +++ b/05-es6-plus/01-template-and-destructuring/starter.html @@ -0,0 +1,16 @@ + + + + + + 模板字符串和解构 + + +
+

ES6+ 练习 1

+

等待渲染

+
+ + + + diff --git a/05-es6-plus/01-template-and-destructuring/starter.js b/05-es6-plus/01-template-and-destructuring/starter.js new file mode 100644 index 0000000..a2445dc --- /dev/null +++ b/05-es6-plus/01-template-and-destructuring/starter.js @@ -0,0 +1,11 @@ +const student = { + name: "林晨", + stage: "ES6+", +}; + +const scores = [88, 92, 95]; + +// 任务: +// 1. 解构出 name、stage +// 2. 解构出前两个分数 +// 3. 用模板字符串把信息写入 #output diff --git a/05-es6-plus/02-spread-and-rest/README.md b/05-es6-plus/02-spread-and-rest/README.md new file mode 100644 index 0000000..ff3982e --- /dev/null +++ b/05-es6-plus/02-spread-and-rest/README.md @@ -0,0 +1,24 @@ +# 练习 2:展开运算符和剩余参数 + +## 目标 + +学会复制数组、合并对象,以及用剩余参数接收不定数量的参数。 + +## 你要练什么 + +- 数组展开 +- 对象展开 +- 剩余参数 + +## 任务 + +- 复制一份课程数组并新增一项 +- 基于用户对象生成一个更新后的对象 +- 写一个 `sumScores` 函数,用剩余参数计算总分 + +## 文件 + +- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/02-spread-and-rest/starter.html) +- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/02-spread-and-rest/starter.js) +- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/02-spread-and-rest/answer.html) +- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/02-spread-and-rest/answer.js) diff --git a/05-es6-plus/02-spread-and-rest/answer.html b/05-es6-plus/02-spread-and-rest/answer.html new file mode 100644 index 0000000..55f5883 --- /dev/null +++ b/05-es6-plus/02-spread-and-rest/answer.html @@ -0,0 +1,12 @@ + + + + + + 展开运算符和剩余参数 + + +

+    
+  
+
diff --git a/05-es6-plus/02-spread-and-rest/answer.js b/05-es6-plus/02-spread-and-rest/answer.js
new file mode 100644
index 0000000..1ece5b1
--- /dev/null
+++ b/05-es6-plus/02-spread-and-rest/answer.js
@@ -0,0 +1,24 @@
+const tracks = ["HTML", "CSS", "JavaScript"];
+
+const user = {
+  name: "林晨",
+  stage: "基础阶段",
+};
+
+function sumScores(...scores) {
+  return scores.reduce((total, score) => total + score, 0);
+}
+
+const newTracks = [...tracks, "ES6+"];
+const nextUser = { ...user, stage: "现代 JS" };
+const totalScore = sumScores(88, 91, 95);
+
+document.getElementById("output").textContent = JSON.stringify(
+  {
+    newTracks,
+    nextUser,
+    totalScore,
+  },
+  null,
+  2
+);
diff --git a/05-es6-plus/02-spread-and-rest/starter.html b/05-es6-plus/02-spread-and-rest/starter.html
new file mode 100644
index 0000000..52799ff
--- /dev/null
+++ b/05-es6-plus/02-spread-and-rest/starter.html
@@ -0,0 +1,12 @@
+
+
+  
+    
+    
+    展开运算符和剩余参数
+  
+  
+    

+    
+  
+
diff --git a/05-es6-plus/02-spread-and-rest/starter.js b/05-es6-plus/02-spread-and-rest/starter.js
new file mode 100644
index 0000000..86fbe35
--- /dev/null
+++ b/05-es6-plus/02-spread-and-rest/starter.js
@@ -0,0 +1,16 @@
+const tracks = ["HTML", "CSS", "JavaScript"];
+
+const user = {
+  name: "林晨",
+  stage: "基础阶段",
+};
+
+function sumScores(...scores) {
+  // 返回总分
+}
+
+// 任务:
+// 1. 复制 tracks 并新增 "ES6+"
+// 2. 基于 user 生成一个 stage 为 "现代 JS" 的新对象
+// 3. 调用 sumScores
+// 4. 输出结果到 #output
diff --git a/05-es6-plus/03-arrow-functions/README.md b/05-es6-plus/03-arrow-functions/README.md
new file mode 100644
index 0000000..cf4631a
--- /dev/null
+++ b/05-es6-plus/03-arrow-functions/README.md
@@ -0,0 +1,23 @@
+# 练习 3:箭头函数
+
+## 目标
+
+学会把简单函数改写成箭头函数。
+
+## 你要练什么
+
+- 箭头函数
+- 简写返回值
+- 数组映射
+
+## 任务
+
+- 把两个普通函数改成箭头函数
+- 用 `map` 和箭头函数生成课程标签
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/03-arrow-functions/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/03-arrow-functions/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/03-arrow-functions/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/03-arrow-functions/answer.js)
diff --git a/05-es6-plus/03-arrow-functions/answer.html b/05-es6-plus/03-arrow-functions/answer.html
new file mode 100644
index 0000000..492e3d7
--- /dev/null
+++ b/05-es6-plus/03-arrow-functions/answer.html
@@ -0,0 +1,12 @@
+
+
+  
+    
+    
+    箭头函数
+  
+  
+    

+    
+  
+
diff --git a/05-es6-plus/03-arrow-functions/answer.js b/05-es6-plus/03-arrow-functions/answer.js
new file mode 100644
index 0000000..3ded488
--- /dev/null
+++ b/05-es6-plus/03-arrow-functions/answer.js
@@ -0,0 +1,15 @@
+const getLevel = (score) => (score >= 80 ? "达标" : "继续练习");
+const add = (a, b) => a + b;
+
+const tracks = ["DOM", "异步", "模块化"];
+const labels = tracks.map((track) => `[${track}]`);
+
+document.getElementById("output").textContent = JSON.stringify(
+  {
+    level: getLevel(86),
+    sum: add(12, 8),
+    labels,
+  },
+  null,
+  2
+);
diff --git a/05-es6-plus/03-arrow-functions/starter.html b/05-es6-plus/03-arrow-functions/starter.html
new file mode 100644
index 0000000..1a35351
--- /dev/null
+++ b/05-es6-plus/03-arrow-functions/starter.html
@@ -0,0 +1,12 @@
+
+
+  
+    
+    
+    箭头函数
+  
+  
+    

+    
+  
+
diff --git a/05-es6-plus/03-arrow-functions/starter.js b/05-es6-plus/03-arrow-functions/starter.js
new file mode 100644
index 0000000..29de476
--- /dev/null
+++ b/05-es6-plus/03-arrow-functions/starter.js
@@ -0,0 +1,14 @@
+function getLevel(score) {
+  return score >= 80 ? "达标" : "继续练习";
+}
+
+function add(a, b) {
+  return a + b;
+}
+
+const tracks = ["DOM", "异步", "模块化"];
+
+// 任务:
+// 1. 把 getLevel 改成箭头函数
+// 2. 把 add 改成箭头函数
+// 3. 用 map + 箭头函数生成 ["[DOM]", ...]
diff --git a/05-es6-plus/04-modules-basic/README.md b/05-es6-plus/04-modules-basic/README.md
new file mode 100644
index 0000000..52c8632
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/README.md
@@ -0,0 +1,24 @@
+# 练习 4:模块基础
+
+## 目标
+
+学会用 `export` 和 `import` 把不同文件连起来。
+
+## 你要练什么
+
+- `export`
+- `import`
+- `type="module"`
+
+## 任务
+
+- 从模块文件里导入课程数据和格式化函数
+- 把结果渲染到页面
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/starter.js)
+- [course-data.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/course-data.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/answer.js)
diff --git a/05-es6-plus/04-modules-basic/answer.html b/05-es6-plus/04-modules-basic/answer.html
new file mode 100644
index 0000000..146ebbd
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/answer.html
@@ -0,0 +1,12 @@
+
+
+  
+    
+    
+    模块基础
+  
+  
+    

+    
+  
+
diff --git a/05-es6-plus/04-modules-basic/answer.js b/05-es6-plus/04-modules-basic/answer.js
new file mode 100644
index 0000000..3356d57
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/answer.js
@@ -0,0 +1,4 @@
+import { courses, formatCourse } from "./course-data.js";
+
+const lines = courses.map((course) => formatCourse(course));
+document.getElementById("output").textContent = lines.join("\n");
diff --git a/05-es6-plus/04-modules-basic/course-data.js b/05-es6-plus/04-modules-basic/course-data.js
new file mode 100644
index 0000000..9036db4
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/course-data.js
@@ -0,0 +1,3 @@
+export const courses = ["解构", "展开运算符", "Promise"];
+
+export const formatCourse = (course) => `正在学习:${course}`;
diff --git a/05-es6-plus/04-modules-basic/starter.html b/05-es6-plus/04-modules-basic/starter.html
new file mode 100644
index 0000000..06ece18
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/starter.html
@@ -0,0 +1,12 @@
+
+
+  
+    
+    
+    模块基础
+  
+  
+    

+    
+  
+
diff --git a/05-es6-plus/04-modules-basic/starter.js b/05-es6-plus/04-modules-basic/starter.js
new file mode 100644
index 0000000..e5cd937
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/starter.js
@@ -0,0 +1,3 @@
+// 任务:
+// 1. 从 ./course-data.js 导入 courses 和 formatCourse
+// 2. 把格式化后的结果写入 #output
diff --git a/05-es6-plus/05-promise-basics/README.md b/05-es6-plus/05-promise-basics/README.md
new file mode 100644
index 0000000..87389ed
--- /dev/null
+++ b/05-es6-plus/05-promise-basics/README.md
@@ -0,0 +1,24 @@
+# 练习 5:Promise 基础
+
+## 目标
+
+学会用 Promise 表达异步结果。
+
+## 你要练什么
+
+- Promise
+- `.then()`
+- `.catch()`
+
+## 任务
+
+- 调用模拟请求函数
+- 成功时渲染课程数据
+- 失败时渲染错误状态
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/05-promise-basics/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/05-promise-basics/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/05-promise-basics/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/05-promise-basics/answer.js)
diff --git a/05-es6-plus/05-promise-basics/answer.html b/05-es6-plus/05-promise-basics/answer.html
new file mode 100644
index 0000000..14a7966
--- /dev/null
+++ b/05-es6-plus/05-promise-basics/answer.html
@@ -0,0 +1,13 @@
+
+
+  
+    
+    
+    Promise 基础
+  
+  
+    

等待加载

+

+    
+  
+
diff --git a/05-es6-plus/05-promise-basics/answer.js b/05-es6-plus/05-promise-basics/answer.js
new file mode 100644
index 0000000..9e75840
--- /dev/null
+++ b/05-es6-plus/05-promise-basics/answer.js
@@ -0,0 +1,21 @@
+function loadTracks() {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(["模板字符串", "模块化", "async/await"]);
+    }, 700);
+  });
+}
+
+const status = document.getElementById("status");
+const output = document.getElementById("output");
+
+status.textContent = "加载中...";
+
+loadTracks()
+  .then((tracks) => {
+    status.textContent = "加载完成";
+    output.textContent = tracks.join("\n");
+  })
+  .catch(() => {
+    status.textContent = "加载失败";
+  });
diff --git a/05-es6-plus/05-promise-basics/starter.html b/05-es6-plus/05-promise-basics/starter.html
new file mode 100644
index 0000000..9d38c2f
--- /dev/null
+++ b/05-es6-plus/05-promise-basics/starter.html
@@ -0,0 +1,13 @@
+
+
+  
+    
+    
+    Promise 基础
+  
+  
+    

等待加载

+

+    
+  
+
diff --git a/05-es6-plus/05-promise-basics/starter.js b/05-es6-plus/05-promise-basics/starter.js
new file mode 100644
index 0000000..5f85a80
--- /dev/null
+++ b/05-es6-plus/05-promise-basics/starter.js
@@ -0,0 +1,12 @@
+function loadTracks() {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(["模板字符串", "模块化", "async/await"]);
+    }, 700);
+  });
+}
+
+// 任务:
+// 1. 调用 loadTracks()
+// 2. 成功时更新状态和输出内容
+// 3. 失败时更新状态
diff --git a/05-es6-plus/06-async-await/README.md b/05-es6-plus/06-async-await/README.md
new file mode 100644
index 0000000..c1f4554
--- /dev/null
+++ b/05-es6-plus/06-async-await/README.md
@@ -0,0 +1,24 @@
+# 练习 6:async / await
+
+## 目标
+
+学会用 `async` / `await` 改写一个 Promise 流程。
+
+## 你要练什么
+
+- `async`
+- `await`
+- `try...catch`
+
+## 任务
+
+- 写一个 `async` 函数等待课程配置
+- 成功时渲染结果
+- 失败时更新状态
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/06-async-await/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/06-async-await/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/06-async-await/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/06-async-await/answer.js)
diff --git a/05-es6-plus/06-async-await/answer.html b/05-es6-plus/06-async-await/answer.html
new file mode 100644
index 0000000..5901a63
--- /dev/null
+++ b/05-es6-plus/06-async-await/answer.html
@@ -0,0 +1,13 @@
+
+
+  
+    
+    
+    async / await
+  
+  
+    

等待加载

+

+    
+  
+
diff --git a/05-es6-plus/06-async-await/answer.js b/05-es6-plus/06-async-await/answer.js
new file mode 100644
index 0000000..2b620fa
--- /dev/null
+++ b/05-es6-plus/06-async-await/answer.js
@@ -0,0 +1,27 @@
+function loadConfig() {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve({
+        title: "现代 JS 面板",
+        level: "进阶",
+      });
+    }, 800);
+  });
+}
+
+const status = document.getElementById("status");
+const output = document.getElementById("output");
+
+async function renderConfig() {
+  status.textContent = "加载中...";
+
+  try {
+    const config = await loadConfig();
+    status.textContent = "加载完成";
+    output.textContent = `${config.title} - ${config.level}`;
+  } catch (error) {
+    status.textContent = "加载失败";
+  }
+}
+
+renderConfig();
diff --git a/05-es6-plus/06-async-await/starter.html b/05-es6-plus/06-async-await/starter.html
new file mode 100644
index 0000000..8bad475
--- /dev/null
+++ b/05-es6-plus/06-async-await/starter.html
@@ -0,0 +1,13 @@
+
+
+  
+    
+    
+    async / await
+  
+  
+    

等待加载

+

+    
+  
+
diff --git a/05-es6-plus/06-async-await/starter.js b/05-es6-plus/06-async-await/starter.js
new file mode 100644
index 0000000..3b027f2
--- /dev/null
+++ b/05-es6-plus/06-async-await/starter.js
@@ -0,0 +1,15 @@
+function loadConfig() {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve({
+        title: "现代 JS 面板",
+        level: "进阶",
+      });
+    }, 800);
+  });
+}
+
+// 任务:
+// 1. 写一个 async 函数
+// 2. 用 await 等待 loadConfig()
+// 3. 用 try...catch 处理流程
diff --git a/05-es6-plus/07-arrow-this/README.md b/05-es6-plus/07-arrow-this/README.md
new file mode 100644
index 0000000..7ddb194
--- /dev/null
+++ b/05-es6-plus/07-arrow-this/README.md
@@ -0,0 +1,23 @@
+# 练习 7:箭头函数里的 this
+
+## 目标
+
+理解箭头函数不会创建自己的 `this`。
+
+## 你要练什么
+
+- 箭头函数
+- `this`
+- 定时器回调
+
+## 任务
+
+- 在对象方法里用箭头函数处理 `setTimeout`
+- 让延迟输出仍然拿到当前对象的名称
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/07-arrow-this/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/07-arrow-this/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/07-arrow-this/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/07-arrow-this/answer.js)
diff --git a/05-es6-plus/07-arrow-this/answer.html b/05-es6-plus/07-arrow-this/answer.html
new file mode 100644
index 0000000..cffba6e
--- /dev/null
+++ b/05-es6-plus/07-arrow-this/answer.html
@@ -0,0 +1,12 @@
+
+
+  
+    
+    
+    箭头函数里的 this
+  
+  
+    

+    
+  
+
diff --git a/05-es6-plus/07-arrow-this/answer.js b/05-es6-plus/07-arrow-this/answer.js
new file mode 100644
index 0000000..d37fd7b
--- /dev/null
+++ b/05-es6-plus/07-arrow-this/answer.js
@@ -0,0 +1,10 @@
+const trainer = {
+  name: "现代 JS 训练营",
+  report() {
+    setTimeout(() => {
+      document.getElementById("output").textContent = `当前模块:${this.name}`;
+    }, 500);
+  },
+};
+
+trainer.report();
diff --git a/05-es6-plus/07-arrow-this/starter.html b/05-es6-plus/07-arrow-this/starter.html
new file mode 100644
index 0000000..4d2be44
--- /dev/null
+++ b/05-es6-plus/07-arrow-this/starter.html
@@ -0,0 +1,12 @@
+
+
+  
+    
+    
+    箭头函数里的 this
+  
+  
+    

+    
+  
+
diff --git a/05-es6-plus/07-arrow-this/starter.js b/05-es6-plus/07-arrow-this/starter.js
new file mode 100644
index 0000000..1d60c79
--- /dev/null
+++ b/05-es6-plus/07-arrow-this/starter.js
@@ -0,0 +1,11 @@
+const trainer = {
+  name: "现代 JS 训练营",
+  report() {
+    // 任务:
+    // 1. 用 setTimeout
+    // 2. 在回调里用箭头函数读取 this.name
+    // 3. 把结果写入 #output
+  },
+};
+
+trainer.report();
diff --git a/05-es6-plus/08-fetch-and-json/README.md b/05-es6-plus/08-fetch-and-json/README.md
new file mode 100644
index 0000000..8a326b8
--- /dev/null
+++ b/05-es6-plus/08-fetch-and-json/README.md
@@ -0,0 +1,26 @@
+# 练习 8:fetch 和 JSON
+
+## 目标
+
+补齐 `fetch()` 和 `res.json()` 这一层最常见的现代异步写法。
+
+## 你要练什么
+
+- `fetch()`
+- `await`
+- `res.json()`
+- 接口数据渲染
+
+## 任务
+
+- 点击按钮后发起一次 `fetch`
+- 等待返回的响应对象
+- 调用 `res.json()` 解析 JSON
+- 把课程标题和内容渲染到页面
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/08-fetch-and-json/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/08-fetch-and-json/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/08-fetch-and-json/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/08-fetch-and-json/answer.js)
diff --git a/05-es6-plus/08-fetch-and-json/answer.html b/05-es6-plus/08-fetch-and-json/answer.html
new file mode 100644
index 0000000..a732f51
--- /dev/null
+++ b/05-es6-plus/08-fetch-and-json/answer.html
@@ -0,0 +1,16 @@
+
+
+  
+    
+    
+    fetch 和 JSON
+  
+  
+    

fetch 和 JSON

+ +

等待加载

+

+
+    
+  
+
diff --git a/05-es6-plus/08-fetch-and-json/answer.js b/05-es6-plus/08-fetch-and-json/answer.js
new file mode 100644
index 0000000..e591e84
--- /dev/null
+++ b/05-es6-plus/08-fetch-and-json/answer.js
@@ -0,0 +1,19 @@
+const loadButton = document.getElementById("load-btn");
+const status = document.getElementById("status");
+const output = document.getElementById("output");
+
+async function loadPost() {
+  status.textContent = "加载中...";
+
+  try {
+    const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
+    const data = await res.json();
+
+    status.textContent = "加载完成";
+    output.textContent = `标题:${data.title}\n\n内容:${data.body}`;
+  } catch (error) {
+    status.textContent = "加载失败";
+  }
+}
+
+loadButton.addEventListener("click", loadPost);
diff --git a/05-es6-plus/08-fetch-and-json/starter.html b/05-es6-plus/08-fetch-and-json/starter.html
new file mode 100644
index 0000000..032046a
--- /dev/null
+++ b/05-es6-plus/08-fetch-and-json/starter.html
@@ -0,0 +1,16 @@
+
+
+  
+    
+    
+    fetch 和 JSON
+  
+  
+    

fetch 和 JSON

+ +

等待加载

+

+
+    
+  
+
diff --git a/05-es6-plus/08-fetch-and-json/starter.js b/05-es6-plus/08-fetch-and-json/starter.js
new file mode 100644
index 0000000..3a22d55
--- /dev/null
+++ b/05-es6-plus/08-fetch-and-json/starter.js
@@ -0,0 +1,11 @@
+const loadButton = document.getElementById("load-btn");
+const status = document.getElementById("status");
+const output = document.getElementById("output");
+
+// 任务:
+// 1. 点击按钮后把状态改成“加载中...”
+// 2. 用 fetch 请求 https://jsonplaceholder.typicode.com/posts/1
+// 3. 用 await 等待响应对象
+// 4. 调用 res.json() 解析数据
+// 5. 把 title 和 body 渲染到页面
+// 6. 失败时显示“加载失败”
diff --git a/05-es6-plus/09-final-modern-js/README.md b/05-es6-plus/09-final-modern-js/README.md
new file mode 100644
index 0000000..76fb986
--- /dev/null
+++ b/05-es6-plus/09-final-modern-js/README.md
@@ -0,0 +1,28 @@
+# 练习 9:综合页面
+
+## 目标
+
+把 ES6+ 的关键语法拼起来,做一个现代 JS 小页面。
+
+## 项目名称
+
+现代 JS 学习摘要页
+
+## 任务
+
+请完成一个小页面,要求至少包含:
+
+- 模块导入
+- 解构
+- 展开运算符
+- 模板字符串
+- 箭头函数
+- Promise 或 `async` / `await`
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/starter.js)
+- [summary-service.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/summary-service.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/answer.js)
diff --git a/05-es6-plus/09-final-modern-js/answer.html b/05-es6-plus/09-final-modern-js/answer.html
new file mode 100644
index 0000000..11bdebb
--- /dev/null
+++ b/05-es6-plus/09-final-modern-js/answer.html
@@ -0,0 +1,15 @@
+
+
+  
+    
+    
+    现代 JS 学习摘要页
+  
+  
+    

等待加载

+

等待渲染

+ + + + + diff --git a/05-es6-plus/09-final-modern-js/answer.js b/05-es6-plus/09-final-modern-js/answer.js new file mode 100644 index 0000000..b92015b --- /dev/null +++ b/05-es6-plus/09-final-modern-js/answer.js @@ -0,0 +1,22 @@ +import { baseSummary, loadExtraSkills } from "./summary-service.js"; + +const title = document.getElementById("title"); +const intro = document.getElementById("intro"); +const skillList = document.getElementById("skill-list"); + +async function renderPage() { + const extraSkills = await loadExtraSkills(); + const { name, stage, skills } = baseSummary; + const allSkills = [...skills, ...extraSkills]; + + title.textContent = `${name} 的 ${stage} 学习摘要`; + intro.textContent = `当前已覆盖 ${allSkills.length} 个现代 JS 关键点。`; + + allSkills.forEach((skill) => { + const item = document.createElement("li"); + item.textContent = `已掌握:${skill}`; + skillList.appendChild(item); + }); +} + +renderPage(); diff --git a/05-es6-plus/09-final-modern-js/starter.html b/05-es6-plus/09-final-modern-js/starter.html new file mode 100644 index 0000000..6a94ed0 --- /dev/null +++ b/05-es6-plus/09-final-modern-js/starter.html @@ -0,0 +1,15 @@ + + + + + + 现代 JS 学习摘要页 + + +

等待加载

+

等待渲染

+ + + + + diff --git a/05-es6-plus/09-final-modern-js/starter.js b/05-es6-plus/09-final-modern-js/starter.js new file mode 100644 index 0000000..d99d101 --- /dev/null +++ b/05-es6-plus/09-final-modern-js/starter.js @@ -0,0 +1,7 @@ +// 任务: +// 1. 从 ./summary-service.js 导入数据 +// 2. 用 async / await 获取额外技能 +// 3. 解构 name、stage、skills +// 4. 用展开运算符合并技能 +// 5. 用模板字符串渲染标题和说明 +// 6. 用 forEach 或 map + 箭头函数渲染列表 diff --git a/05-es6-plus/09-final-modern-js/summary-service.js b/05-es6-plus/09-final-modern-js/summary-service.js new file mode 100644 index 0000000..8765d41 --- /dev/null +++ b/05-es6-plus/09-final-modern-js/summary-service.js @@ -0,0 +1,12 @@ +export const baseSummary = { + name: "林晨", + stage: "ES6+", + skills: ["模板字符串", "解构", "展开运算符"], +}; + +export const loadExtraSkills = () => + new Promise((resolve) => { + setTimeout(() => { + resolve(["模块化", "Promise", "async/await"]); + }, 700); + }); diff --git a/05-es6-plus/README.md b/05-es6-plus/README.md new file mode 100644 index 0000000..dc476af --- /dev/null +++ b/05-es6-plus/README.md @@ -0,0 +1,151 @@ +# ES6+(现代 JS) + +这部分只解决一个问题:你能不能用现代 JavaScript 的语法和模块能力,把代码写得更清晰、更工程化。 + +## 学完后你应该掌握 + +- 模板字符串 +- 对象和数组解构 +- 展开运算符和剩余参数 +- 箭头函数的基础写法 +- 箭头函数和 `this` 的常见差异 +- `import` / `export` 的基础用法 +- `fetch()` 和 `res.json()` 的基础用法 +- Promise 的基础链式写法 +- `async` / `await` 的基础写法 +- 如何把这些能力组合成一个现代 JS 小页面 + +## 这一章在解决什么 + +JavaScript 本体解决的是逻辑表达。 + +ES6+ 这一章解决的是: + +- 代码怎么写得更短、更清楚 +- 多文件之间怎么建立依赖 +- 异步代码怎么写得更自然 + +它回答的是: + +- 对象里的值怎么快速取出来 +- 数组和对象怎么复制或合并 +- 回调函数怎么写得更紧凑 +- 多个 JS 文件怎么互相导入 +- Promise 和 `async` / `await` 怎么替代层层回调 + +## 必须建立的 5 个核心意识 + +### 1. ES6+ 不是“炫技语法”,而是为了减少样板代码 + +比如: + +- 模板字符串比字符串拼接更直观 +- 解构让取值更直接 +- 展开运算符让复制和合并更清楚 + +### 2. 语法糖不改变逻辑本质 + +例如: + +- 箭头函数还是函数 +- `async` / `await` 本质上仍然建立在 Promise 上 + +### 3. 模块化是现代前端代码的基础 + +一个文件只做一件事,然后通过: + +- `export` +- `import` + +把能力组合起来。 + +### 4. Promise 和 `async` / `await` 是为了解决异步可读性 + +不是让异步消失,而是让异步流程更好读、更好维护。 + +### 5. 优先理解“什么时候用”,再记语法 + +比如: + +- 需要字符串插值时用模板字符串 +- 需要快速取对象字段时用解构 +- 需要复制对象和数组时用展开运算符 +- 需要跨文件协作时用模块 + +## 高频语法速记 + +### 模板字符串 + +- `` `Hello ${name}` `` + +### 解构 + +- `const { name } = user` +- `const [first, second] = list` + +### 展开和剩余 + +- `const newArr = [...arr]` +- `const newUser = { ...user, age: 21 }` +- `function sum(...numbers) {}` + +### 箭头函数 + +- `const add = (a, b) => a + b` + +### 模块 + +- `export` +- `import` + +### 异步 + +- `Promise` +- `.then()` +- `async` +- `await` + +## 学习顺序 + +1. 模板字符串和解构 +2. 展开运算符和剩余参数 +3. 箭头函数 +4. 模块基础 +5. Promise +6. `async` / `await` +7. 箭头函数里的 `this` +8. `fetch` 和 JSON +9. 综合练习 + +## 练习目录 + +- [01-template-and-destructuring/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/01-template-and-destructuring/README.md) +- [02-spread-and-rest/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/02-spread-and-rest/README.md) +- [03-arrow-functions/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/03-arrow-functions/README.md) +- [04-modules-basic/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/README.md) +- [05-promise-basics/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/05-promise-basics/README.md) +- [06-async-await/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/06-async-await/README.md) +- [07-arrow-this/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/07-arrow-this/README.md) +- [08-fetch-and-json/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/08-fetch-and-json/README.md) +- [09-final-modern-js/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/README.md) + +## 过关标准 + +如果你能独立做到下面这些,就说明这一章已经基本过关: + +- 能用模板字符串代替拼接 +- 能用解构快速读取对象和数组数据 +- 能用展开运算符合并对象、复制数组 +- 能写简单的箭头函数 +- 能理解箭头函数的 `this` 和普通函数的差异 +- 能写出基础的 `import` / `export` +- 能读懂 `fetch()` 返回值,并通过 `res.json()` 取到数据 +- 能用 Promise 和 `async` / `await` 写一个简单异步流程 +- 能把这些语法组合进一个小页面 + +## 学习建议 + +- 先把语法写顺,再去追求更短 +- 一次只替换一类旧写法,避免混乱 +- 学模块时一定要看清“谁导出、谁导入” +- 学异步时先看执行顺序,再看语法形式 diff --git a/06-typescript/01-js-vs-ts/README.md b/06-typescript/01-js-vs-ts/README.md new file mode 100644 index 0000000..44eeaa7 --- /dev/null +++ b/06-typescript/01-js-vs-ts/README.md @@ -0,0 +1,21 @@ +# 练习 1:JavaScript 和 TypeScript 的差别 + +## 目标 + +理解 TypeScript 为什么能在写代码时提前发现类型问题。 + +## 你要练什么 + +- 参数类型 +- 返回值类型 +- 类型报错的意义 + +## 任务 + +- 观察 `add` 函数的类型标注 +- 看懂为什么 `add(1, "2")` 会在 TypeScript 里报错 + +## 文件 + +- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/01-js-vs-ts/starter.ts) +- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/01-js-vs-ts/answer.ts) diff --git a/06-typescript/01-js-vs-ts/answer.ts b/06-typescript/01-js-vs-ts/answer.ts new file mode 100644 index 0000000..050b4e3 --- /dev/null +++ b/06-typescript/01-js-vs-ts/answer.ts @@ -0,0 +1,10 @@ +function add(a: number, b: number): number { + return a + b; +} + +const result = add(1, 2); +console.log(result); + +// const wrongResult = add(1, "2"); +// TypeScript 会在写代码阶段直接提示: +// 第二个参数应该是 number,但这里传入了 string。 diff --git a/06-typescript/01-js-vs-ts/starter.ts b/06-typescript/01-js-vs-ts/starter.ts new file mode 100644 index 0000000..7f7a1a4 --- /dev/null +++ b/06-typescript/01-js-vs-ts/starter.ts @@ -0,0 +1,11 @@ +function add(a: number, b: number): number { + return a + b; +} + +const result = add(1, 2); +console.log(result); + +// 任务: +// 1. 观察 add 的参数和返回值类型 +// 2. 尝试把下面这行取消注释,看看 TypeScript 为什么会报错 +// const wrongResult = add(1, "2"); diff --git a/06-typescript/02-basic-type-annotations/README.md b/06-typescript/02-basic-type-annotations/README.md new file mode 100644 index 0000000..64de115 --- /dev/null +++ b/06-typescript/02-basic-type-annotations/README.md @@ -0,0 +1,21 @@ +# 练习 2:基本类型标注 + +## 目标 + +学会给最常见的值加上类型标注。 + +## 你要练什么 + +- `number` +- `string` +- `boolean` + +## 任务 + +- 给年龄、姓名、付费状态加类型 +- 再输出一段清晰的学习信息 + +## 文件 + +- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/02-basic-type-annotations/starter.ts) +- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/02-basic-type-annotations/answer.ts) diff --git a/06-typescript/02-basic-type-annotations/answer.ts b/06-typescript/02-basic-type-annotations/answer.ts new file mode 100644 index 0000000..b200bab --- /dev/null +++ b/06-typescript/02-basic-type-annotations/answer.ts @@ -0,0 +1,5 @@ +let age: number = 21; +let userName: string = "林晨"; +let isPaid: boolean = true; + +console.log(`${userName} 今年 ${age} 岁,课程付费状态:${isPaid}`); diff --git a/06-typescript/02-basic-type-annotations/starter.ts b/06-typescript/02-basic-type-annotations/starter.ts new file mode 100644 index 0000000..2267730 --- /dev/null +++ b/06-typescript/02-basic-type-annotations/starter.ts @@ -0,0 +1,7 @@ +let age: number = 18; +let userName: string = "Tom"; +let isPaid: boolean = true; + +// 任务: +// 1. 修改上面的值,让它们更像一个学习者资料 +// 2. 输出一句完整信息 diff --git a/06-typescript/03-array-and-function-types/README.md b/06-typescript/03-array-and-function-types/README.md new file mode 100644 index 0000000..3c32e37 --- /dev/null +++ b/06-typescript/03-array-and-function-types/README.md @@ -0,0 +1,22 @@ +# 练习 3:数组和函数类型 + +## 目标 + +学会给数组和函数写类型。 + +## 你要练什么 + +- `number[]` +- 参数类型 +- 返回值类型 + +## 任务 + +- 定义一个数字数组 +- 写一个 `sum` 函数 +- 让它接收两个数字并返回数字结果 + +## 文件 + +- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/03-array-and-function-types/starter.ts) +- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/03-array-and-function-types/answer.ts) diff --git a/06-typescript/03-array-and-function-types/answer.ts b/06-typescript/03-array-and-function-types/answer.ts new file mode 100644 index 0000000..979ec0b --- /dev/null +++ b/06-typescript/03-array-and-function-types/answer.ts @@ -0,0 +1,10 @@ +const scoreList: number[] = [82, 90, 95]; + +function sum(a: number, b: number): number { + return a + b; +} + +const total = sum(88, 12); + +console.log(scoreList); +console.log(total); diff --git a/06-typescript/03-array-and-function-types/starter.ts b/06-typescript/03-array-and-function-types/starter.ts new file mode 100644 index 0000000..f17d2a9 --- /dev/null +++ b/06-typescript/03-array-and-function-types/starter.ts @@ -0,0 +1,11 @@ +const scoreList: number[] = [82, 90, 95]; + +function sum(a: number, b: number): number { + // 返回两个数字之和 + return 0; +} + +// 任务: +// 1. 改写 sum 的返回值 +// 2. 调用 sum +// 3. 输出 scoreList 和 sum 结果 diff --git a/06-typescript/04-interface-object-shape/README.md b/06-typescript/04-interface-object-shape/README.md new file mode 100644 index 0000000..2bdf4ed --- /dev/null +++ b/06-typescript/04-interface-object-shape/README.md @@ -0,0 +1,22 @@ +# 练习 4:interface 描述对象结构 + +## 目标 + +学会用 `interface` 给对象建立结构约束。 + +## 你要练什么 + +- `interface` +- 对象结构 +- 类型约束 + +## 任务 + +- 写一个 `User` 接口 +- 用它约束一个学习者对象 +- 输出对象中的关键信息 + +## 文件 + +- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/04-interface-object-shape/starter.ts) +- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/04-interface-object-shape/answer.ts) diff --git a/06-typescript/04-interface-object-shape/answer.ts b/06-typescript/04-interface-object-shape/answer.ts new file mode 100644 index 0000000..1a2ade1 --- /dev/null +++ b/06-typescript/04-interface-object-shape/answer.ts @@ -0,0 +1,14 @@ +interface User { + id: number; + name: string; + age: number; +} + +const user: User = { + id: 1, + name: "林晨", + age: 21, +}; + +console.log(user.name); +console.log(user.age); diff --git a/06-typescript/04-interface-object-shape/starter.ts b/06-typescript/04-interface-object-shape/starter.ts new file mode 100644 index 0000000..fa5177d --- /dev/null +++ b/06-typescript/04-interface-object-shape/starter.ts @@ -0,0 +1,15 @@ +interface User { + id: number; + name: string; + age: number; +} + +const user: User = { + id: 1, + name: "Tom", + age: 20, +}; + +// 任务: +// 1. 把对象内容改成学习者资料 +// 2. 输出 name 和 age diff --git a/06-typescript/05-generic-functions/README.md b/06-typescript/05-generic-functions/README.md new file mode 100644 index 0000000..c49fdd8 --- /dev/null +++ b/06-typescript/05-generic-functions/README.md @@ -0,0 +1,21 @@ +# 练习 5:泛型函数 + +## 目标 + +理解泛型是在保持输入输出类型一致。 + +## 你要练什么 + +- 泛型 `` +- 输入输出同类型 + +## 任务 + +- 写一个 `getData` 泛型函数 +- 传入什么类型,就返回什么类型 +- 分别用数字和字符串调用 + +## 文件 + +- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/05-generic-functions/starter.ts) +- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/05-generic-functions/answer.ts) diff --git a/06-typescript/05-generic-functions/answer.ts b/06-typescript/05-generic-functions/answer.ts new file mode 100644 index 0000000..760cc0a --- /dev/null +++ b/06-typescript/05-generic-functions/answer.ts @@ -0,0 +1,9 @@ +function getData(data: T): T { + return data; +} + +const numberResult = getData(1); +const stringResult = getData("abc"); + +console.log(numberResult); +console.log(stringResult); diff --git a/06-typescript/05-generic-functions/starter.ts b/06-typescript/05-generic-functions/starter.ts new file mode 100644 index 0000000..461ae75 --- /dev/null +++ b/06-typescript/05-generic-functions/starter.ts @@ -0,0 +1,9 @@ +function getData(data: T): T { + // 返回 data + return data; +} + +// 任务: +// 1. 用 number 调用 +// 2. 用 string 调用 +// 3. 输出结果 diff --git a/06-typescript/06-union-and-optional-props/README.md b/06-typescript/06-union-and-optional-props/README.md new file mode 100644 index 0000000..e9eeca8 --- /dev/null +++ b/06-typescript/06-union-and-optional-props/README.md @@ -0,0 +1,22 @@ +# 练习 6:联合类型和可选属性 + +## 目标 + +学会处理不固定的数据结构。 + +## 你要练什么 + +- 联合类型 +- 可选属性 +- 接口扩展场景 + +## 任务 + +- 定义一个带可选年龄的 `User` 接口 +- 声明一个 `id`,它可以是数字或字符串 +- 分别创建有年龄和没有年龄的对象 + +## 文件 + +- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/06-union-and-optional-props/starter.ts) +- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/06-union-and-optional-props/answer.ts) diff --git a/06-typescript/06-union-and-optional-props/answer.ts b/06-typescript/06-union-and-optional-props/answer.ts new file mode 100644 index 0000000..b391c7c --- /dev/null +++ b/06-typescript/06-union-and-optional-props/answer.ts @@ -0,0 +1,19 @@ +let id: number | string = "user-1"; + +interface User { + name: string; + age?: number; +} + +const userA: User = { + name: "林晨", + age: 21, +}; + +const userB: User = { + name: "小周", +}; + +console.log(id); +console.log(userA); +console.log(userB); diff --git a/06-typescript/06-union-and-optional-props/starter.ts b/06-typescript/06-union-and-optional-props/starter.ts new file mode 100644 index 0000000..e88ff3b --- /dev/null +++ b/06-typescript/06-union-and-optional-props/starter.ts @@ -0,0 +1,12 @@ +let id: number | string = 1; + +interface User { + name: string; + age?: number; +} + +// 任务: +// 1. 把 id 改成字符串也试一次 +// 2. 创建一个带 age 的 userA +// 3. 创建一个不带 age 的 userB +// 4. 输出它们 diff --git a/06-typescript/07-type-safe-renderer/README.md b/06-typescript/07-type-safe-renderer/README.md new file mode 100644 index 0000000..0e12046 --- /dev/null +++ b/06-typescript/07-type-safe-renderer/README.md @@ -0,0 +1,22 @@ +# 练习 7:类型安全渲染 + +## 目标 + +把接口、数组和函数类型组合起来,处理一组结构化数据。 + +## 你要练什么 + +- `interface` +- 数组类型 +- 函数返回值类型 + +## 任务 + +- 定义课程接口 +- 创建课程数组 +- 写一个渲染函数,返回字符串数组 + +## 文件 + +- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/07-type-safe-renderer/starter.ts) +- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/07-type-safe-renderer/answer.ts) diff --git a/06-typescript/07-type-safe-renderer/answer.ts b/06-typescript/07-type-safe-renderer/answer.ts new file mode 100644 index 0000000..8aff99e --- /dev/null +++ b/06-typescript/07-type-safe-renderer/answer.ts @@ -0,0 +1,19 @@ +interface Course { + title: string; + lessons: number; + finished: boolean; +} + +const courses: Course[] = [ + { title: "TypeScript 基础", lessons: 8, finished: true }, + { title: "接口和泛型", lessons: 6, finished: false }, +]; + +function renderCourseLines(list: Course[]): string[] { + return list.map((course) => { + const status = course.finished ? "已完成" : "学习中"; + return `${course.title} - ${course.lessons} 节 - ${status}`; + }); +} + +console.log(renderCourseLines(courses)); diff --git a/06-typescript/07-type-safe-renderer/starter.ts b/06-typescript/07-type-safe-renderer/starter.ts new file mode 100644 index 0000000..cae82dc --- /dev/null +++ b/06-typescript/07-type-safe-renderer/starter.ts @@ -0,0 +1,19 @@ +interface Course { + title: string; + lessons: number; + finished: boolean; +} + +const courses: Course[] = [ + { title: "TypeScript 基础", lessons: 8, finished: true }, + { title: "接口和泛型", lessons: 6, finished: false }, +]; + +function renderCourseLines(list: Course[]): string[] { + // 返回渲染后的字符串数组 + return []; +} + +// 任务: +// 1. 实现 renderCourseLines +// 2. 输出结果 diff --git a/06-typescript/08-final-mini-app/README.md b/06-typescript/08-final-mini-app/README.md new file mode 100644 index 0000000..8f2559e --- /dev/null +++ b/06-typescript/08-final-mini-app/README.md @@ -0,0 +1,24 @@ +# 练习 8:综合小练习 + +## 目标 + +把基础类型、接口、泛型和联合类型组合起来,完成一个小型 TypeScript 数据模型。 + +## 项目名称 + +类型安全学习摘要 + +## 任务 + +请完成一个小程序,要求至少包含: + +- 一个 `Student` 接口 +- 一个课程数组类型 +- 一个联合类型字段 +- 一个泛型函数 +- 一个格式化输出函数 + +## 文件 + +- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/08-final-mini-app/starter.ts) +- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/08-final-mini-app/answer.ts) diff --git a/06-typescript/08-final-mini-app/answer.ts b/06-typescript/08-final-mini-app/answer.ts new file mode 100644 index 0000000..00acd85 --- /dev/null +++ b/06-typescript/08-final-mini-app/answer.ts @@ -0,0 +1,36 @@ +type StudentId = number | string; + +interface Course { + title: string; + finished: boolean; +} + +interface Student { + id: StudentId; + name: string; + age?: number; + courses: Course[]; +} + +function pickFirst(list: T[]): T { + return list[0]; +} + +function formatStudent(student: Student): string { + const courseCount = student.courses.length; + return `${student.name} 当前有 ${courseCount} 门课程,编号是 ${student.id}`; +} + +const student: Student = { + id: "stu-1", + name: "林晨", + courses: [ + { title: "基本类型", finished: true }, + { title: "接口", finished: false }, + ], +}; + +const firstCourse = pickFirst(student.courses); + +console.log(formatStudent(student)); +console.log(firstCourse); diff --git a/06-typescript/08-final-mini-app/starter.ts b/06-typescript/08-final-mini-app/starter.ts new file mode 100644 index 0000000..1216ff1 --- /dev/null +++ b/06-typescript/08-final-mini-app/starter.ts @@ -0,0 +1,36 @@ +type StudentId = number | string; + +interface Course { + title: string; + finished: boolean; +} + +interface Student { + id: StudentId; + name: string; + age?: number; + courses: Course[]; +} + +function pickFirst(list: T[]): T { + return list[0]; +} + +function formatStudent(student: Student): string { + // 返回一段摘要文字 + return ""; +} + +const student: Student = { + id: "stu-1", + name: "林晨", + courses: [ + { title: "基本类型", finished: true }, + { title: "接口", finished: false }, + ], +}; + +// 任务: +// 1. 实现 formatStudent +// 2. 用 pickFirst 取第一门课程 +// 3. 输出摘要和第一门课程 diff --git a/06-typescript/README.md b/06-typescript/README.md new file mode 100644 index 0000000..5af8792 --- /dev/null +++ b/06-typescript/README.md @@ -0,0 +1,170 @@ +# TypeScript(类型系统) + +这部分只解决一个问题:你能不能在写代码阶段就让错误尽量提前暴露,而不是把问题留到运行时。 + +## 学完后你应该掌握 + +- TypeScript 和 JavaScript 的差别 +- 基本类型标注 +- 数组类型和函数参数/返回值类型 +- `interface` 描述对象结构 +- 泛型函数的基本写法 +- 联合类型和可选属性 +- 如何用类型约束一组业务数据 +- 如何把这些类型能力组合成一个小程序 + +## TypeScript 是什么 + +TypeScript 不是让程序“更能运行”,而是让程序“更不容易写错”。 + +它回答的是: + +- 这个值应该是什么类型 +- 这个函数该接收什么参数 +- 这个对象应该有哪些字段 +- 一组数据之间的结构是否一致 + +如果 JavaScript 更关注“能不能表达逻辑”,那么 TypeScript 更关注“表达出来的逻辑有没有类型保障”。 + +## 必须建立的 5 个核心意识 + +### 1. 类型信息是写给人和工具看的约束 + +```ts +function add(a: number, b: number): number { + return a + b; +} +``` + +这段代码的重点不是语法更复杂,而是把“这个函数只能接收数字”说清楚了。 + +### 2. TypeScript 的价值主要发生在运行前 + +TypeScript 很多时候不是修复 bug,而是阻止 bug 被写进去。 + +### 3. 对象结构要先约定,再使用 + +当数据开始变复杂时,优先考虑用 `interface` 或类型别名描述结构。 + +### 4. 泛型是在复用“结构能力” + +泛型不是高难技巧,它本质上是在说: + +- 这个函数可以处理很多类型 +- 但输入和输出之间的类型关系要保持一致 + +### 5. 类型要服务于业务,不要为了类型而类型 + +目标不是把代码写得最花,而是让代码更清晰、更稳。 + +## 高频概念速记 + +### 基本类型 + +- `number` +- `string` +- `boolean` + +### 容器类型 + +- `number[]` +- `string[]` +- `Array` + +### 函数类型 + +- 参数类型 +- 返回值类型 + +### 对象结构 + +- `interface` + +### 进阶类型 + +- 泛型 `` +- 联合类型 `|` +- 可选属性 `?` + +## 学习顺序 + +1. JavaScript 和 TypeScript 的差别 +2. 基本类型标注 +3. 数组和函数类型 +4. `interface` +5. 泛型 +6. 联合类型和可选属性 +7. 类型安全渲染 +8. 综合小练习 + +## 练习目录 + +- [01-js-vs-ts/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/01-js-vs-ts/README.md) +- [02-basic-type-annotations/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/02-basic-type-annotations/README.md) +- [03-array-and-function-types/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/03-array-and-function-types/README.md) +- [04-interface-object-shape/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/04-interface-object-shape/README.md) +- [05-generic-functions/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/05-generic-functions/README.md) +- [06-union-and-optional-props/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/06-union-and-optional-props/README.md) +- [07-type-safe-renderer/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/07-type-safe-renderer/README.md) +- [08-final-mini-app/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/08-final-mini-app/README.md) + +## 过关标准 + +如果你能独立做到下面这些,就说明这一章已经基本过关: + +- 能给基本值加类型标注 +- 能给数组和函数写正确类型 +- 能用 `interface` 描述对象结构 +- 能写一个简单泛型函数 +- 能理解联合类型和可选属性的使用场景 +- 能看懂 TypeScript 在写代码阶段提示的问题 +- 能把类型约束用到一个小型数据模型里 + +## 学习建议 + +- 先保证类型写对,再考虑写得更少 +- 报错时先看“期望类型”和“实际类型”分别是什么 +- 不确定对象结构时,先写接口再写数据 +- 遇到泛型不要先背定义,先看输入输出是不是保持同类关系 + +## 运行调试 + +这一章现在已经做成了 `Vite + TypeScript` 工程。 + +它保留了原来的 8 个练习目录,同时新增了一个统一的学习面板,你可以在浏览器里切换章节、查看 `starter.ts`、查看 `answer.ts`,并直接看到示例输出。 + +### 启动方式 + +在 [06-typescript](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript) 目录执行: + +```bash +npm install +npm run dev +``` + +### 常用命令 + +```bash +npm run dev +npm run build +npm run preview +npm run check +npm run check:app +npm run check:lessons +``` + +### 工程入口 + +- 入口页面:[index.html](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/index.html) +- 应用入口:[src/main.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/src/main.ts) +- 章节数据:[src/lessons.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/src/lessons.ts) +- 页面样式:[src/style.css](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/src/style.css) +- 包管理配置:[package.json](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/package.json) +- TypeScript 配置:[tsconfig.json](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/tsconfig.json) + +### 说明 + +- `npm run check` 会先检查 `src/` 下的 Vite 入口代码,再逐个检查每个练习目录里的 `starter.ts` 和 `answer.ts` +- `npm run check:app` 只检查 Vite 应用入口 +- `npm run check:lessons` 会按文件逐个检查 8 组练习,避免不同练习之间的全局变量互相污染 +- 原来的练习目录仍然保留,继续作为题目和答案素材使用 diff --git a/06-typescript/index.html b/06-typescript/index.html new file mode 100644 index 0000000..ad9e758 --- /dev/null +++ b/06-typescript/index.html @@ -0,0 +1,15 @@ + + + + + + TypeScript Learning Lab + + +
+ + + diff --git a/06-typescript/package-lock.json b/06-typescript/package-lock.json new file mode 100644 index 0000000..131a095 --- /dev/null +++ b/06-typescript/package-lock.json @@ -0,0 +1,913 @@ +{ + "name": "06-typescript", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "06-typescript", + "version": "1.0.0", + "dependencies": { + "pnpm": "^10.32.1" + }, + "devDependencies": { + "typescript": "latest", + "vite": "latest" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pnpm": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-10.32.1.tgz", + "integrity": "sha512-pwaTjw6JrBRWtlY+q07fHR+vM2jRGR/FxZeQ6W3JGORFarLmfWE94QQ9LoyB+HMD5rQNT/7KnfFe8a1Wc0jyvg==", + "license": "MIT", + "bin": { + "pnpm": "bin/pnpm.cjs", + "pnpx": "bin/pnpx.cjs" + }, + "engines": { + "node": ">=18.12" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/06-typescript/package.json b/06-typescript/package.json new file mode 100644 index 0000000..1c11691 --- /dev/null +++ b/06-typescript/package.json @@ -0,0 +1,21 @@ +{ + "name": "06-typescript", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "tsc --noEmit -p ./tsconfig.json && node ./scripts/check-lessons.mjs", + "check:app": "tsc --noEmit -p ./tsconfig.json", + "check:lessons": "node ./scripts/check-lessons.mjs" + }, + "devDependencies": { + "typescript": "latest", + "vite": "latest" + }, + "dependencies": { + "pnpm": "^10.32.1" + } +} diff --git a/06-typescript/pnpm-lock.yaml b/06-typescript/pnpm-lock.yaml new file mode 100644 index 0000000..7ac9d3a --- /dev/null +++ b/06-typescript/pnpm-lock.yaml @@ -0,0 +1,518 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + pnpm: + specifier: ^10.32.1 + version: 10.32.1 + devDependencies: + typescript: + specifier: latest + version: 5.9.3 + vite: + specifier: latest + version: 8.0.0 + +packages: + + '@emnapi/core@1.9.0': + resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} + + '@emnapi/runtime@1.9.0': + resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@oxc-project/runtime@0.115.0': + resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.115.0': + resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + + '@rolldown/binding-android-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.9': + resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.9': + resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pnpm@10.32.1: + resolution: {integrity: sha512-pwaTjw6JrBRWtlY+q07fHR+vM2jRGR/FxZeQ6W3JGORFarLmfWE94QQ9LoyB+HMD5rQNT/7KnfFe8a1Wc0jyvg==} + engines: {node: '>=18.12'} + hasBin: true + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + rolldown@1.0.0-rc.9: + resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + vite@8.0.0: + resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.0.0-alpha.31 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + +snapshots: + + '@emnapi/core@1.9.0': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.9.0 + '@emnapi/runtime': 1.9.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/runtime@0.115.0': {} + + '@oxc-project/types@0.115.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.9': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + detect-libc@2.1.2: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + nanoid@3.3.11: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pnpm@10.32.1: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rolldown@1.0.0-rc.9: + dependencies: + '@oxc-project/types': 0.115.0 + '@rolldown/pluginutils': 1.0.0-rc.9 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-x64': 1.0.0-rc.9 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 + + source-map-js@1.2.1: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + vite@8.0.0: + dependencies: + '@oxc-project/runtime': 0.115.0 + lightningcss: 1.32.0 + picomatch: 4.0.3 + postcss: 8.5.8 + rolldown: 1.0.0-rc.9 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 diff --git a/06-typescript/scripts/check-lessons.mjs b/06-typescript/scripts/check-lessons.mjs new file mode 100644 index 0000000..c1e06a0 --- /dev/null +++ b/06-typescript/scripts/check-lessons.mjs @@ -0,0 +1,58 @@ +import { spawnSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootDir = path.resolve(__dirname, ".."); +const tscPath = path.join(rootDir, "node_modules", "typescript", "bin", "tsc"); + +const lessonFiles = [ + "01-js-vs-ts/starter.ts", + "01-js-vs-ts/answer.ts", + "02-basic-type-annotations/starter.ts", + "02-basic-type-annotations/answer.ts", + "03-array-and-function-types/starter.ts", + "03-array-and-function-types/answer.ts", + "04-interface-object-shape/starter.ts", + "04-interface-object-shape/answer.ts", + "05-generic-functions/starter.ts", + "05-generic-functions/answer.ts", + "06-union-and-optional-props/starter.ts", + "06-union-and-optional-props/answer.ts", + "07-type-safe-renderer/starter.ts", + "07-type-safe-renderer/answer.ts", + "08-final-mini-app/starter.ts", + "08-final-mini-app/answer.ts", +]; + +const sharedArgs = [ + "--noEmit", + "--pretty", + "false", + "--target", + "ES2020", + "--module", + "ESNext", + "--moduleResolution", + "Bundler", + "--strict", + "--skipLibCheck", +]; + +for (const lessonFile of lessonFiles) { + const filePath = path.join(rootDir, lessonFile); + + console.log(`Checking ${lessonFile}`); + + const result = spawnSync(process.execPath, [tscPath, ...sharedArgs, filePath], { + cwd: rootDir, + stdio: "inherit", + }); + + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +console.log("All lesson files passed TypeScript checks."); diff --git a/06-typescript/src/lessons.ts b/06-typescript/src/lessons.ts new file mode 100644 index 0000000..450bfa9 --- /dev/null +++ b/06-typescript/src/lessons.ts @@ -0,0 +1,308 @@ +import lesson01Starter from "../01-js-vs-ts/starter.ts?raw"; +import lesson01Answer from "../01-js-vs-ts/answer.ts?raw"; +import lesson02Starter from "../02-basic-type-annotations/starter.ts?raw"; +import lesson02Answer from "../02-basic-type-annotations/answer.ts?raw"; +import lesson03Starter from "../03-array-and-function-types/starter.ts?raw"; +import lesson03Answer from "../03-array-and-function-types/answer.ts?raw"; +import lesson04Starter from "../04-interface-object-shape/starter.ts?raw"; +import lesson04Answer from "../04-interface-object-shape/answer.ts?raw"; +import lesson05Starter from "../05-generic-functions/starter.ts?raw"; +import lesson05Answer from "../05-generic-functions/answer.ts?raw"; +import lesson06Starter from "../06-union-and-optional-props/starter.ts?raw"; +import lesson06Answer from "../06-union-and-optional-props/answer.ts?raw"; +import lesson07Starter from "../07-type-safe-renderer/starter.ts?raw"; +import lesson07Answer from "../07-type-safe-renderer/answer.ts?raw"; +import lesson08Starter from "../08-final-mini-app/starter.ts?raw"; +import lesson08Answer from "../08-final-mini-app/answer.ts?raw"; + +export interface Lesson { + id: string; + title: string; + focus: string; + summary: string; + starterCode: string; + answerCode: string; + keyPoints: string[]; + runDemo: () => string[]; +} + +function runLesson01(): string[] { + function add(a: number, b: number): number { + return a + b; + } + + const result = add(1, 2); + + return [ + `add(1, 2) => ${result}`, + '如果写成 add(1, "2"),TypeScript 会在编码阶段报错。', + ]; +} + +function runLesson02(): string[] { + const age: number = 25; + const userName: string = "李青"; + const isLearning: boolean = true; + + return [ + `age 的类型是 number,值为 ${age}`, + `userName 的类型是 string,值为 ${userName}`, + `isLearning 的类型是 boolean,值为 ${isLearning}`, + ]; +} + +function runLesson03(): string[] { + const scoreList: number[] = [82, 90, 95]; + + function sum(a: number, b: number): number { + return a + b; + } + + const total = sum(88, 12); + + return [ + `scoreList => [${scoreList.join(", ")}]`, + `sum(88, 12) => ${total}`, + ]; +} + +function runLesson04(): string[] { + interface User { + id: number; + name: string; + age: number; + } + + const user: User = { + id: 1, + name: "小王", + age: 22, + }; + + return [ + `User.name => ${user.name}`, + `User.age => ${user.age}`, + "interface 让对象结构在写代码时就固定下来。", + ]; +} + +function runLesson05(): string[] { + function getData(data: T): T { + return data; + } + + const text = getData("hello"); + const count = getData(100); + + return [ + `getData("hello") => ${text}`, + `getData(100) => ${count}`, + "泛型可以在复用函数时保留输入和输出的类型关系。", + ]; +} + +function runLesson06(): string[] { + type UserId = number | string; + + interface Profile { + id: UserId; + name: string; + age?: number; + } + + const profileA: Profile = { + id: 1, + name: "周周", + }; + + const profileB: Profile = { + id: "u-2", + name: "小林", + age: 24, + }; + + return [ + `profileA.id => ${profileA.id}`, + `profileB.id => ${profileB.id}`, + `profileB.age => ${profileB.age}`, + ]; +} + +function runLesson07(): string[] { + interface Course { + title: string; + lessons: number; + finished: boolean; + } + + const courses: Course[] = [ + { title: "TypeScript 基础", lessons: 8, finished: true }, + { title: "接口和泛型", lessons: 6, finished: false }, + ]; + + function renderCourseLines(list: Course[]): string[] { + return list.map((course) => { + const status = course.finished ? "已完成" : "学习中"; + return `${course.title} - ${course.lessons} 节 - ${status}`; + }); + } + + return renderCourseLines(courses); +} + +function runLesson08(): string[] { + type StudentId = number | string; + + interface Course { + title: string; + finished: boolean; + } + + interface Student { + id: StudentId; + name: string; + age?: number; + courses: Course[]; + } + + function pickFirst(list: T[]): T { + return list[0]; + } + + function formatStudent(student: Student): string { + const courseCount = student.courses.length; + return `${student.name} 当前有 ${courseCount} 门课程,编号是 ${student.id}`; + } + + const student: Student = { + id: "stu-1", + name: "林晨", + courses: [ + { title: "基本类型", finished: true }, + { title: "接口", finished: false }, + ], + }; + + const firstCourse = pickFirst(student.courses); + + return [ + formatStudent(student), + `第一门课程:${firstCourse.title}`, + `完成状态:${firstCourse.finished ? "已完成" : "学习中"}`, + ]; +} + +export const lessons: Lesson[] = [ + { + id: "01", + title: "JavaScript vs TypeScript", + focus: "理解为什么要引入类型系统", + summary: "从一个最简单的加法函数开始,看 TypeScript 如何把错误提前到编码阶段。", + starterCode: lesson01Starter, + answerCode: lesson01Answer, + keyPoints: [ + "TypeScript 在写代码阶段检查参数类型", + "运行前发现问题,比运行后排查问题更省成本", + "最常见的第一步就是给函数参数和返回值加类型", + ], + runDemo: runLesson01, + }, + { + id: "02", + title: "基本类型标注", + focus: "number / string / boolean", + summary: "把最常见的值类型写清楚,建立最基础的类型意识。", + starterCode: lesson02Starter, + answerCode: lesson02Answer, + keyPoints: [ + "基础值先学会标注类型", + "看到报错时先分清期望类型和实际类型", + "能写对基本类型后再往对象和函数走", + ], + runDemo: runLesson02, + }, + { + id: "03", + title: "数组和函数类型", + focus: "number[] 与函数参数/返回值", + summary: "把类型从单个值扩展到数组和函数,是进入业务代码前必须过的一关。", + starterCode: lesson03Starter, + answerCode: lesson03Answer, + keyPoints: [ + "数组里的元素类型要保持一致", + "函数参数和返回值都应该尽量明确", + "函数签名是 TypeScript 最高频的使用场景之一", + ], + runDemo: runLesson03, + }, + { + id: "04", + title: "interface 对象结构", + focus: "用接口约束对象字段", + summary: "当数据结构开始变复杂时,先定义 shape,再写数据和逻辑。", + starterCode: lesson04Starter, + answerCode: lesson04Answer, + keyPoints: [ + "对象字段越多,越应该先定义 interface", + "接口让团队协作时更容易知道一个对象该长什么样", + "字段缺失、字段类型不匹配都会更早暴露", + ], + runDemo: runLesson04, + }, + { + id: "05", + title: "泛型函数", + focus: "保持输入输出的类型关系", + summary: "泛型的重点不是复杂,而是让复用函数在不同类型下依然安全。", + starterCode: lesson05Starter, + answerCode: lesson05Answer, + keyPoints: [ + "泛型适合处理同类输入输出关系", + "输入是什么类型,输出就跟着保持什么类型", + "先理解例子,再去记住 这个写法", + ], + runDemo: runLesson05, + }, + { + id: "06", + title: "联合类型和可选属性", + focus: "number | string 与 age?", + summary: "业务数据并不总是完全整齐,联合类型和可选属性就是为这种情况准备的。", + starterCode: lesson06Starter, + answerCode: lesson06Answer, + keyPoints: [ + "联合类型描述多个合法输入分支", + "可选属性适合那些可能缺失的字段", + "真实业务里常常需要同时使用这两类能力", + ], + runDemo: runLesson06, + }, + { + id: "07", + title: "类型安全渲染", + focus: "把接口用到列表渲染里", + summary: "进入接近真实业务的场景,用类型约束一组列表数据和渲染函数。", + starterCode: lesson07Starter, + answerCode: lesson07Answer, + keyPoints: [ + "先定义 Course,再定义 Course[]", + "渲染函数应该接收明确的数据结构", + "数据结构一旦变化,相关代码会一起被提醒", + ], + runDemo: runLesson07, + }, + { + id: "08", + title: "综合小练习", + focus: "把常见类型能力组合起来", + summary: "把联合类型、接口、可选属性和泛型组合起来,做一个完整的小示例。", + starterCode: lesson08Starter, + answerCode: lesson08Answer, + keyPoints: [ + "综合练习的重点是把前面知识点串起来", + "能读懂数据模型,通常就能更稳地写业务代码", + "类型不是目的,减少错误和提升可维护性才是目的", + ], + runDemo: runLesson08, + }, +]; diff --git a/06-typescript/src/main.ts b/06-typescript/src/main.ts new file mode 100644 index 0000000..19e5168 --- /dev/null +++ b/06-typescript/src/main.ts @@ -0,0 +1,151 @@ +import "./style.css"; +import { lessons } from "./lessons"; + +function getRequiredElement( + selector: string, + parent: ParentNode = document, +): T { + const element = parent.querySelector(selector); + + if (!element) { + throw new Error(`Required element was not found: ${selector}`); + } + + return element; +} + +const app = getRequiredElement("#app"); + +app.innerHTML = ` +
+ +
+
+

06-typescript

+

+

+

+
+
+
+
+

知识点

+
+
    +
    +
    +
    +

    运行结果

    + +
    +
    +
    +
    +
    +
    +
    +

    Starter

    +
    +
    +
    +
    +
    +

    Answer

    +
    +
    +
    +
    +
    +
    +`; + +const nav = getRequiredElement(".lesson-nav"); +const title = getRequiredElement("#lesson-title"); +const focus = getRequiredElement("#lesson-focus"); +const summary = getRequiredElement("#lesson-summary"); +const pointList = getRequiredElement("#lesson-points"); +const output = getRequiredElement("#lesson-output"); +const starterCode = getRequiredElement("#starter-code"); +const answerCode = getRequiredElement("#answer-code"); +const runButton = getRequiredElement("#run-demo"); + +let activeLessonId = lessons[0]?.id ?? ""; + +function renderLessonList(): void { + nav.innerHTML = lessons + .map((lesson) => { + const isActive = lesson.id === activeLessonId; + return ` + + `; + }) + .join(""); +} + +function renderOutput(lines: string[]): void { + output.innerHTML = lines + .map((line) => `
    ${line}
    `) + .join(""); +} + +function renderActiveLesson(): void { + const lesson = lessons.find((item) => item.id === activeLessonId); + + if (!lesson) { + return; + } + + title.textContent = `${lesson.id}. ${lesson.title}`; + focus.textContent = lesson.focus; + summary.textContent = lesson.summary; + pointList.innerHTML = lesson.keyPoints + .map((item) => `
  • ${item}
  • `) + .join(""); + starterCode.textContent = lesson.starterCode; + answerCode.textContent = lesson.answerCode; + renderOutput(lesson.runDemo()); + renderLessonList(); +} + +nav.addEventListener("click", (event) => { + const target = event.target as HTMLElement; + const button = target.closest("[data-id]"); + + if (!button) { + return; + } + + activeLessonId = button.dataset.id ?? activeLessonId; + renderActiveLesson(); +}); + +runButton.addEventListener("click", () => { + const lesson = lessons.find((item) => item.id === activeLessonId); + + if (!lesson) { + return; + } + + renderOutput(lesson.runDemo()); +}); + +renderActiveLesson(); diff --git a/06-typescript/src/style.css b/06-typescript/src/style.css new file mode 100644 index 0000000..3d9b9e0 --- /dev/null +++ b/06-typescript/src/style.css @@ -0,0 +1,237 @@ +:root { + font-family: "IBM Plex Sans", "PingFang SC", "Hiragino Sans GB", sans-serif; + line-height: 1.5; + font-weight: 400; + color: #f7f4ec; + background: + radial-gradient(circle at top left, rgba(255, 191, 73, 0.22), transparent 24rem), + radial-gradient(circle at bottom right, rgba(86, 154, 255, 0.16), transparent 26rem), + #11131a; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +button, +code, +pre { + font: inherit; +} + +#app { + min-height: 100vh; +} + +.shell { + display: grid; + grid-template-columns: 320px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + padding: 28px 20px; + border-right: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(9, 10, 14, 0.62); + backdrop-filter: blur(18px); +} + +.brand { + margin-bottom: 24px; +} + +.eyebrow, +.badge { + display: inline-flex; + align-items: center; + gap: 8px; + margin: 0 0 12px; + padding: 6px 10px; + border-radius: 999px; + background: rgba(255, 191, 73, 0.14); + color: #ffcc6a; + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.brand h1 { + margin: 0 0 10px; + font-size: 28px; + line-height: 1.1; +} + +.intro, +.summary { + margin: 0; + color: rgba(247, 244, 236, 0.72); +} + +.lesson-nav { + display: grid; + gap: 10px; +} + +.lesson-link { + display: flex; + align-items: flex-start; + gap: 12px; + width: 100%; + padding: 14px 12px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 18px; + background: rgba(255, 255, 255, 0.03); + color: inherit; + text-align: left; + cursor: pointer; + transition: + transform 180ms ease, + border-color 180ms ease, + background 180ms ease; +} + +.lesson-link:hover, +.lesson-link.is-active { + transform: translateY(-1px); + border-color: rgba(255, 191, 73, 0.42); + background: rgba(255, 191, 73, 0.1); +} + +.lesson-index { + min-width: 40px; + padding: 8px 0; + border-radius: 12px; + background: rgba(255, 255, 255, 0.06); + text-align: center; + font-weight: 700; +} + +.lesson-meta { + display: grid; + gap: 4px; +} + +.lesson-meta small { + color: rgba(247, 244, 236, 0.64); +} + +.content { + padding: 28px; +} + +.hero { + margin-bottom: 20px; +} + +.focus { + margin: 0 0 10px; + color: #ffcc6a; + font-weight: 600; +} + +.grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px; + margin-bottom: 20px; +} + +.code-grid { + align-items: start; +} + +.card { + padding: 20px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 28px; + background: rgba(15, 17, 24, 0.72); + box-shadow: 0 18px 48px rgba(0, 0, 0, 0.22); +} + +.card-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; +} + +.card-head h3 { + margin: 0; + font-size: 18px; +} + +.point-list { + margin: 0; + padding-left: 18px; + color: rgba(247, 244, 236, 0.84); +} + +.point-list li + li { + margin-top: 10px; +} + +.output-panel { + display: grid; + gap: 10px; +} + +.output-line { + padding: 12px 14px; + border-radius: 16px; + background: rgba(86, 154, 255, 0.12); + color: #d8e9ff; +} + +.run-button { + padding: 10px 14px; + border: 0; + border-radius: 999px; + background: linear-gradient(135deg, #ffbf49, #ff8f48); + color: #1d1302; + font-weight: 700; + cursor: pointer; +} + +.code-card pre { + overflow: auto; + margin: 0; + padding: 18px; + border-radius: 20px; + background: #0b0d12; +} + +.code-card code { + font-family: "JetBrains Mono", "SFMono-Regular", monospace; + font-size: 13px; + white-space: pre; +} + +@media (max-width: 960px) { + .shell { + grid-template-columns: 1fr; + } + + .sidebar { + border-right: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + } + + .grid { + grid-template-columns: 1fr; + } + + .content { + padding: 20px; + } +} diff --git a/06-typescript/src/vite-env.d.ts b/06-typescript/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/06-typescript/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/06-typescript/tsconfig.json b/06-typescript/tsconfig.json new file mode 100644 index 0000000..d894758 --- /dev/null +++ b/06-typescript/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "moduleResolution": "Bundler", + "types": ["vite/client"], + "resolveJsonModule": true, + "isolatedModules": true, + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["./src/**/*.ts", "./src/**/*.d.ts"] +} diff --git a/README.md b/README.md index f9de029..9ba6c92 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ - `02-css-layout`:预留给 CSS - `03-javascript-core`:预留给 JavaScript - `04-dom-events-async`:预留给 DOM + 事件 + 异步 -- `05-typescript`:预留给 TypeScript +- `05-es6-plus`:预留给 ES6+(现代 JS) +- `06-typescript`:预留给 TypeScript ## 当前可学内容 @@ -33,15 +34,41 @@ - `starter.js` 起始代码 - `answer.js` 参考答案 -前三部分现在都已经补充到“核心主线 + 常见细分知识点”。 +现在也已经整理好 `04-dom-events-async`,里面包含: + +- DOM + 事件 + 异步讲义 +- 分阶段练习 +- `starter.html` / `starter.js` 起始代码 +- `answer.html` / `answer.js` 参考答案 + +现在也已经整理好 `05-es6-plus`,里面包含: + +- ES6+(现代 JS)讲义 +- 分阶段练习 +- `starter.html` / `starter.js` 起始代码 +- `answer.html` / `answer.js` 参考答案 + +现在也已经整理好 `06-typescript`,里面包含: + +- TypeScript(类型系统)讲义 +- 分阶段练习 +- `starter.ts` 起始代码 +- `answer.ts` 参考答案 +- `Vite + TypeScript` 学习面板 + +前六部分现在都已经补充到“核心主线 + 常见细分知识点”。 ## 使用方式 -1. 先阅读 [01-html-structure/README.md](/Volumes/Macintosh HD 1/home/front-end-example/01-html-structure/README.md) -2. 再阅读 [02-css-layout/README.md](/Volumes/Macintosh HD 1/home/front-end-example/02-css-layout/README.md) -3. 再阅读 [03-javascript-core/README.md](/Volumes/Macintosh HD 1/home/front-end-example/03-javascript-core/README.md) -4. 按顺序完成每个练习目录 -5. 先写 `starter.html`、`starter.css` 或 `starter.js` -6. 写完后再对照答案文件 +1. 先阅读 [01-html-structure/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/01-html-structure/README.md) +2. 再阅读 [02-css-layout/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/02-css-layout/README.md) +3. 再阅读 [03-javascript-core/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/03-javascript-core/README.md) +4. 再阅读 [04-dom-events-async/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/README.md) +5. 再阅读 [05-es6-plus/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/README.md) +6. 再阅读 [06-typescript/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/README.md) +7. 按顺序完成每个练习目录 +8. 先写 `starter.html`、`starter.css`、`starter.js` 或 `starter.ts` +9. 写完后再对照答案文件 +10. 学 `06-typescript` 时,也可以进入 [06-typescript](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript) 后执行 `npm install` 和 `npm run dev` -如果你后面要继续学其他知识点,我可以按同样结构继续给你补 `04-dom-events-async`、`05-typescript` 等目录。 +如果你后面要继续学其他知识点,我可以按同样结构继续给你补更多工程化目录。